import { FastifyInstance } from "fastify" import { db } from "../db" import { projects, timeEntries, users } from "../db/schema" import { eq, and, sql, inArray } from "drizzle-orm" import { z } from "zod" const ProjectSchema = z.object({ name: z.string().min(1), customerId: z.string().uuid() }) const ProjectUpdateSchema = ProjectSchema.partial() const ProjectCloneSchema = z.object({ name: z.string().optional() }) const BulkRenameSchema = z.object({ ids: z.array(z.string().uuid()).min(1), prefix: z.string() }) const BulkDeleteSchema = z.object({ ids: z.array(z.string().uuid()).min(1) }) export default async function projectRoutes(fastify: FastifyInstance) { 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 conds = [] if (isActiveDefault) conds.push(eq(projects.active, true)) if (customerId) conds.push(eq(projects.customerId, customerId)) const results = await db .select() .from(projects) .where(conds.length ? and(...conds) : undefined as any) .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.get("/:id/stats", async (request, reply) => { const { id } = request.params as { id: string } const projectExists = await db .select({ id: projects.id }) .from(projects) .where(eq(projects.id, id)) .limit(1) if (!projectExists.length) { return reply.code(404).send({ message: "Project not found" }) } const totalStats = await db .select({ totalHours: sql`sum(hours)`, entryCount: sql`count(*)`, }) .from(timeEntries) .where(eq(timeEntries.projectId, id)) const byUser = await db .select({ userId: users.id, name: users.name, hours: sql`sum(hours)`, }) .from(timeEntries) .innerJoin(users, eq(timeEntries.userId, users.id)) .where(eq(timeEntries.projectId, id)) .groupBy(users.id, users.name) const byMonth = await db .select({ month: sql`date_trunc('month', date)`, hours: sql`sum(hours)`, }) .from(timeEntries) .where(eq(timeEntries.projectId, id)) .groupBy(sql`date_trunc('month', date)`) .orderBy(sql`date_trunc('month', date)`) return { totalHours: totalStats[0]?.totalHours || 0, entryCount: totalStats[0]?.entryCount || 0, byUser, byMonth, } }) 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.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) 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.post("/bulk-rename", async (request, reply) => { const { ids, prefix } = BulkRenameSchema.parse(request.body) await db .update(projects) .set({ name: sql`${prefix} ${projects.name}` }) .where(inArray(projects.id, ids)) return { success: true } }) fastify.post("/bulk-delete", async (request, reply) => { const { ids } = BulkDeleteSchema.parse(request.body) await db .delete(projects) .where(inArray(projects.id, ids)) return { success: true } }) }