import { permissions } from "@mrp/shared"; import type { WorkOrderCompletionInput, WorkOrderDetailDto, WorkOrderMaterialIssueInput, WorkOrderOperationScheduleInput, 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 [issueForm, setIssueForm] = useState(emptyMaterialIssueInput); const [completionForm, setCompletionForm] = useState(emptyCompletionInput); 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 [reschedulingOperationId, setReschedulingOperationId] = 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 }]) ) ); 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([])); }, [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, nextStatus); setWorkOrder(nextWorkOrder); 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); } } 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 === "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, }); } 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}

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)}

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 Capacity Start End MinutesReschedule
{operation.sequence}
{operation.stationCode}
{operation.stationName}
{operation.stationDailyCapacityMinutes} min/day x {operation.stationParallelCapacity}
{operation.stationWorkingDays.join(",")}
{new Date(operation.plannedStart).toLocaleString()} {new Date(operation.plannedEnd).toLocaleString()} {operation.plannedMinutes}
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" />
)}
{canManage ? (

Material Issue