2026-03-15 11:12:58 -05:00
|
|
|
import { permissions } from "@mrp/shared";
|
|
|
|
|
import { workOrderStatuses } from "@mrp/shared/dist/manufacturing/types.js";
|
|
|
|
|
import { Router } from "express";
|
|
|
|
|
import { z } from "zod";
|
|
|
|
|
|
|
|
|
|
import { fail, ok } from "../../lib/http.js";
|
|
|
|
|
import { requirePermissions } from "../../lib/rbac.js";
|
|
|
|
|
import {
|
2026-03-15 12:11:46 -05:00
|
|
|
createManufacturingStation,
|
2026-03-15 11:12:58 -05:00
|
|
|
createWorkOrder,
|
|
|
|
|
getWorkOrderById,
|
|
|
|
|
issueWorkOrderMaterial,
|
|
|
|
|
listManufacturingItemOptions,
|
|
|
|
|
listManufacturingProjectOptions,
|
2026-03-15 12:11:46 -05:00
|
|
|
listManufacturingStations,
|
2026-03-15 11:12:58 -05:00
|
|
|
listWorkOrders,
|
|
|
|
|
recordWorkOrderCompletion,
|
|
|
|
|
updateWorkOrder,
|
|
|
|
|
updateWorkOrderStatus,
|
|
|
|
|
} from "./service.js";
|
|
|
|
|
|
2026-03-15 12:11:46 -05:00
|
|
|
const stationSchema = z.object({
|
|
|
|
|
code: z.string().trim().min(1).max(64),
|
|
|
|
|
name: z.string().trim().min(1).max(160),
|
|
|
|
|
description: z.string(),
|
|
|
|
|
queueDays: z.number().int().min(0).max(365),
|
|
|
|
|
isActive: z.boolean(),
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-15 11:12:58 -05:00
|
|
|
const workOrderSchema = z.object({
|
|
|
|
|
itemId: z.string().trim().min(1),
|
|
|
|
|
projectId: z.string().trim().min(1).nullable(),
|
|
|
|
|
status: z.enum(workOrderStatuses),
|
|
|
|
|
quantity: z.number().int().positive(),
|
|
|
|
|
warehouseId: z.string().trim().min(1),
|
|
|
|
|
locationId: z.string().trim().min(1),
|
|
|
|
|
dueDate: z.string().datetime().nullable(),
|
|
|
|
|
notes: z.string(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const workOrderFiltersSchema = z.object({
|
|
|
|
|
q: z.string().optional(),
|
|
|
|
|
status: z.enum(workOrderStatuses).optional(),
|
|
|
|
|
projectId: z.string().optional(),
|
|
|
|
|
itemId: z.string().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const statusUpdateSchema = z.object({
|
|
|
|
|
status: z.enum(workOrderStatuses),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const materialIssueSchema = z.object({
|
|
|
|
|
componentItemId: z.string().trim().min(1),
|
|
|
|
|
warehouseId: z.string().trim().min(1),
|
|
|
|
|
locationId: z.string().trim().min(1),
|
|
|
|
|
quantity: z.number().int().positive(),
|
|
|
|
|
notes: z.string(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const completionSchema = z.object({
|
|
|
|
|
quantity: z.number().int().positive(),
|
|
|
|
|
notes: z.string(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function getRouteParam(value: unknown) {
|
|
|
|
|
return typeof value === "string" ? value : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const manufacturingRouter = Router();
|
|
|
|
|
|
|
|
|
|
manufacturingRouter.get("/items/options", requirePermissions([permissions.manufacturingRead]), async (_request, response) => {
|
|
|
|
|
return ok(response, await listManufacturingItemOptions());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
manufacturingRouter.get("/projects/options", requirePermissions([permissions.manufacturingRead]), async (_request, response) => {
|
|
|
|
|
return ok(response, await listManufacturingProjectOptions());
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-15 12:11:46 -05:00
|
|
|
manufacturingRouter.get("/stations", requirePermissions([permissions.manufacturingRead]), async (_request, response) => {
|
|
|
|
|
return ok(response, await listManufacturingStations());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
manufacturingRouter.post("/stations", requirePermissions([permissions.manufacturingWrite]), async (request, response) => {
|
|
|
|
|
const parsed = stationSchema.safeParse(request.body);
|
|
|
|
|
if (!parsed.success) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Manufacturing station payload is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 14:11:21 -05:00
|
|
|
return ok(response, await createManufacturingStation(parsed.data, request.authUser?.id), 201);
|
2026-03-15 12:11:46 -05:00
|
|
|
});
|
|
|
|
|
|
2026-03-15 11:12:58 -05:00
|
|
|
manufacturingRouter.get("/work-orders", requirePermissions([permissions.manufacturingRead]), async (request, response) => {
|
|
|
|
|
const parsed = workOrderFiltersSchema.safeParse(request.query);
|
|
|
|
|
if (!parsed.success) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Work-order filters are invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ok(response, await listWorkOrders(parsed.data));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
manufacturingRouter.get("/work-orders/:workOrderId", requirePermissions([permissions.manufacturingRead]), async (request, response) => {
|
|
|
|
|
const workOrderId = getRouteParam(request.params.workOrderId);
|
|
|
|
|
if (!workOrderId) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Work-order id is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const workOrder = await getWorkOrderById(workOrderId);
|
|
|
|
|
if (!workOrder) {
|
|
|
|
|
return fail(response, 404, "WORK_ORDER_NOT_FOUND", "Work order was not found.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ok(response, workOrder);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
manufacturingRouter.post("/work-orders", requirePermissions([permissions.manufacturingWrite]), async (request, response) => {
|
|
|
|
|
const parsed = workOrderSchema.safeParse(request.body);
|
|
|
|
|
if (!parsed.success) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Work-order payload is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 14:11:21 -05:00
|
|
|
const result = await createWorkOrder(parsed.data, request.authUser?.id);
|
2026-03-15 11:12:58 -05:00
|
|
|
if (!result.ok) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", result.reason);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ok(response, result.workOrder, 201);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
manufacturingRouter.put("/work-orders/:workOrderId", requirePermissions([permissions.manufacturingWrite]), async (request, response) => {
|
|
|
|
|
const workOrderId = getRouteParam(request.params.workOrderId);
|
|
|
|
|
if (!workOrderId) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Work-order id is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const parsed = workOrderSchema.safeParse(request.body);
|
|
|
|
|
if (!parsed.success) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Work-order payload is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 14:11:21 -05:00
|
|
|
const result = await updateWorkOrder(workOrderId, parsed.data, request.authUser?.id);
|
2026-03-15 11:12:58 -05:00
|
|
|
if (!result.ok) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", result.reason);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ok(response, result.workOrder);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
manufacturingRouter.patch("/work-orders/:workOrderId/status", requirePermissions([permissions.manufacturingWrite]), async (request, response) => {
|
|
|
|
|
const workOrderId = getRouteParam(request.params.workOrderId);
|
|
|
|
|
if (!workOrderId) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Work-order id is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const parsed = statusUpdateSchema.safeParse(request.body);
|
|
|
|
|
if (!parsed.success) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Work-order status payload is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 14:11:21 -05:00
|
|
|
const result = await updateWorkOrderStatus(workOrderId, parsed.data.status, request.authUser?.id);
|
2026-03-15 11:12:58 -05:00
|
|
|
if (!result.ok) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", result.reason);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ok(response, result.workOrder);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
manufacturingRouter.post("/work-orders/:workOrderId/issues", requirePermissions([permissions.manufacturingWrite]), async (request, response) => {
|
|
|
|
|
const workOrderId = getRouteParam(request.params.workOrderId);
|
|
|
|
|
if (!workOrderId) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Work-order id is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const parsed = materialIssueSchema.safeParse(request.body);
|
|
|
|
|
if (!parsed.success) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Material-issue payload is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await issueWorkOrderMaterial(workOrderId, parsed.data, request.authUser?.id);
|
|
|
|
|
if (!result.ok) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", result.reason);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ok(response, result.workOrder, 201);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
manufacturingRouter.post("/work-orders/:workOrderId/completions", requirePermissions([permissions.manufacturingWrite]), async (request, response) => {
|
|
|
|
|
const workOrderId = getRouteParam(request.params.workOrderId);
|
|
|
|
|
if (!workOrderId) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Work-order id is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const parsed = completionSchema.safeParse(request.body);
|
|
|
|
|
if (!parsed.success) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", "Completion payload is invalid.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await recordWorkOrderCompletion(workOrderId, parsed.data, request.authUser?.id);
|
|
|
|
|
if (!result.ok) {
|
|
|
|
|
return fail(response, 400, "INVALID_INPUT", result.reason);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ok(response, result.workOrder, 201);
|
|
|
|
|
});
|