import { permissions, projectMilestoneStatuses, projectPriorities, projectStatuses } from "@mrp/shared"; import { Router } from "express"; import { z } from "zod"; import { fail, ok } from "../../lib/http.js"; import { requirePermissions } from "../../lib/rbac.js"; import { createProject, getProjectById, listProjectCustomerOptions, listProjectOrderOptions, listProjectOwnerOptions, listProjects, listProjectQuoteOptions, listProjectShipmentOptions, updateProject, updateProjectMilestoneStatus, } from "./service.js"; const projectSchema = z.object({ name: z.string().trim().min(1).max(160), status: z.enum(projectStatuses), priority: z.enum(projectPriorities), customerId: z.string().trim().min(1), salesQuoteId: z.string().trim().min(1).nullable(), salesOrderId: z.string().trim().min(1).nullable(), shipmentId: z.string().trim().min(1).nullable(), ownerId: z.string().trim().min(1).nullable(), dueDate: z.string().datetime().nullable(), notes: z.string(), milestones: z.array( z.object({ id: z.string().trim().min(1).nullable().optional(), title: z.string().trim().min(1).max(160), status: z.enum(projectMilestoneStatuses), dueDate: z.string().datetime().nullable(), notes: z.string(), sortOrder: z.number().int(), }) ), }); const projectListQuerySchema = z.object({ q: z.string().optional(), status: z.enum(projectStatuses).optional(), priority: z.enum(projectPriorities).optional(), customerId: z.string().optional(), ownerId: z.string().optional(), }); const projectOptionQuerySchema = z.object({ customerId: z.string().optional(), }); const milestoneStatusSchema = z.object({ status: z.enum(projectMilestoneStatuses), }); function getRouteParam(value: unknown) { return typeof value === "string" ? value : null; } export const projectsRouter = Router(); projectsRouter.get("/customers/options", requirePermissions([permissions.projectsRead]), async (_request, response) => { return ok(response, await listProjectCustomerOptions()); }); projectsRouter.get("/owners/options", requirePermissions([permissions.projectsRead]), async (_request, response) => { return ok(response, await listProjectOwnerOptions()); }); projectsRouter.get("/quotes/options", requirePermissions([permissions.projectsRead]), async (request, response) => { const parsed = projectOptionQuerySchema.safeParse(request.query); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "Project quote filters are invalid."); } return ok(response, await listProjectQuoteOptions(parsed.data.customerId)); }); projectsRouter.get("/orders/options", requirePermissions([permissions.projectsRead]), async (request, response) => { const parsed = projectOptionQuerySchema.safeParse(request.query); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "Project order filters are invalid."); } return ok(response, await listProjectOrderOptions(parsed.data.customerId)); }); projectsRouter.get("/shipments/options", requirePermissions([permissions.projectsRead]), async (request, response) => { const parsed = projectOptionQuerySchema.safeParse(request.query); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "Project shipment filters are invalid."); } return ok(response, await listProjectShipmentOptions(parsed.data.customerId)); }); projectsRouter.get("/", requirePermissions([permissions.projectsRead]), async (request, response) => { const parsed = projectListQuerySchema.safeParse(request.query); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "Project filters are invalid."); } return ok(response, await listProjects(parsed.data)); }); projectsRouter.get("/:projectId", requirePermissions([permissions.projectsRead]), async (request, response) => { const projectId = getRouteParam(request.params.projectId); if (!projectId) { return fail(response, 400, "INVALID_INPUT", "Project id is invalid."); } const project = await getProjectById(projectId); if (!project) { return fail(response, 404, "PROJECT_NOT_FOUND", "Project was not found."); } return ok(response, project); }); projectsRouter.post("/", requirePermissions([permissions.projectsWrite]), async (request, response) => { const parsed = projectSchema.safeParse(request.body); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "Project payload is invalid."); } const result = await createProject(parsed.data, request.authUser?.id); if (!result.ok) { return fail(response, 400, "INVALID_INPUT", result.reason); } return ok(response, result.project, 201); }); projectsRouter.put("/:projectId", requirePermissions([permissions.projectsWrite]), async (request, response) => { const projectId = getRouteParam(request.params.projectId); if (!projectId) { return fail(response, 400, "INVALID_INPUT", "Project id is invalid."); } const parsed = projectSchema.safeParse(request.body); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "Project payload is invalid."); } const result = await updateProject(projectId, parsed.data, request.authUser?.id); if (!result.ok) { return fail(response, 400, "INVALID_INPUT", result.reason); } return ok(response, result.project); }); projectsRouter.patch("/:projectId/milestones/:milestoneId/status", requirePermissions([permissions.projectsWrite]), async (request, response) => { const projectId = getRouteParam(request.params.projectId); const milestoneId = getRouteParam(request.params.milestoneId); if (!projectId || !milestoneId) { return fail(response, 400, "INVALID_INPUT", "Project or milestone id is invalid."); } const parsed = milestoneStatusSchema.safeParse(request.body); if (!parsed.success) { return fail(response, 400, "INVALID_INPUT", "Project milestone status payload is invalid."); } const result = await updateProjectMilestoneStatus(projectId, milestoneId, parsed.data, request.authUser?.id); if (!result.ok) { return fail(response, 400, "INVALID_INPUT", result.reason); } return ok(response, result.project); });