diff --git a/CHANGELOG.md b/CHANGELOG.md index f605c4f..bb18398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ This file is the running release and change log for CODEXIUM. Keep it updated wh - Manufacturing finite-capacity slice with station daily capacity, parallel capacity, working-day calendars, calendar-aware operation scheduling, and operation-level rescheduling from the work-order detail page - Manufacturing station edit support for working days, active state, queue, and capacity settings directly from the manufacturing screen - Operation execution controls on work orders, including start/pause/resume/complete actions, labor posting, and actual-minute rollups by operation and work order +- Operation operator assignment and timer-based labor capture, with timer stop posting elapsed minutes back as labor entries - Workbench rebalance controls for operation rows, including planner-side datetime rescheduling, quick shift moves, and heatmap-day targeting without leaving the dispatch surface - Workbench station-to-station rebalance so planners can move an operation onto another active work center and rebuild the downstream chain from the same dispatch surface - Workbench drag scheduling in station grouping mode, with draggable operation cards, station drop targets, heatmap-day-aware drop timing, and projected post-drop load cues before the move is committed diff --git a/README.md b/README.md index 535f8f3..881e0be 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Current foundation scope includes: - purchase-order supporting documents for vendor invoices, acknowledgements, certifications, and backup files - shipping shipments linked to sales orders with packing slips, shipping labels, bills of lading, and logistics attachments - projects with customer/commercial/shipment linkage, owners, due dates, milestones, rollups, notes, and attachments -- manufacturing work orders with project linkage, station-based operation templates, editable station calendars/capacity settings, calendar-aware operation scheduling, operation execution controls, labor posting, material issue posting, completion posting, operation rescheduling, and work-order attachments +- manufacturing work orders with project linkage, station-based operation templates, editable station calendars/capacity settings, calendar-aware operation scheduling, operation execution controls, operator assignment, timer-based and manual labor posting, material issue posting, completion posting, operation rescheduling, and work-order attachments - planning workbench with live project/manufacturing schedule data, exception rail, heatmap load view, agenda view, focus drawer, station load grouping, readiness filters, overload visibility, inline dispatch actions, planner-side operation rebalance controls including station-to-station moves, and station-lane drag scheduling - sales-order demand planning with multi-level BOM explosion, stock/open-supply netting, and build/buy recommendations - planner-assisted conversion of demand-planning recommendations into prefilled work-order and purchase-order drafts @@ -110,7 +110,7 @@ Next expansion areas: ## Manufacturing Direction -Manufacturing is now a separate execution subsystem rather than being collapsed into Projects. The current slice ships work-order records with build-item linkage, optional project linkage, warehouse/location output posting, BOM-based material requirement visibility, station master data, item-level operation templates, editable station calendars and capacity settings, automatic work-order operation plans, operation-level execution controls, labor posting, operation-level rescheduling, material issue posting, completion posting, work-order attachments, and dashboard visibility. +Manufacturing is now a separate execution subsystem rather than being collapsed into Projects. The current slice ships work-order records with build-item linkage, optional project linkage, warehouse/location output posting, BOM-based material requirement visibility, station master data, item-level operation templates, editable station calendars and capacity settings, automatic work-order operation plans, operation-level execution controls, operator assignment, timer-based and manual labor posting, operation-level rescheduling, material issue posting, completion posting, work-order attachments, and dashboard visibility. Current interactions: diff --git a/ROADMAP.md b/ROADMAP.md index 50e2fe1..5c7304d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -90,7 +90,7 @@ This file tracks work that still needs to be completed. Shipped phase history an - Work orders tied more explicitly to sales demand or internal build demand where appropriate - Routing/work-center structure for manufacturing steps and handoffs beyond the current station templates - Material consumption depth, WIP tracking, and execution traceability -- Deeper labor depth beyond the shipped manual operation labor posting, including crew assignment, live timer capture, and machine/runtime integration +- Deeper labor depth beyond the shipped operator assignment and timer-based labor capture, including crew-level staffing, labor approvals, and machine/runtime integration - Manufacturing rollups for open work, blockers, shortages, and throughput - Traveler/job packet output - Partial completions and split-order execution visibility diff --git a/SHIPPED.md b/SHIPPED.md index 5110e7e..d6846ff 100644 --- a/SHIPPED.md +++ b/SHIPPED.md @@ -36,7 +36,7 @@ This file tracks roadmap phases, slices, and major foundations that have already - Project milestones and project-side milestone/work-order rollups - Project cockpit section on detail pages for commercial, supply, execution, delivery, purchasing, readiness-risk, and cost-snapshot visibility, with direct launch paths into prefilled project work orders and demand-linked purchase orders plus a project activity timeline - Project list/detail/create/edit workflows and dashboard program widgets -- Manufacturing foundation with work orders, project linkage, operation execution controls, labor posting, material issue posting, completion posting, and work-order attachments +- Manufacturing foundation with work orders, project linkage, operation execution controls, operator assignment, timer-based and manual labor posting, material issue posting, completion posting, and work-order attachments - Manufacturing stations, item routing templates, editable station calendars/capacity settings, automatic work-order operation planning, and operation-level rescheduling for the workbench schedule - Vendor invoice/supporting-document attachments directly on purchase orders - Vendor-detail purchasing visibility with recent purchase-order activity diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index be30030..fd9bc23 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -61,12 +61,15 @@ import type { WorkOrderCompletionInput, WorkOrderDetailDto, WorkOrderInput, + WorkOrderOperationAssignmentInput, WorkOrderOperationExecutionInput, WorkOrderOperationLaborEntryInput, WorkOrderOperationScheduleInput, + WorkOrderOperationTimerInput, WorkOrderMaterialIssueInput, WorkOrderStatus, WorkOrderSummaryDto, + ManufacturingUserOptionDto, } from "@mrp/shared"; import type { ProjectCustomerOptionDto, @@ -608,6 +611,9 @@ export const api = { getManufacturingProjectOptions(token: string) { return request("/api/v1/manufacturing/projects/options", undefined, token); }, + getManufacturingUserOptions(token: string) { + return request("/api/v1/manufacturing/users/options", undefined, token); + }, getManufacturingStations(token: string) { return request("/api/v1/manufacturing/stations", undefined, token); }, @@ -666,6 +672,20 @@ export const api = { token ); }, + updateWorkOrderOperationAssignment(token: string, workOrderId: string, operationId: string, payload: WorkOrderOperationAssignmentInput) { + return request( + `/api/v1/manufacturing/work-orders/${workOrderId}/operations/${operationId}/assignment`, + { method: "PATCH", body: JSON.stringify(payload) }, + token + ); + }, + updateWorkOrderOperationTimer(token: string, workOrderId: string, operationId: string, payload: WorkOrderOperationTimerInput) { + return request( + `/api/v1/manufacturing/work-orders/${workOrderId}/operations/${operationId}/timer`, + { method: "PATCH", body: JSON.stringify(payload) }, + token + ); + }, issueWorkOrderMaterial(token: string, workOrderId: string, payload: WorkOrderMaterialIssueInput) { return request( `/api/v1/manufacturing/work-orders/${workOrderId}/issues`, diff --git a/client/src/modules/manufacturing/WorkOrderDetailPage.tsx b/client/src/modules/manufacturing/WorkOrderDetailPage.tsx index 955a411..e4c3b7b 100644 --- a/client/src/modules/manufacturing/WorkOrderDetailPage.tsx +++ b/client/src/modules/manufacturing/WorkOrderDetailPage.tsx @@ -1,11 +1,14 @@ 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"; @@ -24,6 +27,7 @@ export function WorkOrderDetailPage() { 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 [status, setStatus] = useState("Loading work order..."); @@ -32,9 +36,13 @@ export function WorkOrderDetailPage() { 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"; @@ -79,6 +87,16 @@ export function WorkOrderDetailPage() { 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) => { @@ -87,6 +105,7 @@ export function WorkOrderDetailPage() { }); api.getWarehouseLocationOptions(token).then(setLocationOptions).catch(() => setLocationOptions([])); + api.getManufacturingUserOptions(token).then(setOperatorOptions).catch(() => setOperatorOptions([])); }, [token, workOrderId]); const filteredLocationOptions = useMemo( @@ -247,6 +266,60 @@ export function WorkOrderDetailPage() { } } + 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; @@ -402,6 +475,8 @@ export function WorkOrderDetailPage() {
{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"}
@@ -431,6 +506,35 @@ export function WorkOrderDetailPage() { ) : null} +
+ + +
+
+ + 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" + /> + +