import { FastifyInstance } from "fastify"; import prisma from "../../config/database"; import { requireAuth, requirePermission } from "../../middleware/auth"; import { logAudit } from "../../services/audit"; import { success, error, parseId } from "../../utils/response"; import { parsePagination, buildPaginationMeta } from "../../utils/pagination"; import { parseBody } from "../../schemas/common"; import { CreateCustomerSchema, UpdateCustomerSchema, } from "../../schemas/customers.schema"; const ALLOWED_SORT_FIELDS = ["id", "name", "company_id", "city", "country"]; /** Encode custom_fields + customer_field_order into a single JSON blob (matching PHP format) */ function encodeCustomFields( fields: unknown, fieldOrder: unknown, ): string | null { const f = Array.isArray(fields) ? fields : []; const o = Array.isArray(fieldOrder) ? fieldOrder : []; if (f.length === 0 && o.length === 0) return null; return JSON.stringify({ fields: f, field_order: o }); } /** Decode custom_fields JSON blob into separate fields + field_order for frontend */ function decodeCustomFields(raw: string | null): { custom_fields: unknown[]; customer_field_order: string[]; } { if (!raw) return { custom_fields: [], customer_field_order: [] }; try { const parsed = JSON.parse(raw); // PHP format: { fields: [...], field_order: [...] } if ( parsed && typeof parsed === "object" && !Array.isArray(parsed) && "fields" in parsed ) { return { custom_fields: parsed.fields || [], customer_field_order: parsed.field_order || [], }; } // Legacy TS format: raw array if (Array.isArray(parsed)) { return { custom_fields: parsed, customer_field_order: [] }; } return { custom_fields: [], customer_field_order: [] }; } catch { return { custom_fields: [], customer_field_order: [] }; } } export default async function customersRoutes( fastify: FastifyInstance, ): Promise { fastify.get( "/", { preHandler: requirePermission("customers.view") }, async (request, reply) => { const { page, limit, skip, sort, order, search } = parsePagination( request.query as Record, ); const sortField = ALLOWED_SORT_FIELDS.includes(sort) ? sort : "name"; const where = search ? { OR: [ { name: { contains: search } }, { company_id: { contains: search } }, ], } : {}; const [customers, total] = await Promise.all([ prisma.customers.findMany({ where, skip, take: limit, orderBy: { [sortField]: order }, include: { _count: { select: { quotations: true } } }, }), prisma.customers.count({ where }), ]); const enriched = customers.map((c) => { const { custom_fields, customer_field_order } = decodeCustomFields( c.custom_fields, ); return { ...c, custom_fields, customer_field_order, quotation_count: c._count?.quotations ?? 0, }; }); return reply.send({ success: true, data: enriched, pagination: buildPaginationMeta(total, page, limit), }); }, ); fastify.get<{ Params: { id: string } }>( "/:id", { preHandler: requirePermission("customers.view") }, async (request, reply) => { const id = parseId(request.params.id, reply); if (id === null) return; const customer = await prisma.customers.findUnique({ where: { id } }); if (!customer) return error(reply, "Zákazník nenalezen", 404); const { custom_fields, customer_field_order } = decodeCustomFields( customer.custom_fields, ); return success(reply, { ...customer, custom_fields, customer_field_order, }); }, ); fastify.post( "/", { preHandler: requirePermission("customers.create") }, async (request, reply) => { const parsed = parseBody(CreateCustomerSchema, request.body); if ("error" in parsed) return error(reply, parsed.error, 400); const body = parsed.data; const name = body.name; const customer = await prisma.customers.create({ data: { name, street: body.street ? String(body.street) : null, city: body.city ? String(body.city) : null, postal_code: body.postal_code ? String(body.postal_code) : null, country: body.country ? String(body.country) : null, company_id: body.company_id ? String(body.company_id) : null, vat_id: body.vat_id ? String(body.vat_id) : null, custom_fields: encodeCustomFields( body.custom_fields, body.customer_field_order, ), }, }); await logAudit({ request, authData: request.authData, action: "create", entityType: "customer", entityId: customer.id, description: `Vytvořen zákazník ${customer.name}`, }); return success(reply, { id: customer.id }, 201, "Zákazník byl vytvořen"); }, ); fastify.put<{ Params: { id: string } }>( "/:id", { preHandler: requirePermission("customers.edit") }, async (request, reply) => { const id = parseId(request.params.id, reply); if (id === null) return; const parsed = parseBody(UpdateCustomerSchema, request.body); if ("error" in parsed) return error(reply, parsed.error, 400); const body = parsed.data; const existing = await prisma.customers.findUnique({ where: { id } }); if (!existing) return error(reply, "Zákazník nenalezen", 404); await prisma.customers.update({ where: { id }, data: { name: body.name !== undefined ? String(body.name) : undefined, street: body.street !== undefined ? body.street ? String(body.street) : null : undefined, city: body.city !== undefined ? body.city ? String(body.city) : null : undefined, postal_code: body.postal_code !== undefined ? body.postal_code ? String(body.postal_code) : null : undefined, country: body.country !== undefined ? body.country ? String(body.country) : null : undefined, company_id: body.company_id !== undefined ? body.company_id ? String(body.company_id) : null : undefined, vat_id: body.vat_id !== undefined ? body.vat_id ? String(body.vat_id) : null : undefined, custom_fields: body.custom_fields !== undefined ? encodeCustomFields( body.custom_fields, body.customer_field_order, ) : undefined, }, }); await logAudit({ request, authData: request.authData, action: "update", entityType: "customer", entityId: id, description: `Upraven zákazník ${existing.name}`, }); return success(reply, { id }, 200, "Zákazník byl uložen"); }, ); fastify.delete<{ Params: { id: string } }>( "/:id", { preHandler: requirePermission("customers.delete") }, async (request, reply) => { const id = parseId(request.params.id, reply); if (id === null) return; const existing = await prisma.customers.findUnique({ where: { id } }); if (!existing) return error(reply, "Zákazník nenalezen", 404); // Check for FK references before deleting const [quotCount, orderCount, invoiceCount, projectCount] = await Promise.all([ prisma.quotations.count({ where: { customer_id: id } }), prisma.orders.count({ where: { customer_id: id } }), prisma.invoices.count({ where: { customer_id: id } }), prisma.projects.count({ where: { customer_id: id } }), ]); if (quotCount + orderCount + invoiceCount + projectCount > 0) { return error( reply, "Zákazníka nelze smazat — existují propojené nabídky, objednávky, faktury nebo projekty", 400, ); } await prisma.customers.delete({ where: { id } }); await logAudit({ request, authData: request.authData, action: "delete", entityType: "customer", entityId: id, description: `Smazán zákazník ${existing.name}`, }); return success(reply, null, 200, "Zákazník smazán"); }, ); }