import { FastifyInstance } from "fastify" import { db } from "../db" import { customers, projects, timeEntries } from "../db/schema" import { eq, and, inArray, desc } from "drizzle-orm" import { z } from "zod" const CustomerSchema = z.object({ name: z.string().min(1) }) const CustomerUpdateSchema = CustomerSchema.partial() const MergeSchema = z.object({ targetId: z.string().uuid() }) export default async function customerRoutes(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 } = request.query as { onlyActive?: string } const isActiveDefault = onlyActive !== "false" const results = await db .select() .from(customers) .where(isActiveDefault ? eq(customers.active, true) : undefined) .orderBy(customers.name) return results }) fastify.get("/:id", async (request, reply) => { const { id } = request.params as { id: string } const [customer] = await db .select() .from(customers) .where(eq(customers.id, id)) .limit(1) if (!customer) { return reply.code(404).send({ message: "Customer not found" }) } return customer }) fastify.get("/:id/projects", async (request, reply) => { const { id } = request.params as { id: string } const results = await db .select() .from(projects) .where(eq(projects.customerId, id)) .orderBy(projects.name) return results }) fastify.get("/:id/time-entries", async (request, reply) => { const { id } = request.params as { id: string } const customerProjects = await db .select({ id: projects.id }) .from(projects) .where(eq(projects.customerId, id)) if (customerProjects.length === 0) { return [] } const projectIds = customerProjects.map((p) => p.id) const results = await db .select() .from(timeEntries) .where(inArray(timeEntries.projectId, projectIds)) .orderBy(desc(timeEntries.startTime)) .limit(50) return results }) fastify.post("/", async (request, reply) => { const body = CustomerSchema.parse(request.body) const [customer] = await db .insert(customers) .values({ name: body.name }) .returning() return reply.code(201).send(customer) }) fastify.patch("/:id", async (request, reply) => { const { id } = request.params as { id: string } const body = CustomerUpdateSchema.parse(request.body) const [customer] = await db .update(customers) .set(body) .where(eq(customers.id, id)) .returning() if (!customer) { return reply.code(404).send({ message: "Customer not found" }) } return customer }) fastify.delete("/:id", async (request, reply) => { const { id } = request.params as { id: string } const [customer] = await db .update(customers) .set({ active: false }) .where(eq(customers.id, id)) .returning() if (!customer) { return reply.code(404).send({ message: "Customer not found" }) } return reply.code(204).send() }) fastify.post("/import", async (request, reply) => { const user = request.user as any if (user?.role !== "admin") { return reply.code(403).send({ message: "Admin privileges required" }) } const data = await request.file() if (!data) { return reply.code(400).send({ message: "No file uploaded" }) } const content = await data.toBuffer() const csvText = content.toString("utf-8") const lines = csvText.split(/\r?\n/).filter(line => line.trim()) const rows = lines.slice(1) let imported = 0 const errors: string[] = [] for (let i = 0; i < rows.length; i++) { try { const [name, activeStr] = rows[i].split(",").map(s => s.trim()) if (!name) throw new Error("Name is required") await db.insert(customers).values({ name, active: activeStr === "true" }) imported++ } catch (e: any) { errors.push(`Row ${i + 2}: ${e.message}`) } } return { imported, errors } }) fastify.post("/:id/merge", async (request, reply) => { const user = request.user as any if (user?.role !== "admin") { return reply.code(403).send({ message: "Admin privileges required" }) } const { id: sourceId } = request.params as { id: string } const { targetId } = MergeSchema.parse(request.body) if (sourceId === targetId) { return reply.code(400).send({ message: "Source and target customer must be different" }) } await db.transaction(async (tx) => { // Move all projects from source to target await tx .update(projects) .set({ customerId: targetId }) .where(eq(projects.customerId, sourceId)) // Deactivate source customer const [customer] = await tx .update(customers) .set({ active: false }) .where(eq(customers.id, sourceId)) .returning() if (!customer) { throw new Error("Source customer not found") } }) return reply.code(200).send({ message: "Customers merged successfully" }) }) }