From 4610ff24b86f1fcc552257fbcca2d89b5fe06a29 Mon Sep 17 00:00:00 2001 From: "Dennis (via Claude+Gemma)" Date: Sat, 23 May 2026 04:45:09 +0200 Subject: [PATCH] feat(projects-crud): Projects-CRUD: API + Web-Page mit Customer-Picker [tsc:fail] --- .phase2-state.json | 7 +- GENERATION_LOG.md | 30 +++++++ apps/api/src/routes/projects.ts | 115 +++++++++++++++++++++++++++ apps/web/src/pages/Projects.tsx | 137 ++++++++++++++++++++++++++++++++ 4 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 apps/api/src/routes/projects.ts create mode 100644 apps/web/src/pages/Projects.tsx diff --git a/.phase2-state.json b/.phase2-state.json index 64cd8bb..600e847 100644 --- a/.phase2-state.json +++ b/.phase2-state.json @@ -1,5 +1,8 @@ { "completed_features": [], - "current_feature": "customers-crud", - "started_at": "2026-05-23T04:42:59.289476" + "current_feature": "projects-crud", + "started_at": "2026-05-23T04:42:59.289476", + "attempted_features": [ + "customers-crud" + ] } \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index 1bf9743..6ed898f 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -196,3 +196,33 @@ src/routes/customers.ts(14,49): error TS7006: Parameter 'reply' implicitly has a src/routes/customers.ts(22,11): error TS2339: Property 'get' does not exist on type 'FastifyPluginAsync'. src/routes/customers.ts(22,27): error TS7006: Parameter 'request' implicitly has an 'any' type. src/routes/customers.ts(22,36): error TS7006: Parameter +- `04:43:56` **INFO** Committed feature customers-crud +- `04:43:57` **INFO** Pushed: rc=0 +- `04:43:57` **WARN** ⚠️ Feature customers-crud partial — moving on + +## Feature: projects-crud (2026-05-23 04:43:57) + +- `04:43:57` **INFO** Description: Projects-CRUD: API + Web-Page mit Customer-Picker +- `04:43:57` **INFO** Files: 2 +- `04:43:57` **INFO** Generating apps/api/src/routes/projects.ts (Fastify-Plugin /api/projects. CRUD wie customers.ts. Felder: name, cus…) +- `04:44:23` **INFO** wrote 2891 chars in 26.1s (attempt 1) +- `04:44:23` **INFO** Generating apps/web/src/pages/Projects.tsx (Projects-Page. Liste + Create-Form mit name (text) + customerId (selec…) +- `04:45:07` **INFO** wrote 5600 chars in 44.8s (attempt 1) +- `04:45:07` **INFO** Running tsc --noEmit on api… +- `04:45:09` **WARN** tsc errors: +src/routes/auth.ts(9,11): error TS2339: Property 'post' does not exist on type 'FastifyPluginAsync'. +src/routes/auth.ts(9,33): error TS7006: Parameter 'request' implicitly has an 'any' type. +src/routes/auth.ts(9,42): error TS7006: Parameter 'reply' implicitly has an 'any' type. +src/routes/auth.ts(22,27): error TS2339: Property 'jwt' does not exist on type 'FastifyPluginAsync'. +src/routes/auth.ts(44,11): error TS2339: Property 'get' does not exist on type 'FastifyPluginAsync'. +src/routes/auth.ts(44,29): error TS7006: Parameter 'request' implicitly has an 'any' type. +src/routes/auth.ts(44,38): error TS7006: Parameter 'reply' implicitly has an 'any' type. +src/routes/auth.ts(70,11): error TS2339: Property 'post' does not exist on type 'FastifyPluginAsync'. +src/routes/auth.ts(70,34): error TS7006: Parameter 'request' implicitly has an 'any' type. +src/routes/auth.ts(70,43): error TS7006: Parameter 'reply' implicitly has an 'any' type. +src/routes/customers.ts(14,11): error TS2339: Property 'addHook' does not exist on type 'FastifyPluginAsync'. +src/routes/customers.ts(14,40): error TS7006: Parameter 'request' implicitly has an 'any' type. +src/routes/customers.ts(14,49): error TS7006: Parameter 'reply' implicitly has an 'any' type. +src/routes/customers.ts(22,11): error TS2339: Property 'get' does not exist on type 'FastifyPluginAsync'. +src/routes/customers.ts(22,27): error TS7006: Parameter 'request' implicitly has an 'any' type. +src/routes/customers.ts(22,36): error TS7006: Parameter diff --git a/apps/api/src/routes/projects.ts b/apps/api/src/routes/projects.ts new file mode 100644 index 0000000..9f0f322 --- /dev/null +++ b/apps/api/src/routes/projects.ts @@ -0,0 +1,115 @@ +import { FastifyPluginAsync } from "fastify" +import { db } from "../db" +import { projects } from "../db/schema" +import { eq } from "drizzle-orm" +import { z } from "zod" + +const ProjectSchema = z.object({ + name: z.string().min(1), + customerId: z.string().uuid() +}) + +const ProjectUpdateSchema = ProjectSchema.partial() + +export default async function projectRoutes(fastify: FastifyPluginAsync) { + fastify.addHook("preHandler", async (request, reply) => { + try { + await request.jwtVerify() + } catch (err) { + return reply.code(401).send({ message: "Unauthorized" }) + } + }) + + fastify.get("/", async (request, reply) => { + const { onlyActive, customerId } = request.query as { onlyActive?: string, customerId?: string } + const isActiveDefault = onlyActive !== "false" + + const filters = [] + if (isActiveDefault) { + filters.push(eq(projects.active, true)) + } + if (customerId) { + filters.push(eq(projects.customerId, customerId)) + } + + const results = await db + .select() + .from(projects) + .where(filters.length > 0 ? and(...filters) : undefined) + .orderBy(projects.name) + + return results + }) + + fastify.get("/:id", async (request, reply) => { + const { id } = request.params as { id: string } + + const [project] = await db + .select() + .from(projects) + .where(eq(projects.id, id)) + .limit(1) + + if (!project) { + return reply.code(404).send({ message: "Project not found" }) + } + + return project + }) + + fastify.post("/", async (request, reply) => { + const body = ProjectSchema.parse(request.body) + + const [project] = await db + .insert(projects) + .values({ + name: body.name, + customerId: body.customerId + }) + .returning() + + return reply.code(201).send(project) + }) + + fastify.patch("/:id", async (request, reply) => { + const { id } = request.params as { id: string } + const body = ProjectUpdateSchema.parse(request.body) + + const [project] = await db + .update(projects) + .set(body) + .where(eq(projects.id, id)) + .returning() + + if (!project) { + return reply.code(404).send({ message: "Project not found" }) + } + + return project + }) + + fastify.delete("/:id", async (request, reply) => { + const { id } = request.params as { id: string } + + const [project] = await db + .update(projects) + .set({ active: false }) + .where(eq(projects.id, id)) + .returning() + + if (!project) { + return reply.code(404).send({ message: "Project not found" }) + } + + return reply.code(204).send() + }) +} + +// Helper for the filter array since 'and' is needed +function and(...conditions: any[]) { + return conditions.length > 0 ? conditions : undefined +} + +// Overriding the local 'and' to use drizzle's actual 'and' +import { and as drizzleAnd } from "drizzle-orm" +const andHelper = drizzleAnd; \ No newline at end of file diff --git a/apps/web/src/pages/Projects.tsx b/apps/web/src/pages/Projects.tsx new file mode 100644 index 0000000..ebbf223 --- /dev/null +++ b/apps/web/src/pages/Projects.tsx @@ -0,0 +1,137 @@ +import { useState } from "react" +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" +import { api } from "../lib/api" + +export default function Projects() { + const queryClient = useQueryClient() + const [name, setName] = useState("") + const [customerId, setCustomerId] = useState("") + + const { data: projects, isLoading: projectsLoading, isError: projectsError } = useQuery({ + queryKey: ["projects"], + queryFn: () => api.listProjects() + }) + + const { data: customers, isLoading: customersLoading } = useQuery({ + queryKey: ["customers"], + queryFn: () => api.listCustomers() + }) + + const createMutation = useMutation({ + mutationFn: ({ name, customerId }: { name: string; customerId: string }) => + api.createProject({ name, customerId }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["projects"] }) + setName("") + setCustomerId("") + } + }) + + const deleteMutation = useMutation({ + mutationFn: (id: string) => api.deleteProject(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["projects"] }) + } + }) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (!name.trim() || !customerId) return + createMutation.mutate({ name, customerId }) + } + + if (projectsLoading) return
Loading projects...
+ if (projectsError) return
Error loading projects.
+ + return ( +
+
+

Projects

+

Manage your project portfolio

+
+ +
+

Add New Project

+
+
+ + setName(e.target.value)} + placeholder="e.g. Website Redesign" + /> +
+
+ + +
+
+ +
+
+
+ +
+ + + + + + + + + + {projects && projects.length > 0 ? ( + projects.map((project: any) => ( + + + + + + )) + ) : ( + + + + )} + +
Project NameCustomerActions
{project.name} + {customers?.find((c: any) => c.id === project.customerId)?.name || "Unknown Customer"} + + +
No projects found. Create one above.
+
+
+ ) +} \ No newline at end of file