diff --git a/.phase11-state.json b/.phase11-state.json index e94acf0..3a1dba3 100644 --- a/.phase11-state.json +++ b/.phase11-state.json @@ -1,9 +1,10 @@ { "completed_features": [], - "current_feature": "customer-archive", + "current_feature": "project-cloning", "started_at": "2026-05-23T06:21:46.924268", "attempted_features": [ "onboarding-tour", - "time-entry-csv-import" + "time-entry-csv-import", + "customer-archive" ] } \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index 21aeb18..61d4fa1 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -1361,3 +1361,22 @@ src/index.ts(27,25): error TS2769: No overload matches this call. Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTy +- `06:27:29` **INFO** Committed feature customer-archive +- `06:27:29` **INFO** Pushed: rc=0 + +## Phase-3 Feature: project-cloning (2026-05-23 06:27:29) + +- `06:27:29` **INFO** Description: Project-Clone Endpoint + UI-Button +- `06:27:29` **INFO** Generating apps/api/src/routes/projects.ts (ERWEITERT — behalte alles. Füge POST /:id/clone (body optional: {name}…) +- `06:28:12` **INFO** wrote 4670 chars in 43.1s (attempt 1) +- `06:28:12` **INFO** Generating apps/web/src/pages/Projects.tsx (ERWEITERT — füge Clone-Button (Copy-Icon lucide-react) pro Row. Mutati…) +- `06:29:02` **INFO** wrote 6180 chars in 50.1s (attempt 1) +- `06:29:02` **INFO** Running tsc --noEmit on api… +- `06:29:04` **WARN** tsc errors: +src/index.ts(27,25): error TS2769: No overload matches this call. + Overload 1 of 3, '(plugin: FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. + Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. + Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTypeProvider>, opts: { ...; }, done: (err?: Error | undefined) => void): void'. + Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. + Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. + Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTy diff --git a/apps/api/src/routes/projects.ts b/apps/api/src/routes/projects.ts index f7535ea..0287fd0 100644 --- a/apps/api/src/routes/projects.ts +++ b/apps/api/src/routes/projects.ts @@ -11,6 +11,10 @@ const ProjectSchema = z.object({ const ProjectUpdateSchema = ProjectSchema.partial() +const ProjectCloneSchema = z.object({ + name: z.string().optional() +}) + export default async function projectRoutes(fastify: FastifyInstance) { fastify.addHook("preHandler", async (request, reply) => { try { @@ -117,6 +121,32 @@ export default async function projectRoutes(fastify: FastifyInstance) { return reply.code(201).send(project) }) + fastify.post("/:id/clone", async (request, reply) => { + const { id } = request.params as { id: string } + const body = ProjectCloneSchema.parse(request.body || {}) + + const [original] = await db + .select() + .from(projects) + .where(eq(projects.id, id)) + .limit(1) + + if (!original) { + return reply.code(404).send({ message: "Project not found" }) + } + + const [cloned] = await db + .insert(projects) + .values({ + name: body.name || `${original.name} (Kopie)`, + customerId: original.customerId, + active: original.active + }) + .returning() + + return reply.code(201).send(cloned) + }) + fastify.patch("/:id", async (request, reply) => { const { id } = request.params as { id: string } const body = ProjectUpdateSchema.parse(request.body) diff --git a/apps/web/src/pages/Projects.tsx b/apps/web/src/pages/Projects.tsx index ebbf223..959727b 100644 --- a/apps/web/src/pages/Projects.tsx +++ b/apps/web/src/pages/Projects.tsx @@ -1,5 +1,6 @@ import { useState } from "react" import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" +import { Copy, Trash2 } from "lucide-react" import { api } from "../lib/api" export default function Projects() { @@ -27,6 +28,13 @@ export default function Projects() { } }) + const cloneMutation = useMutation({ + mutationFn: (id: string) => api.cloneProject(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["projects"] }) + } + }) + const deleteMutation = useMutation({ mutationFn: (id: string) => api.deleteProject(id), onSuccess: () => { @@ -102,31 +110,37 @@ export default function Projects() { - {projects && projects.length > 0 ? ( - projects.map((project: any) => ( - - {project.name} - - {customers?.find((c: any) => c.id === project.customerId)?.name || "Unknown Customer"} - - - - - - )) - ) : ( + {projects?.map((project: any) => ( + + {project.name} + {project.customer?.name || "N/A"} + + + + + + ))} + {projects?.length === 0 && ( - No projects found. Create one above. + No projects found. )}