From 9f4b8a897a5fae4aeb5e7b15f292400c1c826d41 Mon Sep 17 00:00:00 2001 From: BOHA Date: Tue, 9 Jun 2026 11:26:32 +0200 Subject: [PATCH] feat(orders): issued-orders CRUD routes + audit entity type Co-Authored-By: Claude Opus 4.8 (1M context) --- src/admin/lib/entityTypeLabels.ts | 1 + src/routes/admin/issued-orders.ts | 146 ++++++++++++++++++++++++++++++ src/server.ts | 4 + src/types/index.ts | 1 + 4 files changed, 152 insertions(+) create mode 100644 src/routes/admin/issued-orders.ts diff --git a/src/admin/lib/entityTypeLabels.ts b/src/admin/lib/entityTypeLabels.ts index 3f615cb..3507826 100644 --- a/src/admin/lib/entityTypeLabels.ts +++ b/src/admin/lib/entityTypeLabels.ts @@ -16,6 +16,7 @@ export const ENTITY_TYPE_LABELS: Record = { invoice: "Faktura", quotation: "Nabídka", order: "Objednávka", + issued_order: "Objednávka vydaná", project: "Projekt", project_file: "Soubor projektu", customer: "Zákazník", diff --git a/src/routes/admin/issued-orders.ts b/src/routes/admin/issued-orders.ts new file mode 100644 index 0000000..cafe4bf --- /dev/null +++ b/src/routes/admin/issued-orders.ts @@ -0,0 +1,146 @@ +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 { + CreateIssuedOrderSchema, + UpdateIssuedOrderSchema, +} from "../../schemas/issued-orders.schema"; +import { + listIssuedOrders, + 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, + customer_id: query.customer_id ? Number(query.customer_id) : 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/: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); + await logAudit({ + request, + authData: request.authData, + action: "create", + entityType: "issued_order", + entityId: order.id, + description: `Vytvořena vydaná objednávka ${order.po_number}`, + }); + 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 === "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"); + }, + ); +} diff --git a/src/server.ts b/src/server.ts index 09c9ec3..2bd6206 100644 --- a/src/server.ts +++ b/src/server.ts @@ -14,6 +14,7 @@ import rolesRoutes from "./routes/admin/roles"; import attendanceRoutes from "./routes/admin/attendance"; import customersRoutes from "./routes/admin/customers"; import invoicesRoutes from "./routes/admin/invoices"; +import issuedOrdersRoutes from "./routes/admin/issued-orders"; import quotationsRoutes from "./routes/admin/quotations"; import ordersRoutes from "./routes/admin/orders"; import projectsRoutes from "./routes/admin/projects"; @@ -120,6 +121,9 @@ async function start() { await app.register(attendanceRoutes, { prefix: "/api/admin/attendance" }); await app.register(customersRoutes, { prefix: "/api/admin/customers" }); await app.register(invoicesRoutes, { prefix: "/api/admin/invoices" }); + await app.register(issuedOrdersRoutes, { + prefix: "/api/admin/issued-orders", + }); await app.register(quotationsRoutes, { prefix: "/api/admin/offers" }); await app.register(ordersRoutes, { prefix: "/api/admin/orders" }); await app.register(projectsRoutes, { prefix: "/api/admin/projects" }); diff --git a/src/types/index.ts b/src/types/index.ts index b1e0064..cec7322 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -129,6 +129,7 @@ export type EntityType = | "invoice" | "quotation" | "order" + | "issued_order" | "project" | "customer" | "trip"