import { permissions } from "@mrp/shared"; import type { ManufacturingUserOptionDto, WorkOrderCompletionInput, WorkOrderDetailDto, WorkOrderMaterialIssueInput, WorkOrderOperationAssignmentInput, WorkOrderOperationExecutionInput, WorkOrderOperationLaborEntryInput, WorkOrderOperationScheduleInput, WorkOrderOperationTimerInput, WorkOrderStatus, } from "@mrp/shared"; import type { WarehouseLocationOptionDto } from "@mrp/shared/dist/inventory/types.js"; import { useEffect, useMemo, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { FileAttachmentsPanel } from "../../components/FileAttachmentsPanel"; import { useAuth } from "../../auth/AuthProvider"; import { api, ApiError } from "../../lib/api"; import { ConfirmActionDialog } from "../../components/ConfirmActionDialog"; import { emptyCompletionInput, emptyMaterialIssueInput, workOrderStatusOptions } from "./config"; import { WorkOrderStatusBadge } from "./WorkOrderStatusBadge"; export function WorkOrderDetailPage() { const { token, user } = useAuth(); const { workOrderId } = useParams(); const [workOrder, setWorkOrder] = useState(null); const [locationOptions, setLocationOptions] = useState([]); const [operatorOptions, setOperatorOptions] = useState([]); const [issueForm, setIssueForm] = useState(emptyMaterialIssueInput); const [completionForm, setCompletionForm] = useState(emptyCompletionInput); const [holdReasonDraft, setHoldReasonDraft] = useState(""); const [status, setStatus] = useState("Loading work order..."); const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); const [isPostingIssue, setIsPostingIssue] = useState(false); const [isPostingCompletion, setIsPostingCompletion] = useState(false); const [operationScheduleForm, setOperationScheduleForm] = useState>({}); const [operationLaborForm, setOperationLaborForm] = useState>({}); const [operationAssignmentForm, setOperationAssignmentForm] = useState>({}); const [operationTimerForm, setOperationTimerForm] = useState>({}); const [reschedulingOperationId, setReschedulingOperationId] = useState(null); const [executingOperationId, setExecutingOperationId] = useState(null); const [postingLaborOperationId, setPostingLaborOperationId] = useState(null); const [assigningOperationId, setAssigningOperationId] = useState(null); const [timerOperationId, setTimerOperationId] = useState(null); const [pendingConfirmation, setPendingConfirmation] = useState< | { kind: "status" | "issue" | "completion"; title: string; description: string; impact: string; recovery: string; confirmLabel: string; confirmationLabel?: string; confirmationValue?: string; nextStatus?: WorkOrderStatus; } | null >(null); const canManage = user?.permissions.includes(permissions.manufacturingWrite) ?? false; useEffect(() => { if (!token || !workOrderId) { return; } api.getWorkOrder(token, workOrderId) .then((nextWorkOrder) => { setWorkOrder(nextWorkOrder); setIssueForm({ ...emptyMaterialIssueInput, warehouseId: nextWorkOrder.warehouseId, locationId: nextWorkOrder.locationId, }); setCompletionForm({ ...emptyCompletionInput, quantity: Math.max(nextWorkOrder.dueQuantity, 1), }); setOperationScheduleForm( Object.fromEntries( nextWorkOrder.operations.map((operation) => [operation.id, { plannedStart: operation.plannedStart }]) ) ); setOperationLaborForm( Object.fromEntries( nextWorkOrder.operations.map((operation) => [operation.id, { minutes: Math.max(Math.round(operation.plannedMinutes / 4), 15), notes: "" }]) ) ); setOperationAssignmentForm( Object.fromEntries( nextWorkOrder.operations.map((operation) => [operation.id, { assignedOperatorId: operation.assignedOperatorId }]) ) ); setOperationTimerForm( Object.fromEntries( nextWorkOrder.operations.map((operation) => [operation.id, { action: operation.activeTimerStartedAt ? "STOP" : "START", notes: "" }]) ) ); setStatus("Work order loaded."); }) .catch((error: unknown) => { const message = error instanceof ApiError ? error.message : "Unable to load work order."; setStatus(message); }); api.getWarehouseLocationOptions(token).then(setLocationOptions).catch(() => setLocationOptions([])); api.getManufacturingUserOptions(token).then(setOperatorOptions).catch(() => setOperatorOptions([])); }, [token, workOrderId]); const filteredLocationOptions = useMemo( () => locationOptions.filter((option) => option.warehouseId === issueForm.warehouseId), [issueForm.warehouseId, locationOptions] ); async function applyStatusChange(nextStatus: WorkOrderStatus) { if (!token || !workOrder) { return; } setIsUpdatingStatus(true); setStatus("Updating work-order status..."); try { const nextWorkOrder = await api.updateWorkOrderStatus(token, workOrder.id, { status: nextStatus, reason: nextStatus === "ON_HOLD" ? holdReasonDraft : null, }); setWorkOrder(nextWorkOrder); setHoldReasonDraft(""); setStatus("Work-order status updated. Review downstream planning and shipment readiness if this change affects execution timing."); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : "Unable to update work-order status."; setStatus(message); } finally { setIsUpdatingStatus(false); } } async function submitIssue() { if (!token || !workOrder) { return; } setIsPostingIssue(true); setStatus("Posting material issue..."); try { const nextWorkOrder = await api.issueWorkOrderMaterial(token, workOrder.id, issueForm); setWorkOrder(nextWorkOrder); setIssueForm({ ...emptyMaterialIssueInput, warehouseId: nextWorkOrder.warehouseId, locationId: nextWorkOrder.locationId, }); setStatus("Material issue posted. This consumed inventory immediately; post a correcting stock movement if the issue quantity was wrong."); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : "Unable to post material issue."; setStatus(message); } finally { setIsPostingIssue(false); } } async function submitCompletion() { if (!token || !workOrder) { return; } setIsPostingCompletion(true); setStatus("Posting completion..."); try { const nextWorkOrder = await api.recordWorkOrderCompletion(token, workOrder.id, completionForm); setWorkOrder(nextWorkOrder); setCompletionForm({ ...emptyCompletionInput, quantity: Math.max(nextWorkOrder.dueQuantity, 1), }); setStatus("Completion posted. Finished-goods stock has been received; verify the remaining quantity and post a correcting transaction if this completion was overstated."); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : "Unable to post completion."; setStatus(message); } finally { setIsPostingCompletion(false); } } async function submitOperationReschedule(operationId: string) { if (!token || !workOrder) { return; } const payload = operationScheduleForm[operationId]; if (!payload?.plannedStart) { return; } setReschedulingOperationId(operationId); setStatus("Rebuilding operation schedule..."); try { const nextWorkOrder = await api.updateWorkOrderOperationSchedule(token, workOrder.id, operationId, payload); setWorkOrder(nextWorkOrder); setOperationScheduleForm( Object.fromEntries( nextWorkOrder.operations.map((operation) => [operation.id, { plannedStart: operation.plannedStart }]) ) ); setStatus("Operation schedule updated with station calendar constraints."); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : "Unable to reschedule operation."; setStatus(message); } finally { setReschedulingOperationId(null); } } async function submitOperationExecution(operationId: string, action: WorkOrderOperationExecutionInput["action"]) { if (!token || !workOrder) { return; } setExecutingOperationId(operationId); setStatus("Updating operation execution..."); try { const nextWorkOrder = await api.updateWorkOrderOperationExecution(token, workOrder.id, operationId, { action, notes: `${action} from work-order detail`, }); setWorkOrder(nextWorkOrder); setOperationScheduleForm( Object.fromEntries( nextWorkOrder.operations.map((operation) => [operation.id, { plannedStart: operation.plannedStart }]) ) ); setStatus("Operation execution updated."); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : "Unable to update operation execution."; setStatus(message); } finally { setExecutingOperationId(null); } } async function submitOperationLabor(operationId: string) { if (!token || !workOrder) { return; } const payload = operationLaborForm[operationId]; if (!payload?.minutes) { return; } setPostingLaborOperationId(operationId); setStatus("Posting labor entry..."); try { const nextWorkOrder = await api.recordWorkOrderOperationLabor(token, workOrder.id, operationId, payload); setWorkOrder(nextWorkOrder); setOperationLaborForm((current) => ({ ...current, [operationId]: { minutes: Math.max(Math.round((nextWorkOrder.operations.find((operation) => operation.id === operationId)?.plannedMinutes ?? 60) / 4), 15), notes: "", }, })); setStatus("Labor entry posted."); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : "Unable to post operation labor."; setStatus(message); } finally { setPostingLaborOperationId(null); } } async function submitOperationAssignment(operationId: string) { if (!token || !workOrder) { return; } const payload = operationAssignmentForm[operationId]; if (!payload) { return; } setAssigningOperationId(operationId); setStatus("Updating operator assignment..."); try { const nextWorkOrder = await api.updateWorkOrderOperationAssignment(token, workOrder.id, operationId, payload); setWorkOrder(nextWorkOrder); setStatus("Operator assignment updated."); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : "Unable to update operator assignment."; setStatus(message); } finally { setAssigningOperationId(null); } } async function submitOperationTimer(operationId: string, action: WorkOrderOperationTimerInput["action"]) { if (!token || !workOrder) { return; } const payload = operationTimerForm[operationId] ?? { action, notes: "" }; setTimerOperationId(operationId); setStatus(action === "START" ? "Starting timer..." : "Stopping timer..."); try { const nextWorkOrder = await api.updateWorkOrderOperationTimer(token, workOrder.id, operationId, { action, notes: payload.notes, }); setWorkOrder(nextWorkOrder); setOperationTimerForm((current) => ({ ...current, [operationId]: { action: action === "START" ? "STOP" : "START", notes: "", }, })); setStatus(action === "START" ? "Operation timer started." : "Operation timer stopped and labor posted."); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : "Unable to update operation timer."; setStatus(message); } finally { setTimerOperationId(null); } } function handleStatusChange(nextStatus: WorkOrderStatus) { if (!workOrder) { return; } const option = workOrderStatusOptions.find((entry) => entry.value === nextStatus); setPendingConfirmation({ kind: "status", title: `Change status to ${option?.label ?? nextStatus}`, description: `Update work order ${workOrder.workOrderNumber} from ${workOrder.status} to ${nextStatus}.`, impact: nextStatus === "CANCELLED" ? "Cancelling a work order can invalidate planning assumptions, reservations, and operator expectations." : nextStatus === "ON_HOLD" ? "Putting a work order on hold pauses expected execution and should capture the exact blocker so planning and shop-floor review stay aligned." : nextStatus === "COMPLETE" ? "Completing the work order signals execution closure and can change readiness views across the system." : "This changes the execution state used by planning, dashboards, and downstream operational review.", recovery: "If this status was selected in error, set the work order back to the correct state immediately after review.", confirmLabel: `Set ${option?.label ?? nextStatus}`, confirmationLabel: nextStatus === "CANCELLED" ? "Type work-order number to confirm:" : undefined, confirmationValue: nextStatus === "CANCELLED" ? workOrder.workOrderNumber : undefined, nextStatus, }); setHoldReasonDraft(nextStatus === "ON_HOLD" ? workOrder.holdReason ?? "" : ""); } function handleIssueSubmit(event: React.FormEvent) { event.preventDefault(); if (!workOrder) { return; } const component = workOrder.materialRequirements.find((requirement) => requirement.componentItemId === issueForm.componentItemId); setPendingConfirmation({ kind: "issue", title: "Post material issue", description: `Issue ${issueForm.quantity} units of ${component?.componentSku ?? "the selected component"} to work order ${workOrder.workOrderNumber}.`, impact: "This consumes component inventory immediately and updates work-order material history.", recovery: "If the wrong quantity was issued, post a correcting stock transaction and note the reason on the work order.", confirmLabel: "Post issue", confirmationLabel: "Type work-order number to confirm:", confirmationValue: workOrder.workOrderNumber, }); } function handleCompletionSubmit(event: React.FormEvent) { event.preventDefault(); if (!workOrder) { return; } setPendingConfirmation({ kind: "completion", title: "Post production completion", description: `Receive ${completionForm.quantity} finished units into ${workOrder.warehouseCode} / ${workOrder.locationCode}.`, impact: "This increases finished-goods inventory immediately and advances the execution history for this work order.", recovery: "If the completion quantity is wrong, post the correcting inventory movement and verify the work-order remaining quantity.", confirmLabel: "Post completion", confirmationLabel: completionForm.quantity >= workOrder.dueQuantity ? "Type work-order number to confirm:" : undefined, confirmationValue: completionForm.quantity >= workOrder.dueQuantity ? workOrder.workOrderNumber : undefined, }); } if (!workOrder) { return
{status}
; } return (

Work Order

{workOrder.workOrderNumber}

{workOrder.itemSku} - {workOrder.itemName}

{workOrder.status === "ON_HOLD" && workOrder.holdReason ? (
Current Hold Reason
{workOrder.holdReason}
) : null}
Back to work orders {workOrder.projectId ? Open project : null} {workOrder.salesOrderId ? Open sales order : null} Open item {canManage ? Edit work order : null}
{canManage ? (

Quick Actions

Release, hold, or close administrative status from the work-order record.

{workOrderStatusOptions.map((option) => ( ))}
) : null}

Planned

{workOrder.quantity}

Completed

{workOrder.completedQuantity}

Remaining

{workOrder.dueQuantity}

Project

{workOrder.projectNumber || "Unlinked"}

Operations

{workOrder.operations.length}

Due Date

{workOrder.dueDate ? new Date(workOrder.dueDate).toLocaleDateString() : "Not set"}

Material Shortage

{workOrder.materialRequirements.reduce((sum, requirement) => sum + requirement.shortageQuantity, 0)}

Actual Hours

{(workOrder.totalActualMinutes / 60).toFixed(1)}

Execution Context

Build item
{workOrder.itemSku} - {workOrder.itemName}
Item type
{workOrder.itemType}
Output location
{workOrder.warehouseCode} / {workOrder.locationCode}
Project customer
{workOrder.projectCustomerName || "Not linked"}
Demand source
{workOrder.salesOrderNumber ?? "Not linked"}

Work Instructions

{workOrder.notes || "No work-order notes recorded."}

Operation Plan

{workOrder.operations.length === 0 ? (
This work order has no inherited station operations. Add routing steps on the item record to automate planning.
) : (
{canManage ? : null} {workOrder.operations.map((operation) => ( {canManage ? ( ) : null} ))}
Seq Station Execution Capacity Start End Planned / ActualExecution Controls
{operation.sequence}
{operation.stationCode}
{operation.stationName}
{operation.status.replaceAll("_", " ")}
Start {operation.actualStart ? new Date(operation.actualStart).toLocaleString() : "Not started"}
End {operation.actualEnd ? new Date(operation.actualEnd).toLocaleString() : "Open"}
Operator {operation.assignedOperatorName ?? "Unassigned"}
{operation.activeTimerStartedAt ? `Timer running since ${new Date(operation.activeTimerStartedAt).toLocaleTimeString()}` : "Timer stopped"}
{operation.laborEntryCount} labor entr{operation.laborEntryCount === 1 ? "y" : "ies"}
{operation.stationDailyCapacityMinutes} min/day x {operation.stationParallelCapacity}
{operation.stationWorkingDays.join(",")}
{new Date(operation.plannedStart).toLocaleString()} {new Date(operation.plannedEnd).toLocaleString()}
{operation.plannedMinutes} planned
{operation.actualMinutes} actual
{operation.status === "PENDING" ? ( ) : null} {(operation.status === "PENDING" || operation.status === "PAUSED") ? ( ) : null} {operation.status === "IN_PROGRESS" ? ( ) : null} {operation.status !== "COMPLETE" ? ( ) : null}
setOperationScheduleForm((current) => ({ ...current, [operation.id]: { plannedStart: new Date(event.target.value).toISOString() }, })) } className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-xs text-text outline-none transition focus:border-brand" />
setOperationLaborForm((current) => ({ ...current, [operation.id]: { ...(current[operation.id] ?? { notes: "" }), minutes: Number.parseInt(event.target.value, 10) || 1, }, })) } className="w-24 rounded-2xl border border-line/70 bg-page px-2 py-2 text-xs text-text outline-none transition focus:border-brand" /> setOperationLaborForm((current) => ({ ...current, [operation.id]: { ...(current[operation.id] ?? { minutes: 15 }), notes: event.target.value, }, })) } className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-xs text-text outline-none transition focus:border-brand" />
setOperationTimerForm((current) => ({ ...current, [operation.id]: { action: operation.activeTimerStartedAt ? "STOP" : "START", notes: event.target.value, }, })) } className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-xs text-text outline-none transition focus:border-brand" />
)}
{canManage ? (

Material Issue