import type { ManufacturingStationDto, ManufacturingStationInput, ManufacturingItemOptionDto, ManufacturingProjectOptionDto, WorkOrderCompletionInput, WorkOrderDetailDto, WorkOrderInput, WorkOrderOperationExecutionInput, WorkOrderOperationDto, WorkOrderOperationLaborEntryInput, WorkOrderOperationScheduleInput, WorkOrderMaterialIssueInput, WorkOrderStatus, WorkOrderSummaryDto, } from "@mrp/shared"; import { logAuditEvent } from "../../lib/audit.js"; import { prisma } from "../../lib/prisma.js"; const workOrderModel = (prisma as any).workOrder; type StationRecord = { id: string; code: string; name: string; description: string; queueDays: number; dailyCapacityMinutes: number; parallelCapacity: number; workingDays: string; isActive: boolean; createdAt: Date; updatedAt: Date; }; type WorkOrderRecord = { id: string; workOrderNumber: string; status: string; quantity: number; completedQuantity: number; dueDate: Date | null; notes: string; createdAt: Date; updatedAt: Date; item: { id: string; sku: string; name: string; type: string; unitOfMeasure: string; operations: Array<{ setupMinutes: number; runMinutesPerUnit: number; moveMinutes: number; position: number; notes: string; station: { id: string; code: string; name: string; queueDays: number; dailyCapacityMinutes: number; parallelCapacity: number; workingDays: string; }; }>; bomLines: Array<{ quantity: number; unitOfMeasure: string; componentItem: { id: string; sku: string; name: string; }; }>; }; project: { id: string; projectNumber: string; name: string; customer: { name: string; }; } | null; salesOrder: { id: string; documentNumber: string; } | null; salesOrderLine: { id: string; } | null; warehouse: { id: string; code: string; name: string; }; location: { id: string; code: string; name: string; }; operations: Array<{ id: string; sequence: number; setupMinutes: number; runMinutesPerUnit: number; moveMinutes: number; plannedMinutes: number; plannedStart: Date; plannedEnd: Date; notes: string; status: string; actualStart: Date | null; actualEnd: Date | null; actualMinutes: number; station: { id: string; code: string; name: string; dailyCapacityMinutes: number; parallelCapacity: number; workingDays: string; }; laborEntries: Array<{ id: string; minutes: number; notes: string; createdAt: Date; createdBy: { firstName: string; lastName: string; } | null; }>; }>; materialIssues: Array<{ id: string; quantity: number; notes: string; createdAt: Date; componentItem: { id: string; sku: string; name: string; }; warehouse: { id: string; code: string; name: string; }; location: { id: string; code: string; name: string; }; createdBy: { firstName: string; lastName: string; } | null; }>; completions: Array<{ id: string; quantity: number; notes: string; createdAt: Date; createdBy: { firstName: string; lastName: string; } | null; }>; }; function mapStation(record: StationRecord): ManufacturingStationDto { const workingDays = record.workingDays.split(",").map((value) => Number.parseInt(value, 10)).filter((value) => Number.isInteger(value) && value >= 0 && value <= 6); return { id: record.id, code: record.code, name: record.name, description: record.description, queueDays: record.queueDays, dailyCapacityMinutes: record.dailyCapacityMinutes, parallelCapacity: record.parallelCapacity, workingDays: workingDays.length > 0 ? workingDays : [1, 2, 3, 4, 5], isActive: record.isActive, createdAt: record.createdAt.toISOString(), updatedAt: record.updatedAt.toISOString(), }; } function buildInclude() { return { item: { include: { operations: { include: { station: { select: { id: true, code: true, name: true, queueDays: true, dailyCapacityMinutes: true, parallelCapacity: true, workingDays: true, }, }, }, orderBy: [{ position: "asc" }, { createdAt: "asc" }], }, bomLines: { include: { componentItem: { select: { id: true, sku: true, name: true, }, }, }, orderBy: [{ position: "asc" }, { createdAt: "asc" }], }, }, }, project: { include: { customer: { select: { name: true, }, }, }, }, salesOrder: { select: { id: true, documentNumber: true, }, }, salesOrderLine: { select: { id: true, }, }, warehouse: { select: { id: true, code: true, name: true, }, }, location: { select: { id: true, code: true, name: true, }, }, operations: { include: { station: { select: { id: true, code: true, name: true, dailyCapacityMinutes: true, parallelCapacity: true, workingDays: true, }, }, laborEntries: { include: { createdBy: { select: { firstName: true, lastName: true, }, }, }, orderBy: [{ createdAt: "desc" }], }, }, orderBy: [{ sequence: "asc" }], }, materialIssues: { include: { componentItem: { select: { id: true, sku: true, name: true, }, }, warehouse: { select: { id: true, code: true, name: true, }, }, location: { select: { id: true, code: true, name: true, }, }, createdBy: { select: { firstName: true, lastName: true, }, }, }, orderBy: [{ createdAt: "desc" }], }, completions: { include: { createdBy: { select: { firstName: true, lastName: true, }, }, }, orderBy: [{ createdAt: "desc" }], }, }; } function getUserName(user: { firstName: string; lastName: string } | null) { return user ? `${user.firstName} ${user.lastName}`.trim() : "System"; } function mapSummary(record: WorkOrderRecord): WorkOrderSummaryDto { return { id: record.id, workOrderNumber: record.workOrderNumber, status: record.status as WorkOrderStatus, itemId: record.item.id, itemSku: record.item.sku, itemName: record.item.name, projectId: record.project?.id ?? null, projectNumber: record.project?.projectNumber ?? null, projectName: record.project?.name ?? null, salesOrderId: record.salesOrder?.id ?? null, salesOrderLineId: record.salesOrderLine?.id ?? null, salesOrderNumber: record.salesOrder?.documentNumber ?? null, quantity: record.quantity, completedQuantity: record.completedQuantity, dueDate: record.dueDate ? record.dueDate.toISOString() : null, warehouseId: record.warehouse.id, warehouseCode: record.warehouse.code, warehouseName: record.warehouse.name, locationId: record.location.id, locationCode: record.location.code, locationName: record.location.name, operationCount: record.operations.length, totalPlannedMinutes: record.operations.reduce((sum, operation) => sum + operation.plannedMinutes, 0), updatedAt: record.updatedAt.toISOString(), }; } function mapDetail( record: WorkOrderRecord, componentAvailability: Map ): WorkOrderDetailDto { const issuedByComponent = new Map(); for (const issue of record.materialIssues) { issuedByComponent.set(issue.componentItem.id, (issuedByComponent.get(issue.componentItem.id) ?? 0) + issue.quantity); } return { ...mapSummary(record), notes: record.notes, createdAt: record.createdAt.toISOString(), itemType: record.item.type, itemUnitOfMeasure: record.item.unitOfMeasure, projectCustomerName: record.project?.customer.name ?? null, dueQuantity: record.quantity - record.completedQuantity, totalActualMinutes: record.operations.reduce((sum, operation) => sum + operation.actualMinutes, 0), operations: record.operations.map((operation): WorkOrderOperationDto => ({ id: operation.id, stationId: operation.station.id, stationCode: operation.station.code, stationName: operation.station.name, stationDailyCapacityMinutes: operation.station.dailyCapacityMinutes, stationParallelCapacity: operation.station.parallelCapacity, stationWorkingDays: parseWorkingDays(operation.station.workingDays), sequence: operation.sequence, setupMinutes: operation.setupMinutes, runMinutesPerUnit: operation.runMinutesPerUnit, moveMinutes: operation.moveMinutes, plannedMinutes: operation.plannedMinutes, plannedStart: operation.plannedStart.toISOString(), plannedEnd: operation.plannedEnd.toISOString(), notes: operation.notes, status: operation.status as WorkOrderOperationDto["status"], actualStart: operation.actualStart ? operation.actualStart.toISOString() : null, actualEnd: operation.actualEnd ? operation.actualEnd.toISOString() : null, actualMinutes: operation.actualMinutes, laborEntryCount: operation.laborEntries.length, laborEntries: operation.laborEntries.map((entry) => ({ id: entry.id, minutes: entry.minutes, notes: entry.notes, createdAt: entry.createdAt.toISOString(), createdByName: getUserName(entry.createdBy), })), })), materialRequirements: record.item.bomLines.map((line) => { const requiredQuantity = line.quantity * record.quantity; const issuedQuantity = issuedByComponent.get(line.componentItem.id) ?? 0; const availability = componentAvailability.get(line.componentItem.id) ?? { onHandQuantity: 0, reservedQuantity: 0 }; const onHandQuantity = availability.onHandQuantity; const reservedQuantity = availability.reservedQuantity; const availableQuantity = onHandQuantity - reservedQuantity; const shortageQuantity = Math.max(requiredQuantity - issuedQuantity - Math.max(availableQuantity, 0), 0); return { componentItemId: line.componentItem.id, componentSku: line.componentItem.sku, componentName: line.componentItem.name, unitOfMeasure: line.unitOfMeasure, quantityPer: line.quantity, requiredQuantity, issuedQuantity, remainingQuantity: Math.max(requiredQuantity - issuedQuantity, 0), onHandQuantity, reservedQuantity, availableQuantity, shortageQuantity, }; }), materialIssues: record.materialIssues.map((issue) => ({ id: issue.id, componentItemId: issue.componentItem.id, componentSku: issue.componentItem.sku, componentName: issue.componentItem.name, quantity: issue.quantity, warehouseId: issue.warehouse.id, warehouseCode: issue.warehouse.code, warehouseName: issue.warehouse.name, locationId: issue.location.id, locationCode: issue.location.code, locationName: issue.location.name, notes: issue.notes, createdAt: issue.createdAt.toISOString(), createdByName: getUserName(issue.createdBy), })), completions: record.completions.map((completion) => ({ id: completion.id, quantity: completion.quantity, notes: completion.notes, createdAt: completion.createdAt.toISOString(), createdByName: getUserName(completion.createdBy), })), }; } function addMinutes(value: Date, minutes: number) { return new Date(value.getTime() + minutes * 60 * 1000); } function parseWorkingDays(value: string) { const parsed = value .split(",") .map((entry) => Number.parseInt(entry.trim(), 10)) .filter((entry) => Number.isInteger(entry) && entry >= 0 && entry <= 6); return parsed.length > 0 ? parsed : [1, 2, 3, 4, 5]; } function normalizeStationWorkingDays(value: number[]) { const normalized = [...new Set(value.filter((entry) => Number.isInteger(entry) && entry >= 0 && entry <= 6))].sort((left, right) => left - right); return normalized.length > 0 ? normalized : [1, 2, 3, 4, 5]; } function alignToWorkingWindow(value: Date, workingDays: number[]) { let next = new Date(value); while (!workingDays.includes(next.getDay())) { next = new Date(next.getFullYear(), next.getMonth(), next.getDate() + 1, 8, 0, 0, 0); } return next; } function addWorkingMinutes( start: Date, minutes: number, station: { dailyCapacityMinutes: number; parallelCapacity: number; workingDays: string; queueDays: number } ) { const workingDays = parseWorkingDays(station.workingDays); const dailyCapacityMinutes = Math.max(station.dailyCapacityMinutes * Math.max(station.parallelCapacity, 1), 60); let cursor = alignToWorkingWindow(start, workingDays); let remaining = Math.max(minutes, 0); while (remaining > 0) { const dayStart = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate(), 8, 0, 0, 0); const dayEnd = addMinutes(dayStart, dailyCapacityMinutes); const effectiveStart = cursor < dayStart ? dayStart : cursor; const availableToday = Math.max(Math.round((dayEnd.getTime() - effectiveStart.getTime()) / 60000), 0); if (availableToday <= 0) { cursor = alignToWorkingWindow(new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() + 1, 8, 0, 0, 0), workingDays); continue; } const consumed = Math.min(availableToday, remaining); cursor = addMinutes(effectiveStart, consumed); remaining -= consumed; if (remaining > 0) { cursor = alignToWorkingWindow(new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() + 1, 8, 0, 0, 0), workingDays); } } if (station.queueDays > 0) { let queued = new Date(cursor); for (let day = 0; day < station.queueDays; day += 1) { queued = alignToWorkingWindow(new Date(queued.getFullYear(), queued.getMonth(), queued.getDate() + 1, queued.getHours(), queued.getMinutes(), 0, 0), workingDays); } return queued; } return cursor; } function shouldReserveForStatus(status: string) { return status === "RELEASED" || status === "IN_PROGRESS" || status === "ON_HOLD"; } function buildWorkOrderOperationPlan( itemOperations: WorkOrderRecord["item"]["operations"], quantity: number, dueDate: Date | null, fallbackStart: Date ) { if (itemOperations.length === 0) { return []; } const operationDurations = itemOperations.map((operation) => { const plannedMinutes = Math.max(operation.setupMinutes + operation.runMinutesPerUnit * quantity + operation.moveMinutes, 1); return { stationId: operation.station.id, sequence: operation.position, setupMinutes: operation.setupMinutes, runMinutesPerUnit: operation.runMinutesPerUnit, moveMinutes: operation.moveMinutes, plannedMinutes, notes: operation.notes, station: operation.station, }; }); let nextStart = new Date(dueDate ?? fallbackStart); return operationDurations.map((operation) => { const plannedStart = alignToWorkingWindow(nextStart, parseWorkingDays(operation.station.workingDays)); const plannedEnd = addWorkingMinutes(plannedStart, operation.plannedMinutes, operation.station); const planned = { ...operation, plannedStart, plannedEnd, }; nextStart = plannedEnd; return planned; }); } async function regenerateWorkOrderOperations(workOrderId: string) { const workOrder = await workOrderModel.findUnique({ where: { id: workOrderId }, include: buildInclude(), }); if (!workOrder) { return; } const plan = buildWorkOrderOperationPlan( (workOrder as WorkOrderRecord).item.operations, workOrder.quantity, workOrder.dueDate, workOrder.createdAt ); await prisma.workOrderOperation.deleteMany({ where: { workOrderId, }, }); if (plan.length === 0) { return; } await prisma.workOrderOperation.createMany({ data: plan.map((operation) => ({ workOrderId, stationId: operation.stationId, sequence: operation.sequence, setupMinutes: operation.setupMinutes, runMinutesPerUnit: operation.runMinutesPerUnit, moveMinutes: operation.moveMinutes, plannedMinutes: operation.plannedMinutes, plannedStart: operation.plannedStart, plannedEnd: operation.plannedEnd, notes: operation.notes, status: "PENDING", })), }); } async function rescheduleWorkOrderOperations(workOrderId: string, operationId: string, plannedStart: Date) { const workOrder = await workOrderModel.findUnique({ where: { id: workOrderId }, include: buildInclude(), }); if (!workOrder) { return null; } const operations = (workOrder as WorkOrderRecord).operations; const anchorIndex = operations.findIndex((operation) => operation.id === operationId); if (anchorIndex < 0) { return null; } return rescheduleWorkOrderOperationsToStation(workOrderId, operations, anchorIndex, plannedStart, null); } async function rescheduleWorkOrderOperationsToStation( workOrderId: string, operations: WorkOrderRecord["operations"], anchorIndex: number, plannedStart: Date, targetStation: Pick | null ) { let nextStart = plannedStart; const updates = operations.slice(anchorIndex).map((operation) => { const station = operation.id === operations[anchorIndex]?.id && targetStation ? targetStation : { id: operation.station.id, dailyCapacityMinutes: operation.station.dailyCapacityMinutes, parallelCapacity: operation.station.parallelCapacity, workingDays: operation.station.workingDays, queueDays: 0, }; const alignedStart = alignToWorkingWindow(nextStart, parseWorkingDays(station.workingDays)); const plannedEnd = addWorkingMinutes(alignedStart, operation.plannedMinutes, station); nextStart = plannedEnd; return { id: operation.id, stationId: station.id, plannedStart: alignedStart, plannedEnd, }; }); await prisma.$transaction( updates.map((update) => prisma.workOrderOperation.update({ where: { id: update.id }, data: { stationId: update.stationId, plannedStart: update.plannedStart, plannedEnd: update.plannedEnd, }, }) ) ); return getWorkOrderById(workOrderId); } async function syncWorkOrderStatusFromOperationActivity(workOrderId: string) { const workOrder = await prisma.workOrder.findUnique({ where: { id: workOrderId }, select: { id: true, status: true, }, }); if (!workOrder) { return; } if (workOrder.status === "RELEASED" || workOrder.status === "ON_HOLD") { await prisma.workOrder.update({ where: { id: workOrderId }, data: { status: "IN_PROGRESS", }, }); } } async function syncWorkOrderReservations(workOrderId: string) { const workOrder = await getWorkOrderById(workOrderId); if (!workOrder) { return; } await prisma.inventoryReservation.deleteMany({ where: { workOrderId, sourceType: "WORK_ORDER", }, }); if (!shouldReserveForStatus(workOrder.status)) { return; } const reservations = workOrder.materialRequirements .filter((requirement) => requirement.remainingQuantity > 0) .map((requirement) => ({ itemId: requirement.componentItemId, warehouseId: workOrder.warehouseId, locationId: workOrder.locationId, workOrderId, sourceType: "WORK_ORDER", sourceId: workOrderId, quantity: requirement.remainingQuantity, status: "ACTIVE", notes: `${workOrder.workOrderNumber} component demand`, })); if (reservations.length === 0) { return; } await prisma.inventoryReservation.createMany({ data: reservations, }); } async function nextWorkOrderNumber() { const next = (await workOrderModel.count()) + 1; return `WO-${String(next).padStart(5, "0")}`; } async function getItemLocationOnHand(itemId: string, warehouseId: string, locationId: string) { const transactions = await prisma.inventoryTransaction.findMany({ where: { itemId, warehouseId, locationId, }, select: { transactionType: true, quantity: true, }, }); return transactions.reduce((total, transaction) => { return total + (transaction.transactionType === "RECEIPT" || transaction.transactionType === "ADJUSTMENT_IN" ? transaction.quantity : -transaction.quantity); }, 0); } async function getComponentAvailability(workOrder: WorkOrderRecord) { const componentItemIds = [...new Set(workOrder.item.bomLines.map((line) => line.componentItem.id))]; if (componentItemIds.length === 0) { return new Map(); } const [transactions, reservations] = await Promise.all([ prisma.inventoryTransaction.findMany({ where: { itemId: { in: componentItemIds }, warehouseId: workOrder.warehouse.id, locationId: workOrder.location.id, }, select: { itemId: true, transactionType: true, quantity: true, }, }), prisma.inventoryReservation.findMany({ where: { itemId: { in: componentItemIds }, warehouseId: workOrder.warehouse.id, locationId: workOrder.location.id, status: "ACTIVE", }, select: { itemId: true, quantity: true, }, }), ]); const availability = new Map(); for (const itemId of componentItemIds) { availability.set(itemId, { onHandQuantity: 0, reservedQuantity: 0 }); } for (const transaction of transactions) { const current = availability.get(transaction.itemId); if (!current) { continue; } current.onHandQuantity += transaction.transactionType === "RECEIPT" || transaction.transactionType === "ADJUSTMENT_IN" ? transaction.quantity : -transaction.quantity; } for (const reservation of reservations) { const current = availability.get(reservation.itemId); if (!current) { continue; } current.reservedQuantity += reservation.quantity; } return availability; } async function validateWorkOrderInput(payload: WorkOrderInput) { const item = await prisma.inventoryItem.findUnique({ where: { id: payload.itemId }, select: { id: true, type: true, status: true, _count: { select: { operations: true, }, }, }, }); if (!item) { return { ok: false as const, reason: "Build item was not found." }; } if (item.status !== "ACTIVE") { return { ok: false as const, reason: "Build item must be active." }; } if (item.type !== "ASSEMBLY" && item.type !== "MANUFACTURED") { return { ok: false as const, reason: "Work orders can only be created for assembly or manufactured items." }; } if (item._count.operations === 0) { return { ok: false as const, reason: "Build item must have at least one station operation before a work order can be created." }; } if (payload.projectId) { const project = await prisma.project.findUnique({ where: { id: payload.projectId }, select: { id: true }, }); if (!project) { return { ok: false as const, reason: "Linked project was not found." }; } } if (payload.salesOrderId) { const salesOrder = await prisma.salesOrder.findUnique({ where: { id: payload.salesOrderId }, select: { id: true }, }); if (!salesOrder) { return { ok: false as const, reason: "Linked sales order was not found." }; } } if (payload.salesOrderLineId) { if (!payload.salesOrderId) { return { ok: false as const, reason: "Linked sales-order line requires a linked sales order." }; } const salesOrderLine = await prisma.salesOrderLine.findUnique({ where: { id: payload.salesOrderLineId }, select: { id: true, orderId: true, itemId: true, }, }); if (!salesOrderLine || salesOrderLine.orderId !== payload.salesOrderId) { return { ok: false as const, reason: "Linked sales-order line was not found on the selected sales order." }; } if (salesOrderLine.itemId !== payload.itemId) { return { ok: false as const, reason: "Linked sales-order line item does not match the selected build item." }; } } const location = await prisma.warehouseLocation.findUnique({ where: { id: payload.locationId }, select: { id: true, warehouseId: true, }, }); if (!location || location.warehouseId !== payload.warehouseId) { return { ok: false as const, reason: "Warehouse location is invalid for the selected warehouse." }; } return { ok: true as const }; } export async function listManufacturingItemOptions(): Promise { const items = await prisma.inventoryItem.findMany({ where: { status: "ACTIVE", type: { in: ["ASSEMBLY", "MANUFACTURED"], }, }, select: { id: true, sku: true, name: true, type: true, unitOfMeasure: true, operations: { select: { setupMinutes: true, runMinutesPerUnit: true, moveMinutes: true, }, }, }, orderBy: [{ sku: "asc" }], }); return items.map((item) => ({ id: item.id, sku: item.sku, name: item.name, type: item.type, unitOfMeasure: item.unitOfMeasure, operationCount: item.operations.length, totalEstimatedMinutesPerUnit: item.operations.reduce( (sum, operation) => sum + operation.setupMinutes + operation.runMinutesPerUnit + operation.moveMinutes, 0 ), })); } export async function listManufacturingStations(): Promise { const stations = await prisma.manufacturingStation.findMany({ orderBy: [{ code: "asc" }], }); return stations.map(mapStation); } export async function createManufacturingStation(payload: ManufacturingStationInput, actorId?: string | null) { const workingDays = normalizeStationWorkingDays(payload.workingDays); const station = await prisma.manufacturingStation.create({ data: { code: payload.code.trim(), name: payload.name.trim(), description: payload.description, queueDays: payload.queueDays, dailyCapacityMinutes: payload.dailyCapacityMinutes, parallelCapacity: payload.parallelCapacity, workingDays: workingDays.join(","), isActive: payload.isActive, }, }); await logAuditEvent({ actorId, entityType: "manufacturing-station", entityId: station.id, action: "created", summary: `Created manufacturing station ${station.code}.`, metadata: { code: station.code, name: station.name, queueDays: station.queueDays, dailyCapacityMinutes: station.dailyCapacityMinutes, parallelCapacity: station.parallelCapacity, workingDays, isActive: station.isActive, }, }); return mapStation(station); } export async function updateManufacturingStation(stationId: string, payload: ManufacturingStationInput, actorId?: string | null) { const existing = await prisma.manufacturingStation.findUnique({ where: { id: stationId }, }); if (!existing) { return { ok: false as const, reason: "Manufacturing station was not found." }; } const workingDays = normalizeStationWorkingDays(payload.workingDays); const station = await prisma.manufacturingStation.update({ where: { id: stationId }, data: { code: payload.code.trim(), name: payload.name.trim(), description: payload.description, queueDays: payload.queueDays, dailyCapacityMinutes: payload.dailyCapacityMinutes, parallelCapacity: payload.parallelCapacity, workingDays: workingDays.join(","), isActive: payload.isActive, }, }); await logAuditEvent({ actorId, entityType: "manufacturing-station", entityId: station.id, action: "updated", summary: `Updated manufacturing station ${station.code}.`, metadata: { previousCode: existing.code, previousName: existing.name, previousQueueDays: existing.queueDays, previousDailyCapacityMinutes: existing.dailyCapacityMinutes, previousParallelCapacity: existing.parallelCapacity, previousWorkingDays: parseWorkingDays(existing.workingDays), previousIsActive: existing.isActive, code: station.code, name: station.name, queueDays: station.queueDays, dailyCapacityMinutes: station.dailyCapacityMinutes, parallelCapacity: station.parallelCapacity, workingDays, isActive: station.isActive, }, }); return { ok: true as const, station: mapStation(station) }; } export async function listManufacturingProjectOptions(): Promise { const projects = await prisma.project.findMany({ where: { status: { notIn: ["COMPLETE"], }, }, include: { customer: { select: { name: true, }, }, }, orderBy: [{ dueDate: "asc" }, { updatedAt: "desc" }], }); return projects.map((project) => ({ id: project.id, projectNumber: project.projectNumber, name: project.name, customerName: project.customer.name, status: project.status, })); } export async function listWorkOrders(filters: { q?: string; status?: WorkOrderStatus; projectId?: string; itemId?: string; } = {}) { const query = filters.q?.trim(); const workOrders = await workOrderModel.findMany({ where: { ...(filters.status ? { status: filters.status } : {}), ...(filters.projectId ? { projectId: filters.projectId } : {}), ...(filters.itemId ? { itemId: filters.itemId } : {}), ...(query ? { OR: [ { workOrderNumber: { contains: query } }, { item: { sku: { contains: query } } }, { item: { name: { contains: query } } }, { project: { projectNumber: { contains: query } } }, { project: { name: { contains: query } } }, ], } : {}), }, include: buildInclude(), orderBy: [{ dueDate: "asc" }, { updatedAt: "desc" }], }); return workOrders.map((workOrder: unknown) => mapSummary(workOrder as WorkOrderRecord)); } export async function getWorkOrderById(workOrderId: string) { const workOrder = await workOrderModel.findUnique({ where: { id: workOrderId }, include: buildInclude(), }); if (!workOrder) { return null; } return mapDetail(workOrder as WorkOrderRecord, await getComponentAvailability(workOrder as WorkOrderRecord)); } export async function createWorkOrder(payload: WorkOrderInput, actorId?: string | null) { const validated = await validateWorkOrderInput(payload); if (!validated.ok) { return { ok: false as const, reason: validated.reason }; } const workOrderNumber = await nextWorkOrderNumber(); const created = await workOrderModel.create({ data: { workOrderNumber, itemId: payload.itemId, projectId: payload.projectId, salesOrderId: payload.salesOrderId, salesOrderLineId: payload.salesOrderLineId, warehouseId: payload.warehouseId, locationId: payload.locationId, status: payload.status, quantity: payload.quantity, dueDate: payload.dueDate ? new Date(payload.dueDate) : null, notes: payload.notes, }, select: { id: true, }, }); await regenerateWorkOrderOperations(created.id); await syncWorkOrderReservations(created.id); const workOrder = await getWorkOrderById(created.id); if (workOrder) { await logAuditEvent({ actorId, entityType: "work-order", entityId: created.id, action: "created", summary: `Created work order ${workOrder.workOrderNumber}.`, metadata: { workOrderNumber: workOrder.workOrderNumber, itemId: workOrder.itemId, projectId: workOrder.projectId, status: workOrder.status, quantity: workOrder.quantity, }, }); } return workOrder ? { ok: true as const, workOrder } : { ok: false as const, reason: "Unable to load saved work order." }; } export async function updateWorkOrder(workOrderId: string, payload: WorkOrderInput, actorId?: string | null) { const existing = await workOrderModel.findUnique({ where: { id: workOrderId }, select: { id: true, completedQuantity: true, }, }); if (!existing) { return { ok: false as const, reason: "Work order was not found." }; } if (payload.quantity < existing.completedQuantity) { return { ok: false as const, reason: "Planned quantity cannot be less than the already completed quantity." }; } const validated = await validateWorkOrderInput(payload); if (!validated.ok) { return { ok: false as const, reason: validated.reason }; } await workOrderModel.update({ where: { id: workOrderId }, data: { itemId: payload.itemId, projectId: payload.projectId, salesOrderId: payload.salesOrderId, salesOrderLineId: payload.salesOrderLineId, warehouseId: payload.warehouseId, locationId: payload.locationId, status: payload.status, quantity: payload.quantity, dueDate: payload.dueDate ? new Date(payload.dueDate) : null, notes: payload.notes, }, }); await regenerateWorkOrderOperations(workOrderId); await syncWorkOrderReservations(workOrderId); const workOrder = await getWorkOrderById(workOrderId); if (workOrder) { await logAuditEvent({ actorId, entityType: "work-order", entityId: workOrderId, action: "updated", summary: `Updated work order ${workOrder.workOrderNumber}.`, metadata: { workOrderNumber: workOrder.workOrderNumber, itemId: workOrder.itemId, projectId: workOrder.projectId, status: workOrder.status, quantity: workOrder.quantity, }, }); } return workOrder ? { ok: true as const, workOrder } : { ok: false as const, reason: "Unable to load saved work order." }; } export async function updateWorkOrderStatus(workOrderId: string, status: WorkOrderStatus, actorId?: string | null) { const existing = await workOrderModel.findUnique({ where: { id: workOrderId }, select: { id: true, status: true, quantity: true, completedQuantity: true, }, }); if (!existing) { return { ok: false as const, reason: "Work order was not found." }; } if (existing.status === "COMPLETE" && status !== "COMPLETE") { return { ok: false as const, reason: "Completed work orders cannot be reopened from quick actions." }; } if (status === "COMPLETE" && existing.completedQuantity < existing.quantity) { return { ok: false as const, reason: "Use the completion action to finish a work order." }; } await workOrderModel.update({ where: { id: workOrderId }, data: { status, }, }); await syncWorkOrderReservations(workOrderId); const workOrder = await getWorkOrderById(workOrderId); if (workOrder) { await logAuditEvent({ actorId, entityType: "work-order", entityId: workOrderId, action: "status.updated", summary: `Updated work order ${workOrder.workOrderNumber} to ${status}.`, metadata: { workOrderNumber: workOrder.workOrderNumber, status, }, }); } return workOrder ? { ok: true as const, workOrder } : { ok: false as const, reason: "Unable to load saved work order." }; } export async function updateWorkOrderOperationSchedule( workOrderId: string, operationId: string, payload: WorkOrderOperationScheduleInput, actorId?: string | null ) { const existing = await prisma.workOrderOperation.findUnique({ where: { id: operationId }, select: { id: true, workOrderId: true, sequence: true, stationId: true, station: { select: { code: true, name: true, }, }, }, }); if (!existing || existing.workOrderId !== workOrderId) { return { ok: false as const, reason: "Work-order operation was not found." }; } const workOrder = await getWorkOrderById(workOrderId); if (!workOrder) { return { ok: false as const, reason: "Work order was not found." }; } if (workOrder.status === "COMPLETE" || workOrder.status === "CANCELLED") { return { ok: false as const, reason: "Completed or cancelled work orders cannot be rescheduled." }; } let targetStation: | Pick | null = null; if (payload.stationId && payload.stationId !== existing.stationId) { const station = await prisma.manufacturingStation.findUnique({ where: { id: payload.stationId }, select: { id: true, queueDays: true, dailyCapacityMinutes: true, parallelCapacity: true, workingDays: true, isActive: true, }, }); if (!station || !station.isActive) { return { ok: false as const, reason: "Selected manufacturing station was not found or is inactive." }; } targetStation = station; } const workOrderRecord = await workOrderModel.findUnique({ where: { id: workOrderId }, include: buildInclude(), }); if (!workOrderRecord) { return { ok: false as const, reason: "Work order was not found." }; } const operations = (workOrderRecord as WorkOrderRecord).operations; const anchorIndex = operations.findIndex((operation) => operation.id === operationId); if (anchorIndex < 0) { return { ok: false as const, reason: "Work-order operation was not found." }; } const rescheduled = await rescheduleWorkOrderOperationsToStation( workOrderId, operations, anchorIndex, new Date(payload.plannedStart), targetStation ); if (!rescheduled) { return { ok: false as const, reason: "Unable to reschedule the requested operation." }; } await logAuditEvent({ actorId, entityType: "work-order", entityId: workOrderId, action: "operations.rescheduled", summary: `Rescheduled work order ${rescheduled.workOrderNumber} from operation ${existing.sequence}.`, metadata: { workOrderNumber: rescheduled.workOrderNumber, operationId, sequence: existing.sequence, plannedStart: payload.plannedStart, previousStationId: existing.stationId, previousStationCode: existing.station.code, previousStationName: existing.station.name, stationId: payload.stationId ?? existing.stationId, }, }); return { ok: true as const, workOrder: rescheduled }; } export async function updateWorkOrderOperationExecution( workOrderId: string, operationId: string, payload: WorkOrderOperationExecutionInput, actorId?: string | null ) { const existing = await prisma.workOrderOperation.findUnique({ where: { id: operationId }, select: { id: true, workOrderId: true, sequence: true, status: true, actualStart: true, actualEnd: true, actualMinutes: true, plannedMinutes: true, workOrder: { select: { status: true, workOrderNumber: true, }, }, }, }); if (!existing || existing.workOrderId !== workOrderId) { return { ok: false as const, reason: "Work-order operation was not found." }; } if (existing.workOrder.status === "COMPLETE" || existing.workOrder.status === "CANCELLED" || existing.workOrder.status === "DRAFT") { return { ok: false as const, reason: "Operation execution can only be updated on released or active work orders." }; } const now = new Date(); const nextData: Record = {}; if (payload.action === "START") { if (existing.status === "COMPLETE") { return { ok: false as const, reason: "Completed operations cannot be restarted." }; } nextData.status = "IN_PROGRESS"; nextData.actualStart = existing.actualStart ?? now; } else if (payload.action === "PAUSE") { if (existing.status !== "IN_PROGRESS") { return { ok: false as const, reason: "Only in-progress operations can be paused." }; } nextData.status = "PAUSED"; nextData.actualStart = existing.actualStart ?? now; } else if (payload.action === "RESUME") { if (existing.status !== "PAUSED" && existing.status !== "PENDING") { return { ok: false as const, reason: "Only paused or pending operations can be resumed." }; } nextData.status = "IN_PROGRESS"; nextData.actualStart = existing.actualStart ?? now; } else if (payload.action === "COMPLETE") { if (existing.status === "COMPLETE") { return { ok: false as const, reason: "Operation is already complete." }; } nextData.status = "COMPLETE"; nextData.actualStart = existing.actualStart ?? now; nextData.actualEnd = now; } await prisma.workOrderOperation.update({ where: { id: operationId }, data: nextData, }); await syncWorkOrderStatusFromOperationActivity(workOrderId); const workOrder = await getWorkOrderById(workOrderId); if (!workOrder) { return { ok: false as const, reason: "Unable to load updated work order." }; } await logAuditEvent({ actorId, entityType: "work-order", entityId: workOrderId, action: "operation.execution.updated", summary: `${payload.action} operation ${existing.sequence} on ${existing.workOrder.workOrderNumber}.`, metadata: { workOrderNumber: existing.workOrder.workOrderNumber, operationId, sequence: existing.sequence, action: payload.action, notes: payload.notes, }, }); return { ok: true as const, workOrder }; } export async function recordWorkOrderOperationLabor( workOrderId: string, operationId: string, payload: WorkOrderOperationLaborEntryInput, createdById?: string | null ) { const existing = await prisma.workOrderOperation.findUnique({ where: { id: operationId }, select: { id: true, workOrderId: true, sequence: true, status: true, actualStart: true, actualMinutes: true, workOrder: { select: { status: true, workOrderNumber: true, }, }, }, }); if (!existing || existing.workOrderId !== workOrderId) { return { ok: false as const, reason: "Work-order operation was not found." }; } if (existing.workOrder.status === "COMPLETE" || existing.workOrder.status === "CANCELLED" || existing.workOrder.status === "DRAFT") { return { ok: false as const, reason: "Labor can only be posted to released or active work orders." }; } if (existing.status === "COMPLETE") { return { ok: false as const, reason: "Completed operations cannot receive additional labor entries." }; } await prisma.$transaction(async (tx) => { await tx.workOrderOperationLaborEntry.create({ data: { operationId, minutes: payload.minutes, notes: payload.notes, createdById: createdById ?? null, }, }); await tx.workOrderOperation.update({ where: { id: operationId }, data: { status: existing.status === "PENDING" ? "IN_PROGRESS" : existing.status, actualStart: existing.actualStart ?? new Date(), actualMinutes: { increment: payload.minutes, }, }, }); }); await syncWorkOrderStatusFromOperationActivity(workOrderId); const workOrder = await getWorkOrderById(workOrderId); if (!workOrder) { return { ok: false as const, reason: "Unable to load updated work order." }; } await logAuditEvent({ actorId: createdById, entityType: "work-order", entityId: workOrderId, action: "operation.labor.recorded", summary: `Recorded labor on operation ${existing.sequence} for ${existing.workOrder.workOrderNumber}.`, metadata: { workOrderNumber: existing.workOrder.workOrderNumber, operationId, sequence: existing.sequence, minutes: payload.minutes, notes: payload.notes, }, }); return { ok: true as const, workOrder }; } export async function issueWorkOrderMaterial(workOrderId: string, payload: WorkOrderMaterialIssueInput, createdById?: string | null) { const workOrder = await workOrderModel.findUnique({ where: { id: workOrderId }, include: buildInclude(), }); if (!workOrder) { return { ok: false as const, reason: "Work order was not found." }; } if (workOrder.status === "DRAFT" || workOrder.status === "CANCELLED" || workOrder.status === "COMPLETE") { return { ok: false as const, reason: "Material can only be issued to released or active work orders." }; } const componentRequirement = (workOrder as WorkOrderRecord).item.bomLines.find((line) => line.componentItem.id === payload.componentItemId); if (!componentRequirement) { return { ok: false as const, reason: "Issued material must be part of the work order BOM." }; } const location = await prisma.warehouseLocation.findUnique({ where: { id: payload.locationId }, select: { id: true, warehouseId: true, }, }); if (!location || location.warehouseId !== payload.warehouseId) { return { ok: false as const, reason: "Warehouse location is invalid for the selected warehouse." }; } const currentDetail = mapDetail(workOrder as WorkOrderRecord, await getComponentAvailability(workOrder as WorkOrderRecord)); const currentRequirement = currentDetail.materialRequirements.find( (requirement: WorkOrderDetailDto["materialRequirements"][number]) => requirement.componentItemId === payload.componentItemId ); if (!currentRequirement) { return { ok: false as const, reason: "Issued material must be part of the work order BOM." }; } if (payload.quantity > currentRequirement.remainingQuantity) { return { ok: false as const, reason: "Material issue exceeds the remaining required quantity." }; } const onHand = await getItemLocationOnHand(payload.componentItemId, payload.warehouseId, payload.locationId); if (onHand < payload.quantity) { return { ok: false as const, reason: "Material issue would drive the selected location below zero on-hand." }; } await prisma.$transaction(async (tx) => { const transactionClient = tx as any; await transactionClient.workOrderMaterialIssue.create({ data: { workOrderId, componentItemId: payload.componentItemId, warehouseId: payload.warehouseId, locationId: payload.locationId, quantity: payload.quantity, notes: payload.notes, createdById: createdById ?? null, }, }); await transactionClient.inventoryTransaction.create({ data: { itemId: payload.componentItemId, warehouseId: payload.warehouseId, locationId: payload.locationId, transactionType: "ISSUE", quantity: payload.quantity, reference: `${(workOrder as WorkOrderRecord).workOrderNumber} material issue`, notes: payload.notes, createdById: createdById ?? null, }, }); await transactionClient.workOrder.update({ where: { id: workOrderId }, data: { status: workOrder.status === "RELEASED" || workOrder.status === "ON_HOLD" ? "IN_PROGRESS" : workOrder.status, }, }); }); await syncWorkOrderReservations(workOrderId); const nextWorkOrder = await getWorkOrderById(workOrderId); if (nextWorkOrder) { await logAuditEvent({ actorId: createdById, entityType: "work-order", entityId: workOrderId, action: "material.issued", summary: `Issued material to work order ${nextWorkOrder.workOrderNumber}.`, metadata: { workOrderNumber: nextWorkOrder.workOrderNumber, componentItemId: payload.componentItemId, warehouseId: payload.warehouseId, locationId: payload.locationId, quantity: payload.quantity, }, }); } return nextWorkOrder ? { ok: true as const, workOrder: nextWorkOrder } : { ok: false as const, reason: "Unable to load updated work order." }; } export async function recordWorkOrderCompletion(workOrderId: string, payload: WorkOrderCompletionInput, createdById?: string | null) { const workOrder = await workOrderModel.findUnique({ where: { id: workOrderId }, include: buildInclude(), }); if (!workOrder) { return { ok: false as const, reason: "Work order was not found." }; } if (workOrder.status === "DRAFT" || workOrder.status === "CANCELLED" || workOrder.status === "COMPLETE") { return { ok: false as const, reason: "Completion can only be posted to released or active work orders." }; } const remainingQuantity = workOrder.quantity - workOrder.completedQuantity; if (payload.quantity > remainingQuantity) { return { ok: false as const, reason: "Completion quantity exceeds the remaining build quantity." }; } const nextCompletedQuantity = workOrder.completedQuantity + payload.quantity; const nextStatus = nextCompletedQuantity >= workOrder.quantity ? "COMPLETE" : "IN_PROGRESS"; await prisma.$transaction(async (tx) => { const transactionClient = tx as any; await transactionClient.workOrderCompletion.create({ data: { workOrderId, quantity: payload.quantity, notes: payload.notes, createdById: createdById ?? null, }, }); await transactionClient.inventoryTransaction.create({ data: { itemId: workOrder.item.id, warehouseId: workOrder.warehouse.id, locationId: workOrder.location.id, transactionType: "RECEIPT", quantity: payload.quantity, reference: `${workOrder.workOrderNumber} production completion`, notes: payload.notes, createdById: createdById ?? null, }, }); await transactionClient.workOrder.update({ where: { id: workOrderId }, data: { completedQuantity: nextCompletedQuantity, status: nextStatus, }, }); }); await syncWorkOrderReservations(workOrderId); const nextWorkOrder = await getWorkOrderById(workOrderId); if (nextWorkOrder) { await logAuditEvent({ actorId: createdById, entityType: "work-order", entityId: workOrderId, action: "completion.recorded", summary: `Recorded completion against work order ${nextWorkOrder.workOrderNumber}.`, metadata: { workOrderNumber: nextWorkOrder.workOrderNumber, quantity: payload.quantity, status: nextWorkOrder.status, }, }); } return nextWorkOrder ? { ok: true as const, workOrder: nextWorkOrder } : { ok: false as const, reason: "Unable to load updated work order." }; }