import type { ManufacturingItemOptionDto, ManufacturingProjectOptionDto, WorkOrderCompletionInput, WorkOrderDetailDto, WorkOrderInput, WorkOrderMaterialIssueInput, WorkOrderStatus, WorkOrderSummaryDto, } from "@mrp/shared"; import { prisma } from "../../lib/prisma.js"; const workOrderModel = (prisma as any).workOrder; 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; bomLines: Array<{ quantity: number; unitOfMeasure: string; componentItem: { id: string; sku: string; name: string; }; }>; }; project: { id: string; projectNumber: string; name: string; customer: { name: string; }; } | null; warehouse: { id: string; code: string; name: string; }; location: { id: string; code: string; name: string; }; 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 buildInclude() { return { item: { include: { bomLines: { include: { componentItem: { select: { id: true, sku: true, name: true, }, }, }, orderBy: [{ position: "asc" }, { createdAt: "asc" }], }, }, }, project: { include: { customer: { select: { name: true, }, }, }, }, warehouse: { select: { id: true, code: true, name: true, }, }, location: { select: { id: true, code: true, name: true, }, }, 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, 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, updatedAt: record.updatedAt.toISOString(), }; } function mapDetail(record: WorkOrderRecord): 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, materialRequirements: record.item.bomLines.map((line) => { const requiredQuantity = line.quantity * record.quantity; const issuedQuantity = issuedByComponent.get(line.componentItem.id) ?? 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), }; }), 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), })), }; } 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 validateWorkOrderInput(payload: WorkOrderInput) { const item = await prisma.inventoryItem.findUnique({ where: { id: payload.itemId }, select: { id: true, type: true, status: 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 (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." }; } } 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, }, orderBy: [{ sku: "asc" }], }); return items; } 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(), }); return workOrder ? mapDetail(workOrder as WorkOrderRecord) : null; } export async function createWorkOrder(payload: WorkOrderInput) { 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, 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, }, }); const workOrder = await getWorkOrderById(created.id); 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) { 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, warehouseId: payload.warehouseId, locationId: payload.locationId, status: payload.status, quantity: payload.quantity, dueDate: payload.dueDate ? new Date(payload.dueDate) : null, notes: payload.notes, }, }); const workOrder = await getWorkOrderById(workOrderId); 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) { 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, }, }); const workOrder = await getWorkOrderById(workOrderId); return workOrder ? { ok: true as const, workOrder } : { ok: false as const, reason: "Unable to load saved work order." }; } 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); 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, }, }); }); const nextWorkOrder = await getWorkOrderById(workOrderId); 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, }, }); }); const nextWorkOrder = await getWorkOrderById(workOrderId); return nextWorkOrder ? { ok: true as const, workOrder: nextWorkOrder } : { ok: false as const, reason: "Unable to load updated work order." }; }