From c983747418a0823b6f86627701633d8a8fdcda93 Mon Sep 17 00:00:00 2001 From: BOHA Date: Sat, 6 Jun 2026 00:38:25 +0200 Subject: [PATCH] feat(plan): add plan category CRUD routes with audit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/routes/admin/plan.ts | 92 ++++++++++++++++++++++++++++++++++++++++ src/types/index.ts | 3 +- 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/routes/admin/plan.ts b/src/routes/admin/plan.ts index 784d6e6..62c6359 100644 --- a/src/routes/admin/plan.ts +++ b/src/routes/admin/plan.ts @@ -27,6 +27,16 @@ import { listEntries, listOverrides, } from "../../services/plan.service"; +import { + CreatePlanCategorySchema, + UpdatePlanCategorySchema, +} from "../../schemas/planCategory.schema"; +import { + listPlanCategories, + createPlanCategory, + updatePlanCategory, + deactivatePlanCategory, +} from "../../services/planCategory.service"; const MAX_RANGE_DAYS = 92; @@ -135,6 +145,88 @@ export default async function planRoutes(app: FastifyInstance) { }, ); + // --- GET /plan/categories (any planner can read; needed for display) --- + app.get( + "/categories", + { + preHandler: requireAnyPermission( + "attendance.manage", + "attendance.record", + ), + }, + async (_request: FastifyRequest, reply: FastifyReply) => { + const cats = await listPlanCategories(true); + return success(reply, cats); + }, + ); + + // --- POST /plan/categories --- + app.post( + "/categories", + { preHandler: requirePermission("attendance.manage") }, + async (request: FastifyRequest, reply: FastifyReply) => { + const body = parseBody(CreatePlanCategorySchema, request.body); + if ("error" in body) return error(reply, body.error, 400); + const result = await createPlanCategory(body.data!); + if ("error" in result) return error(reply, result.error, result.status); + await logAudit({ + request, + authData: request.authData, + action: "create", + entityType: "plan_category", + entityId: result.data.id, + newValues: result.data, + description: `Kategorie: ${result.data.label}`, + }); + return success(reply, result.data, 201, "Kategorie vytvořena"); + }, + ); + + // --- PATCH /plan/categories/:id --- + app.patch( + "/categories/:id", + { preHandler: requirePermission("attendance.manage") }, + async (request: FastifyRequest, reply: FastifyReply) => { + const id = parseId((request.params as any).id, reply); + if (id === null) return; + const body = parseBody(UpdatePlanCategorySchema, request.body); + if ("error" in body) return error(reply, body.error, 400); + const result = await updatePlanCategory(id, body.data!); + if ("error" in result) return error(reply, result.error, result.status); + await logAudit({ + request, + authData: request.authData, + action: "update", + entityType: "plan_category", + entityId: id, + newValues: result.data, + description: `Kategorie: ${result.data.label}`, + }); + return success(reply, result.data, 200, "Kategorie aktualizována"); + }, + ); + + // --- DELETE /plan/categories/:id (deactivate) --- + app.delete( + "/categories/:id", + { preHandler: requirePermission("attendance.manage") }, + async (request: FastifyRequest, reply: FastifyReply) => { + const id = parseId((request.params as any).id, reply); + if (id === null) return; + const result = await deactivatePlanCategory(id); + if ("error" in result) return error(reply, result.error, result.status); + await logAudit({ + request, + authData: request.authData, + action: "delete", + entityType: "plan_category", + entityId: id, + description: "Kategorie deaktivována", + }); + return success(reply, { ok: true }, 200, "Kategorie deaktivována"); + }, + ); + // --- POST /plan/entries --- app.post( "/entries", diff --git a/src/types/index.ts b/src/types/index.ts index aebb6ec..9bb497d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -145,4 +145,5 @@ export type EntityType = | "warehouse_location" | "warehouse_receipt_attachment" | "work_plan_entry" - | "work_plan_override"; + | "work_plan_override" + | "plan_category";