import { FastifyInstance } from "fastify"; import prisma from "../../config/database"; import { requirePermission, requireAnyPermission } 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 { CreateIssuedOrderSchema, UpdateIssuedOrderSchema, } from "../../schemas/issued-orders.schema"; import { listIssuedOrders, getIssuedOrderTotals, getIssuedOrder, createIssuedOrder, updateIssuedOrder, deleteIssuedOrder, getNextIssuedOrderNumberPreview, } from "../../services/issued-orders.service"; export default async function issuedOrdersRoutes(fastify: FastifyInstance) { // GET /api/admin/issued-orders fastify.get( "/", { preHandler: requirePermission("orders.view") }, async (request, reply) => { const query = request.query as Record; const { page, limit, skip, order, search } = parsePagination(query); const result = await listIssuedOrders({ page, limit, skip, sort: String(query.sort || ""), order, search, status: query.status ? String(query.status) : undefined, supplier_id: query.supplier_id ? Number(query.supplier_id) : undefined, month: query.month ? Number(query.month) : undefined, year: query.year ? Number(query.year) : undefined, }); return paginated( reply, result.data, buildPaginationMeta(result.total, page, limit), ); }, ); // GET /api/admin/issued-orders/next-number fastify.get( "/next-number", { preHandler: requirePermission("orders.create") }, async (_request, reply) => { return success(reply, await getNextIssuedOrderNumberPreview()); }, ); // GET /api/admin/issued-orders/stats — per-currency total (incl. VAT) summed // over the WHOLE filtered set (not one page). Registered BEFORE "/:id" so the // literal "stats" path isn't captured as an order id. fastify.get( "/stats", { preHandler: requirePermission("orders.view") }, async (request, reply) => { const query = request.query as Record; const result = await getIssuedOrderTotals({ search: query.search ? String(query.search) : undefined, status: query.status ? String(query.status) : undefined, supplier_id: query.supplier_id ? Number(query.supplier_id) : undefined, month: query.month ? Number(query.month) : undefined, year: query.year ? Number(query.year) : undefined, }); return success(reply, { totals: result.totals }); }, ); // GET /api/admin/issued-orders/suppliers — lightweight supplier lookup for // the PO supplier picker. Guarded by the ORDERS permissions (any of // view/create/edit), NOT warehouse.manage: the warehouse suppliers CRUD is // gated by warehouse.manage, which orders users typically lack — without // this endpoint they couldn't populate the picker. Registered BEFORE "/:id" // so the literal "suppliers" path isn't captured as an order id. fastify.get( "/suppliers", { preHandler: requireAnyPermission( "orders.view", "orders.create", "orders.edit", ), }, async (_request, reply) => { const suppliers = await prisma.sklad_suppliers.findMany({ where: { is_active: true }, select: { id: true, name: true, ico: true, dic: true, address: true, email: true, phone: true, }, // id tiebreak so same-name suppliers sort deterministically. orderBy: [{ name: "asc" }, { id: "asc" }], }); return success(reply, suppliers); }, ); // GET /api/admin/issued-orders/:id 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 getIssuedOrder(id); if (!order) return error(reply, "Objednávka nenalezena", 404); return success(reply, order); }, ); // POST /api/admin/issued-orders fastify.post( "/", { preHandler: requirePermission("orders.create") }, async (request, reply) => { const parsed = parseBody(CreateIssuedOrderSchema, request.body); if ("error" in parsed) return error(reply, parsed.error, 400); const order = await createIssuedOrder(parsed.data); if ("error" in order) { if (order.error === "supplier_not_found") return error(reply, "Dodavatel nenalezen", 400); return error(reply, "Neznámá chyba", 500); } await logAudit({ request, authData: request.authData, action: "create", entityType: "issued_order", entityId: order.id, description: `Vytvořena vydaná objednávka ${order.po_number ?? "koncept"}`, }); return success( reply, { id: order.id, po_number: order.po_number }, 201, "Objednávka byla vytvořena", ); }, ); // PUT /api/admin/issued-orders/:id 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(UpdateIssuedOrderSchema, request.body); if ("error" in parsed) return error(reply, parsed.error, 400); const result = await updateIssuedOrder(id, parsed.data); if ("error" in result) { if (result.error === "not_found") return error(reply, "Objednávka nenalezena", 404); if (result.error === "supplier_not_found") return error(reply, "Dodavatel nenalezen", 400); if (result.error === "invalid_transition") return error( reply, `Neplatný přechod stavu z "${result.currentStatus}" na "${result.newStatus}"`, 400, ); return error(reply, "Neznámá chyba", 500); } await logAudit({ request, authData: request.authData, action: "update", entityType: "issued_order", entityId: id, description: `Upravena vydaná objednávka ${result.po_number}`, }); return success(reply, { id }, 200, "Objednávka byla aktualizována"); }, ); // DELETE /api/admin/issued-orders/:id 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 existing = await deleteIssuedOrder(id); if (!existing) return error(reply, "Objednávka nenalezena", 404); await logAudit({ request, authData: request.authData, action: "delete", entityType: "issued_order", entityId: id, description: `Smazána vydaná objednávka ${existing.po_number}`, }); return success(reply, null, 200, "Objednávka smazána"); }, ); }