manufacturing layer

This commit is contained in:
2026-03-18 06:22:37 -05:00
parent 6eaf084fcd
commit c49ed4bf4a
14 changed files with 561 additions and 20 deletions

View File

@@ -6,7 +6,9 @@ import type {
WorkOrderCompletionInput,
WorkOrderDetailDto,
WorkOrderInput,
WorkOrderOperationExecutionInput,
WorkOrderOperationDto,
WorkOrderOperationLaborEntryInput,
WorkOrderOperationScheduleInput,
WorkOrderMaterialIssueInput,
WorkOrderStatus,
@@ -109,6 +111,10 @@ type WorkOrderRecord = {
plannedStart: Date;
plannedEnd: Date;
notes: string;
status: string;
actualStart: Date | null;
actualEnd: Date | null;
actualMinutes: number;
station: {
id: string;
code: string;
@@ -117,6 +123,16 @@ type WorkOrderRecord = {
parallelCapacity: number;
workingDays: string;
};
laborEntries: Array<{
id: string;
minutes: number;
notes: string;
createdAt: Date;
createdBy: {
firstName: string;
lastName: string;
} | null;
}>;
}>;
materialIssues: Array<{
id: string;
@@ -252,6 +268,17 @@ function buildInclude() {
workingDays: true,
},
},
laborEntries: {
include: {
createdBy: {
select: {
firstName: true,
lastName: true,
},
},
},
orderBy: [{ createdAt: "desc" }],
},
},
orderBy: [{ sequence: "asc" }],
},
@@ -352,6 +379,7 @@ function mapDetail(
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,
@@ -368,6 +396,18 @@ function mapDetail(
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;
@@ -566,6 +606,7 @@ async function regenerateWorkOrderOperations(workOrderId: string) {
plannedStart: operation.plannedStart,
plannedEnd: operation.plannedEnd,
notes: operation.notes,
status: "PENDING",
})),
});
}
@@ -634,6 +675,29 @@ async function rescheduleWorkOrderOperationsToStation(
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) {
@@ -1312,6 +1376,183 @@ export async function updateWorkOrderOperationSchedule(
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<string, unknown> = {};
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 },