import { FastifyInstance } from "fastify"; import { requirePermission } from "../../middleware/auth"; import { logAudit } from "../../services/audit"; import { success, error, parseId, paginated } from "../../utils/response"; import { parsePagination, buildPaginationMeta } from "../../utils/pagination"; import { parseBody } from "../../schemas/common"; import { config } from "../../config/env"; import { CreateOrderFromQuotationSchema, CreateOrderSchema, UpdateOrderSchema, } from "../../schemas/orders.schema"; import { listOrders, getOrder, getOrderAttachment, createOrderFromQuotation, createOrder, updateOrder, deleteOrder, getNextOrderNumber, } from "../../services/orders.service"; import multipart from "@fastify/multipart"; export default async function ordersRoutes( fastify: FastifyInstance, ): Promise { await fastify.register(multipart, { limits: { fileSize: config.nas.maxUploadSize }, }); // GET /api/admin/orders/next-number fastify.get( "/next-number", { preHandler: requirePermission("orders.create") }, async (_request, reply) => { const number = await getNextOrderNumber(); return success(reply, { number, next_number: number }); }, ); fastify.get( "/", { preHandler: requirePermission("orders.view") }, async (request, reply) => { const query = request.query as Record; const { page, limit, skip, sort, order } = parsePagination(query); const result = await listOrders({ page, limit, skip, sort, order, status: query.status ? String(query.status) : undefined, customer_id: query.customer_id ? Number(query.customer_id) : undefined, }); return paginated( reply, result.data, buildPaginationMeta(result.total, result.page, result.limit), ); }, ); fastify.get<{ Params: { id: string } }>( "/:id", { preHandler: requirePermission("orders.view") }, async (request, reply) => { const id = parseId(request.params.id, reply); if (id === null) return; const order = await getOrder(id); if (!order) return error(reply, "Objednávka nenalezena", 404); return success(reply, order); }, ); // GET /api/admin/orders/:id/attachment fastify.get<{ Params: { id: string } }>( "/:id/attachment", { preHandler: requirePermission("orders.view") }, async (request, reply) => { const id = parseId(request.params.id, reply); if (id === null) return; const attachment = await getOrderAttachment(id); if (!attachment) return error(reply, "Příloha nenalezena", 404); const safeFilename = attachment.filename.replace(/[\r\n"\\/]/g, ""); return reply .type("application/pdf") .header("Content-Disposition", `inline; filename="${safeFilename}"`) .send(attachment.data); }, ); // POST /api/admin/orders — handles both JSON (manual) and multipart (from quotation) fastify.post( "/", { preHandler: requirePermission("orders.create") }, async (request, reply) => { const isMultipart = request.headers["content-type"]?.includes("multipart"); if (isMultipart) { const fields: Record = {}; let attachmentBuffer: Buffer | null = null; let attachmentName: string | null = null; const parts = request.parts(); for await (const part of parts) { if (part.type === "field") { fields[part.fieldname] = String(part.value); } else if (part.type === "file" && part.fieldname === "attachment") { attachmentBuffer = await part.toBuffer(); attachmentName = part.filename; } } // From-quotation flow (multipart with attachment) if (fields.quotationId) { // Validate via the same schema as the JSON path — `intIdFromForm` // is NaN-safe and rejects ≤0 / non-integer (raw parseInt would // silently yield NaN). const fromQuotParsed = parseBody(CreateOrderFromQuotationSchema, { quotationId: fields.quotationId, customerOrderNumber: fields.customerOrderNumber, }); if ("error" in fromQuotParsed) return error(reply, fromQuotParsed.error, 400); const quotationId = fromQuotParsed.data.quotationId; const customerOrderNumber = fromQuotParsed.data.customerOrderNumber; const result = await createOrderFromQuotation({ quotationId, customerOrderNumber, attachmentBuffer, attachmentName, }); if ("error" in result) return error(reply, result.error!, result.status!); await logAudit({ request, authData: request.authData, action: "create", entityType: "order", entityId: result.data.order_id, description: `Vytvořena objednávka ${result.data.order_number} z nabídky #${result.data.quotationId}`, }); return success( reply, { order_id: result.data.order_id, id: result.data.id, order_number: result.data.order_number, }, 201, "Objednávka byla vytvořena", ); } // Manual order (multipart, with optional PO attachment) const rawManual: Record = { ...fields }; if (typeof fields.items === "string" && fields.items.length > 0) { try { rawManual.items = JSON.parse(fields.items); } catch { return error(reply, "Neplatná data položek", 400); } } const manualParsed = parseBody(CreateOrderSchema, rawManual); if ("error" in manualParsed) return error(reply, manualParsed.error, 400); const result = await createOrder( manualParsed.data, attachmentBuffer, attachmentName, ); if ("error" in result) return error(reply, result.error!, result.status!); await logAudit({ request, authData: request.authData, action: "create", entityType: "order", entityId: result.id, description: `Vytvořena objednávka ${result.order_number}`, }); if (result.project_id) { await logAudit({ request, authData: request.authData, action: "create", entityType: "project", entityId: result.project_id, description: `Vytvořen projekt k objednávce ${result.order_number}`, }); } return success( reply, { id: result.id }, 201, "Objednávka byla vytvořena", ); } // === JSON body — either from-quotation (no attachment) or manual order === const rawBody = request.body as Record; // From-quotation flow via JSON (no attachment) if (rawBody.quotationId) { const fromQuotParsed = parseBody( CreateOrderFromQuotationSchema, rawBody, ); if ("error" in fromQuotParsed) return error(reply, fromQuotParsed.error, 400); const quotationId = fromQuotParsed.data.quotationId; const customerOrderNumber = fromQuotParsed.data.customerOrderNumber; if (!quotationId || isNaN(quotationId)) { return error(reply, "Chybí ID nabídky", 400); } const result = await createOrderFromQuotation({ quotationId, customerOrderNumber, }); if ("error" in result) return error(reply, result.error!, result.status!); await logAudit({ request, authData: request.authData, action: "create", entityType: "order", entityId: result.data.order_id, description: `Vytvořena objednávka ${result.data.order_number} z nabídky #${result.data.quotationId}`, }); return success( reply, { order_id: result.data.order_id, id: result.data.id, order_number: result.data.order_number, }, 201, "Objednávka byla vytvořena", ); } // Manual order creation const manualParsed = parseBody(CreateOrderSchema, rawBody); if ("error" in manualParsed) return error(reply, manualParsed.error, 400); const body = manualParsed.data; const result = await createOrder(body); if ("error" in result) return error(reply, result.error!, result.status!); await logAudit({ request, authData: request.authData, action: "create", entityType: "order", entityId: result.id, description: `Vytvořena objednávka ${result.order_number}`, }); if (result.project_id) { await logAudit({ request, authData: request.authData, action: "create", entityType: "project", entityId: result.project_id, description: `Vytvořen projekt k objednávce ${result.order_number}`, }); } return success( reply, { id: result.id }, 201, "Objednávka byla vytvořena", ); }, ); fastify.put<{ Params: { id: string } }>( "/:id", { preHandler: requirePermission("orders.edit") }, async (request, reply) => { const id = parseId(request.params.id, reply); if (id === null) return; const parsed = parseBody(UpdateOrderSchema, request.body); if ("error" in parsed) return error(reply, parsed.error, 400); const result = await updateOrder(id, parsed.data); if ("error" in result) return error(reply, result.error!, result.status!); await logAudit({ request, authData: request.authData, action: "update", entityType: "order", entityId: id, description: `Upravena objednávka ${result.data.order_number}`, }); return success(reply, { id }, 200, "Objednávka byla uložena"); }, ); fastify.delete<{ Params: { id: string } }>( "/:id", { preHandler: requirePermission("orders.delete") }, async (request, reply) => { const id = parseId(request.params.id, reply); if (id === null) return; const body = request.body as Record | undefined; const deleteFiles = !!body?.delete_files; const result = await deleteOrder(id, deleteFiles); if ("error" in result) return error(reply, result.error!, result.status!); await logAudit({ request, authData: request.authData, action: "delete", entityType: "order", entityId: id, description: `Smazána objednávka ${result.data.order_number}`, }); return success(reply, null, 200, "Objednávka smazána"); }, ); }