From 0596970b9926c98367bb2afb2236c3b099180786 Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 15 Mar 2026 11:12:58 -0500 Subject: [PATCH] manufacturing --- AGENTS.md | 13 +- CHANGELOG.md | 6 +- INSTRUCTIONS.md | 2 +- README.md | 29 +- ROADMAP.md | 24 +- UNRAID.md | 2 +- client/src/components/AppShell.tsx | 13 + client/src/lib/api.ts | 58 ++ client/src/main.tsx | 17 + .../src/modules/dashboard/DashboardPage.tsx | 79 ++- .../manufacturing/ManufacturingPage.tsx | 5 + .../manufacturing/WorkOrderDetailPage.tsx | 338 +++++++++ .../manufacturing/WorkOrderFormPage.tsx | 268 +++++++ .../manufacturing/WorkOrderListPage.tsx | 107 +++ .../manufacturing/WorkOrderStatusBadge.tsx | 7 + client/src/modules/manufacturing/config.ts | 48 ++ .../migration.sql | 76 ++ server/prisma/schema.prisma | 71 ++ server/src/app.ts | 2 + server/src/lib/bootstrap.ts | 2 + server/src/modules/manufacturing/router.ts | 180 +++++ server/src/modules/manufacturing/service.ts | 671 ++++++++++++++++++ shared/src/auth/permissions.ts | 2 + shared/src/index.ts | 1 + shared/src/manufacturing/types.ts | 113 +++ 25 files changed, 2097 insertions(+), 37 deletions(-) create mode 100644 client/src/modules/manufacturing/ManufacturingPage.tsx create mode 100644 client/src/modules/manufacturing/WorkOrderDetailPage.tsx create mode 100644 client/src/modules/manufacturing/WorkOrderFormPage.tsx create mode 100644 client/src/modules/manufacturing/WorkOrderListPage.tsx create mode 100644 client/src/modules/manufacturing/WorkOrderStatusBadge.tsx create mode 100644 client/src/modules/manufacturing/config.ts create mode 100644 server/prisma/migrations/20260315163000_manufacturing_foundation/migration.sql create mode 100644 server/src/modules/manufacturing/router.ts create mode 100644 server/src/modules/manufacturing/service.ts create mode 100644 shared/src/manufacturing/types.ts diff --git a/AGENTS.md b/AGENTS.md index f9cc350..d9a2dcb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,7 @@ MRP Codex is a modular Manufacturing Resource Planning platform intended to be a - sales quotes, sales orders, and purchase orders - shipping shipments, packing-slip PDFs, shipping labels, bills of lading, and logistics attachments - projects with customer/commercial/shipment linkage, owners, due dates, notes, and attachments +- manufacturing work orders with project linkage, material issue posting, completion posting, and attachments - Puppeteer PDF foundation - single-container Docker deployment @@ -107,7 +108,7 @@ If implementation changes invalidate those docs, update them in the same change - Customer-facing and logistics PDFs should continue to use the backend documents module and Puppeteer pipeline - The landing experience should remain `Dashboard`, not `Overview`, and should evolve as a modular metric-first operational surface - Projects are a first-class domain that anchors long-running program execution across CRM, sales, inventory, purchasing, shipping, and planning, and future work should continue extending that module rather than scattering project state elsewhere -- Manufacturing should remain a separate future domain for work orders, routings, labor, and shop-floor execution +- Manufacturing is now a first-class domain for work orders and inventory-backed execution, and future work should keep expanding it as a separate subsystem for routings, labor, and shop-floor control - Planning should remain the scheduling/visibility layer over projects and manufacturing, not a replacement for either - New top-level modules added to shell navigation should ship with a matching SVG icon, not text-only nav entries @@ -115,11 +116,11 @@ If implementation changes invalidate those docs, update them in the same change Near-term priorities are: -1. Manufacturing execution -2. Vendor invoice/supporting-document attachments and broader vendor-side operational depth -3. Sales approvals and document revision history -4. Planning and gantt scheduling with live project/manufacturing data -5. Inventory transfers, reservations, and deeper stock controls +1. Vendor invoice/supporting-document attachments and broader vendor-side operational depth +2. Sales approvals and document revision history +3. Planning and gantt scheduling with live project/manufacturing data +4. Inventory transfers, reservations, and deeper stock controls +5. Broader audit-trail coverage and operational diagnostics When adding new modules, preserve the ability to extend the system without refactoring the existing app shell. diff --git a/CHANGELOG.md b/CHANGELOG.md index aedc9af..2ca39d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,15 @@ This file is the running release and change log for MRP Codex. Keep it updated w - Project linkage to sales quotes, sales orders, and shipments for cross-module delivery tracking - Project list/detail/create/edit workflows and app-shell navigation entry - Dashboard project widgets for active, at-risk, and overdue program visibility +- Manufacturing foundation with work orders, optional project linkage, work-order attachments, and app-shell navigation entry +- BOM-based manufacturing requirement visibility plus material issue and completion posting through inventory transactions +- Dashboard manufacturing widgets for released, active, and overdue work visibility ### Changed - The dashboard now treats Projects as a live first-class module alongside CRM, inventory, sales, and shipping -- Roadmap and project docs now treat manufacturing execution as the next active priority after the projects foundation slice +- The dashboard now treats Manufacturing as a live first-class module alongside CRM, inventory, sales, shipping, and projects +- Roadmap and project docs now treat vendor invoice/supporting-document attachments and broader vendor-side operational depth as the next active priority after the manufacturing foundation slice ## 2026-03-15 diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md index ad7ac38..c995f00 100644 --- a/INSTRUCTIONS.md +++ b/INSTRUCTIONS.md @@ -22,6 +22,7 @@ This repository implements the platform foundation milestone: - branded sales and purchasing PDFs through the shared Puppeteer document pipeline - 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, notes, attachments, and dashboard visibility +- manufacturing work orders with project linkage, material issue posting, completion posting, attachments, and dashboard visibility - Dockerized single-container deployment - Puppeteer PDF pipeline foundation @@ -57,7 +58,6 @@ This repository implements the platform foundation milestone: ## Next roadmap candidates -- manufacturing execution - vendor invoice/supporting-document attachments and broader vendor-side operational depth - sales approvals and document revision history - planning and gantt scheduling with live project/manufacturing data diff --git a/README.md b/README.md index 469ed52..a4907df 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Current foundation scope includes: - branded quote, sales-order, and purchase-order PDFs through the shared backend document pipeline - 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, notes, and attachments +- manufacturing work orders with project linkage, material issue posting, completion posting, and work-order attachments - file storage and PDF rendering ## Product Map @@ -33,20 +34,20 @@ Current completed foundation areas: - sales and purchasing foundation - shipping foundation - projects foundation +- manufacturing foundation - branding, attachments, auth/RBAC, and PDF infrastructure Planned cross-module execution areas: -- manufacturing execution - planning and gantt scheduling Near-term priorities: -1. Manufacturing execution -2. Vendor invoice/supporting-document attachments and broader vendor-facing operational depth -3. Sales approvals and revision history -4. Planning and gantt scheduling with live project/manufacturing data -5. Inventory transfers, reservations, and deeper stock controls +1. Vendor invoice/supporting-document attachments and broader vendor-facing operational depth +2. Sales approvals and revision history +3. Planning and gantt scheduling with live project/manufacturing data +4. Inventory transfers, reservations, and deeper stock controls +5. Broader audit-trail coverage and operational diagnostics Revisit / deferred items: @@ -64,6 +65,7 @@ Dashboard direction: - future additions should emphasize relevant metrics, next actions, alerts, and workflow shortcuts - richer recent-activity widgets and exception queues are a planned QOL follow-up, not a separate landing-page redesign - projects now feed dashboard widgets for active programs, overdue work, and risk +- manufacturing now feeds dashboard widgets for released work, overdue orders, and execution load - future project widgets should deepen milestones, shortages, and shipment readiness Navigation direction: @@ -92,12 +94,16 @@ Next expansion areas: ## Manufacturing Direction -Manufacturing should remain a separate execution subsystem rather than being collapsed into Projects. +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, material issue posting, completion posting, work-order attachments, and dashboard visibility. -Planned interactions: +Current interactions: - Projects: manufacturing orders may belong to a project, but projects remain the higher-level long-running record -- Inventory: manufacturing consumes components and produces stock +- Inventory: manufacturing consumes components and produces stock through real issue/receipt transactions +- Dashboard: manufacturing now contributes released/open/overdue load widgets + +Next expansion areas: + - Purchasing: shortages and buyout demand should surface from manufacturing execution - Shipping: completed manufacturing should feed shipment readiness - Planning: manufacturing orders, routings, and work centers should drive capacity and schedule views @@ -308,15 +314,16 @@ As of March 14, 2026, the latest committed domain migrations include: - sales totals and commercial fields - shipping foundation - projects foundation +- manufacturing foundation Recent roadmap-driving migrations should always be applied before validating new CRM, inventory, sales, shipping, or purchasing features in a running environment. ## UI Notes - Dark mode persistence is handled through the frontend theme provider and should remain stable across page navigation. -- The shell layout is tuned for wider desktop use than the original foundation build, and now exposes Dashboard, CRM, inventory, sales, shipping, projects, settings, and planning modules from the same app shell. +- The shell layout is tuned for wider desktop use than the original foundation build, and now exposes Dashboard, CRM, inventory, sales, shipping, projects, manufacturing, settings, and planning modules from the same app shell. - The active module screens now follow a tighter density baseline for forms, tables, and detail cards. -- The dashboard should continue evolving as a modular metric board for future purchasing, shipping, manufacturing, and audit data. +- The dashboard should continue evolving as a modular metric board for future purchasing, shipping, planning, and audit data. - The client build still emits a Vite chunk-size warning because the app has not been code-split yet. ## PDF Generation diff --git a/ROADMAP.md b/ROADMAP.md index 0139ee0..5447ea4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -42,6 +42,7 @@ MRP Codex is being built as a streamlined, modular manufacturing resource planni - Logistics attachments directly on shipment records - Projects foundation with customer, quote, sales-order, shipment, owner, due-date, notes, and attachment linkage - Project list/detail/create/edit workflows and dashboard program widgets +- Manufacturing foundation with work orders, project linkage, material issue posting, completion posting, and work-order attachments - SKU-searchable BOM component selection for inventory-scale datasets - Theme persistence fixes and denser responsive workspace layouts - Full-site density normalization pass across active CRM, inventory, settings, dashboard, and login screens @@ -58,14 +59,16 @@ MRP Codex is being built as a streamlined, modular manufacturing resource planni - The current sales/purchasing/shipping foundation still does not include approvals, revisions, vendor-side attachment handling, or deeper carrier integration - The dashboard is now live-data driven, but still needs richer KPI widgets, alerts, recent-activity queues, and exception reporting as more transactional depth is added - The new projects domain is foundational but still needs milestones, project rollups, and deeper inventory/purchasing/manufacturing tie-ins +- The new manufacturing domain is foundational but still needs routings, labor capture, work-center views, and capacity-aware planning tie-ins ## Dashboard Plan - Keep `Dashboard` as the primary landing surface for operators - Expand it by modular panels rather than redesigning it for each new feature phase - Prefer metric cards, exception queues, action shortcuts, and status summaries over static descriptive content -- Add future widgets for purchasing, shipping exceptions, inventory shortages, manufacturing load, and audit/system health +- Add future widgets for purchasing, shipping exceptions, inventory shortages, planning readiness, and audit/system health - Continue expanding the new project widgets into milestone, blockage, and shipment-readiness views instead of creating a separate landing area +- Continue expanding the new manufacturing widgets into shortage, routing, and bottleneck views instead of creating a separate landing area - Treat dashboard modules as upgradeable blocks that can be reordered or expanded without disturbing the shell ## Planned feature phases @@ -176,6 +179,14 @@ QOL subfeatures: ### Phase 6: Manufacturing execution +Foundation slice shipped: + +- Work orders tied to manufactured or assembly items, with optional project linkage +- BOM-based material requirement visibility from the work-order record +- Material issue posting that creates real inventory issue transactions +- Production completion posting that creates finished-goods receipt transactions +- Work-order list/detail/create/edit flows, attachments, and dashboard visibility + - Work orders tied to projects, sales demand, or internal build demand - Routing/work-center structure for manufacturing steps and handoffs - Material issue, consumption, completion, and WIP tracking @@ -246,7 +257,6 @@ QOL subfeatures: - Audit-trail depth is still thin outside the current record/update flows - Some generated document and workflow screens still need additional polish for dense, keyboard-efficient operational use - Dashboard cards now use live data, but richer recent-activity widgets and exception queues are still deferred -- Manufacturing execution is not yet separated cleanly from planning/scheduling in the current future-state docs and implementation ## Cross-cutting improvements @@ -259,8 +269,8 @@ QOL subfeatures: ## Near-term priority order -1. Manufacturing execution -2. Vendor invoice/supporting-document attachments and broader vendor-side operational depth -3. Sales approvals and document revision history -4. Planning and scheduling with live project/manufacturing data -5. Inventory transfers, reservations, and deeper stock controls +1. Vendor invoice/supporting-document attachments and broader vendor-side operational depth +2. Sales approvals and document revision history +3. Planning and scheduling with live project/manufacturing data +4. Inventory transfers, reservations, and deeper stock controls +5. Broader audit-trail coverage and operational diagnostics diff --git a/UNRAID.md b/UNRAID.md index b712a39..279e784 100644 --- a/UNRAID.md +++ b/UNRAID.md @@ -130,7 +130,7 @@ When you publish a new image: Because MRP Codex runs `prisma migrate deploy` during startup, committed migrations are applied automatically before the app launches. -This is especially important now that recent releases added CRM expansion, inventory transactions, sales and purchasing documents, shipping/logistics documents, the inventory `defaultPrice` field, purchasable-only purchase-order item selection, and the new projects domain. Let the container complete startup migrations before testing new screens. +This is especially important now that recent releases added CRM expansion, inventory transactions, sales and purchasing documents, shipping/logistics documents, the inventory `defaultPrice` field, purchasable-only purchase-order item selection, the new projects domain, and manufacturing work orders. Let the container complete startup migrations before testing new screens. ## Backup guidance diff --git a/client/src/components/AppShell.tsx b/client/src/components/AppShell.tsx index 744b5e0..9a69045 100644 --- a/client/src/components/AppShell.tsx +++ b/client/src/components/AppShell.tsx @@ -16,6 +16,7 @@ const links = [ { to: "/purchasing/orders", label: "Purchase Orders", icon: }, { to: "/shipping/shipments", label: "Shipments", icon: }, { to: "/projects", label: "Projects", icon: }, + { to: "/manufacturing/work-orders", label: "Manufacturing", icon: }, { to: "/planning/gantt", label: "Gantt", icon: }, ]; @@ -170,6 +171,18 @@ function ProjectsIcon() { ); } +function ManufacturingIcon() { + return ( + + + + + + + + ); +} + export function AppShell() { const { user, logout } = useAuth(); diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index 4981769..0fc80e4 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -33,6 +33,16 @@ import type { WarehouseLocationOptionDto, WarehouseSummaryDto, } from "@mrp/shared/dist/inventory/types.js"; +import type { + ManufacturingItemOptionDto, + ManufacturingProjectOptionDto, + WorkOrderCompletionInput, + WorkOrderDetailDto, + WorkOrderInput, + WorkOrderMaterialIssueInput, + WorkOrderStatus, + WorkOrderSummaryDto, +} from "@mrp/shared"; import type { ProjectCustomerOptionDto, ProjectDetailDto, @@ -444,6 +454,54 @@ export const api = { token ); }, + getManufacturingItemOptions(token: string) { + return request("/api/v1/manufacturing/items/options", undefined, token); + }, + getManufacturingProjectOptions(token: string) { + return request("/api/v1/manufacturing/projects/options", undefined, token); + }, + getWorkOrders(token: string, filters?: { q?: string; status?: WorkOrderStatus; projectId?: string; itemId?: string }) { + return request( + `/api/v1/manufacturing/work-orders${buildQueryString({ + q: filters?.q, + status: filters?.status, + projectId: filters?.projectId, + itemId: filters?.itemId, + })}`, + undefined, + token + ); + }, + getWorkOrder(token: string, workOrderId: string) { + return request(`/api/v1/manufacturing/work-orders/${workOrderId}`, undefined, token); + }, + createWorkOrder(token: string, payload: WorkOrderInput) { + return request("/api/v1/manufacturing/work-orders", { method: "POST", body: JSON.stringify(payload) }, token); + }, + updateWorkOrder(token: string, workOrderId: string, payload: WorkOrderInput) { + return request(`/api/v1/manufacturing/work-orders/${workOrderId}`, { method: "PUT", body: JSON.stringify(payload) }, token); + }, + updateWorkOrderStatus(token: string, workOrderId: string, status: WorkOrderStatus) { + return request( + `/api/v1/manufacturing/work-orders/${workOrderId}/status`, + { method: "PATCH", body: JSON.stringify({ status }) }, + token + ); + }, + issueWorkOrderMaterial(token: string, workOrderId: string, payload: WorkOrderMaterialIssueInput) { + return request( + `/api/v1/manufacturing/work-orders/${workOrderId}/issues`, + { method: "POST", body: JSON.stringify(payload) }, + token + ); + }, + recordWorkOrderCompletion(token: string, workOrderId: string, payload: WorkOrderCompletionInput) { + return request( + `/api/v1/manufacturing/work-orders/${workOrderId}/completions`, + { method: "POST", body: JSON.stringify(payload) }, + token + ); + }, getGanttDemo(token: string) { return request<{ tasks: GanttTaskDto[]; links: GanttLinkDto[] }>("/api/v1/gantt/demo", undefined, token); }, diff --git a/client/src/main.tsx b/client/src/main.tsx index 5654193..4e9976d 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -18,6 +18,9 @@ import { GanttPage } from "./modules/gantt/GanttPage"; import { InventoryDetailPage } from "./modules/inventory/InventoryDetailPage"; import { InventoryFormPage } from "./modules/inventory/InventoryFormPage"; import { InventoryItemsPage } from "./modules/inventory/InventoryItemsPage"; +import { ManufacturingPage } from "./modules/manufacturing/ManufacturingPage"; +import { WorkOrderDetailPage } from "./modules/manufacturing/WorkOrderDetailPage"; +import { WorkOrderFormPage } from "./modules/manufacturing/WorkOrderFormPage"; import { PurchaseDetailPage } from "./modules/purchasing/PurchaseDetailPage"; import { PurchaseFormPage } from "./modules/purchasing/PurchaseFormPage"; import { PurchaseListPage } from "./modules/purchasing/PurchaseListPage"; @@ -76,6 +79,13 @@ const router = createBrowserRouter([ { path: "/projects/:projectId", element: }, ], }, + { + element: , + children: [ + { path: "/manufacturing/work-orders", element: }, + { path: "/manufacturing/work-orders/:workOrderId", element: }, + ], + }, { element: , children: [ @@ -115,6 +125,13 @@ const router = createBrowserRouter([ { path: "/projects/:projectId/edit", element: }, ], }, + { + element: , + children: [ + { path: "/manufacturing/work-orders/new", element: }, + { path: "/manufacturing/work-orders/:workOrderId/edit", element: }, + ], + }, { element: , children: [ diff --git a/client/src/modules/dashboard/DashboardPage.tsx b/client/src/modules/dashboard/DashboardPage.tsx index bca0211..beece4b 100644 --- a/client/src/modules/dashboard/DashboardPage.tsx +++ b/client/src/modules/dashboard/DashboardPage.tsx @@ -10,6 +10,7 @@ interface DashboardSnapshot { vendors: Awaited> | null; items: Awaited> | null; warehouses: Awaited> | null; + workOrders: Awaited> | null; quotes: Awaited> | null; orders: Awaited> | null; shipments: Awaited> | null; @@ -51,6 +52,7 @@ export function DashboardPage() { const [snapshot, setSnapshot] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + const canWriteManufacturing = hasPermission(user?.permissions, permissions.manufacturingWrite); const canWriteProjects = hasPermission(user?.permissions, permissions.projectsWrite); useEffect(() => { @@ -67,6 +69,7 @@ export function DashboardPage() { const canReadCrm = hasPermission(user.permissions, permissions.crmRead); const canReadInventory = hasPermission(user.permissions, permissions.inventoryRead); + const canReadManufacturing = hasPermission(user.permissions, permissions.manufacturingRead); const canReadSales = hasPermission(user.permissions, permissions.salesRead); const canReadShipping = hasPermission(user.permissions, permissions.shippingRead); const canReadProjects = hasPermission(user.permissions, permissions.projectsRead); @@ -77,6 +80,7 @@ export function DashboardPage() { canReadCrm ? api.getVendors(authToken) : Promise.resolve(null), canReadInventory ? api.getInventoryItems(authToken) : Promise.resolve(null), canReadInventory ? api.getWarehouses(authToken) : Promise.resolve(null), + canReadManufacturing ? api.getWorkOrders(authToken) : Promise.resolve(null), canReadSales ? api.getQuotes(authToken) : Promise.resolve(null), canReadSales ? api.getSalesOrders(authToken) : Promise.resolve(null), canReadShipping ? api.getShipments(authToken) : Promise.resolve(null), @@ -98,10 +102,11 @@ export function DashboardPage() { vendors: results[1].status === "fulfilled" ? results[1].value : null, items: results[2].status === "fulfilled" ? results[2].value : null, warehouses: results[3].status === "fulfilled" ? results[3].value : null, - quotes: results[4].status === "fulfilled" ? results[4].value : null, - orders: results[5].status === "fulfilled" ? results[5].value : null, - shipments: results[6].status === "fulfilled" ? results[6].value : null, - projects: results[7].status === "fulfilled" ? results[7].value : null, + workOrders: results[4].status === "fulfilled" ? results[4].value : null, + quotes: results[5].status === "fulfilled" ? results[5].value : null, + orders: results[6].status === "fulfilled" ? results[6].value : null, + shipments: results[7].status === "fulfilled" ? results[7].value : null, + projects: results[8].status === "fulfilled" ? results[8].value : null, refreshedAt: new Date().toISOString(), }); setIsLoading(false); @@ -126,6 +131,7 @@ export function DashboardPage() { const vendors = snapshot?.vendors ?? []; const items = snapshot?.items ?? []; const warehouses = snapshot?.warehouses ?? []; + const workOrders = snapshot?.workOrders ?? []; const quotes = snapshot?.quotes ?? []; const orders = snapshot?.orders ?? []; const shipments = snapshot?.shipments ?? []; @@ -134,6 +140,7 @@ export function DashboardPage() { const accessibleModules = [ snapshot?.customers !== null || snapshot?.vendors !== null, snapshot?.items !== null || snapshot?.warehouses !== null, + snapshot?.workOrders !== null, snapshot?.quotes !== null || snapshot?.orders !== null, snapshot?.shipments !== null, snapshot?.projects !== null, @@ -152,6 +159,11 @@ export function DashboardPage() { const warehouseCount = warehouses.length; const locationCount = sumNumber(warehouses.map((warehouse) => warehouse.locationCount)); + const workOrderCount = workOrders.length; + const activeWorkOrderCount = workOrders.filter((workOrder) => workOrder.status === "RELEASED" || workOrder.status === "IN_PROGRESS" || workOrder.status === "ON_HOLD").length; + const releasedWorkOrderCount = workOrders.filter((workOrder) => workOrder.status === "RELEASED").length; + const overdueWorkOrderCount = workOrders.filter((workOrder) => workOrder.dueDate && workOrder.status !== "COMPLETE" && workOrder.status !== "CANCELLED" && new Date(workOrder.dueDate).getTime() < Date.now()).length; + const quoteCount = quotes.length; const orderCount = orders.length; const draftQuoteCount = quotes.filter((quote) => quote.status === "DRAFT").length; @@ -180,6 +192,7 @@ export function DashboardPage() { ...vendors.map((vendor) => vendor.updatedAt), ...items.map((item) => item.updatedAt), ...warehouses.map((warehouse) => warehouse.updatedAt), + ...workOrders.map((workOrder) => workOrder.updatedAt), ...quotes.map((quote) => quote.updatedAt), ...orders.map((order) => order.updatedAt), ...shipments.map((shipment) => shipment.updatedAt), @@ -207,6 +220,15 @@ export function DashboardPage() { : "Inventory metrics are permission-gated.", tone: "border-sky-400/30 bg-sky-500/12 text-sky-700 dark:text-sky-300", }, + { + label: "Manufacturing Load", + value: snapshot?.workOrders !== null ? `${activeWorkOrderCount}` : "No access", + detail: + snapshot?.workOrders !== null + ? `${releasedWorkOrderCount} released and ${overdueWorkOrderCount} overdue` + : "Manufacturing metrics are permission-gated.", + tone: "border-indigo-400/30 bg-indigo-500/12 text-indigo-700 dark:text-indigo-300", + }, { label: "Commercial Value", value: snapshot?.quotes !== null || snapshot?.orders !== null ? formatCurrency(quoteValue + orderValue) : "No access", @@ -271,6 +293,23 @@ export function DashboardPage() { { label: "Open warehouses", to: "/inventory/warehouses" }, ], }, + { + title: "Manufacturing", + eyebrow: "Execution Load", + summary: + snapshot?.workOrders !== null + ? "Work orders, released load, and overdue build pressure are now visible from the dashboard." + : "Manufacturing read permission is required to surface work-order metrics here.", + metrics: [ + { label: "Open work", value: snapshot?.workOrders !== null ? `${activeWorkOrderCount}` : "No access" }, + { label: "Released", value: snapshot?.workOrders !== null ? `${releasedWorkOrderCount}` : "No access" }, + { label: "Overdue", value: snapshot?.workOrders !== null ? `${overdueWorkOrderCount}` : "No access" }, + ], + links: [ + { label: "Open work orders", to: "/manufacturing/work-orders" }, + ...(canWriteManufacturing ? [{ label: "New work order", to: "/manufacturing/work-orders/new" }] : []), + ], + }, { title: "Sales", eyebrow: "Revenue Flow", @@ -327,7 +366,6 @@ export function DashboardPage() { const futureModules = [ "Vendor invoice attachments and supplier exception queues", "Stock transfers, allocations, and cycle counts", - "Manufacturing work orders, routings, and bottleneck metrics", "Planning timeline, milestones, and dependency views", "Audit trails, diagnostics, and system health checks", ]; @@ -350,8 +388,8 @@ export function DashboardPage() {

- This landing page now reads directly from live CRM, inventory, sales, shipping, and project data. It is intentionally modular so future - purchasing, manufacturing, and audit slices can slot into the same command surface without a redesign. + This landing page now reads directly from live CRM, inventory, manufacturing, sales, shipping, and project data. It is intentionally + modular so future purchasing, planning, and audit slices can slot into the same command surface without a redesign.

@@ -380,6 +418,9 @@ export function DashboardPage() { Open projects + + Open manufacturing +
{error ?
{error}
: null}
@@ -396,7 +437,7 @@ export function DashboardPage() { -
+
{metricCards.map((card) => (

{card.label}

@@ -408,7 +449,7 @@ export function DashboardPage() {
))}
-
+
{modulePanels.map((panel) => (

{panel.eyebrow}

@@ -432,7 +473,7 @@ export function DashboardPage() {
))}
-
+

Inventory Watch

Master data pressure points

@@ -469,6 +510,24 @@ export function DashboardPage() {
+
+

Manufacturing Watch

+

Build execution and due-date pressure

+
+
+ Total work orders + {snapshot?.workOrders !== null ? `${workOrderCount}` : "No access"} +
+
+ Active queue + {snapshot?.workOrders !== null ? `${activeWorkOrderCount}` : "No access"} +
+
+ Overdue + {snapshot?.workOrders !== null ? `${overdueWorkOrderCount}` : "No access"} +
+
+

Project Watch

Program status and delivery pressure

diff --git a/client/src/modules/manufacturing/ManufacturingPage.tsx b/client/src/modules/manufacturing/ManufacturingPage.tsx new file mode 100644 index 0000000..4492e57 --- /dev/null +++ b/client/src/modules/manufacturing/ManufacturingPage.tsx @@ -0,0 +1,5 @@ +import { WorkOrderListPage } from "./WorkOrderListPage"; + +export function ManufacturingPage() { + return ; +} diff --git a/client/src/modules/manufacturing/WorkOrderDetailPage.tsx b/client/src/modules/manufacturing/WorkOrderDetailPage.tsx new file mode 100644 index 0000000..d3a81f9 --- /dev/null +++ b/client/src/modules/manufacturing/WorkOrderDetailPage.tsx @@ -0,0 +1,338 @@ +import { permissions } from "@mrp/shared"; +import type { WorkOrderCompletionInput, WorkOrderDetailDto, WorkOrderMaterialIssueInput, 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 { 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 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), + }); + 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 handleStatusChange(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."); + } catch (error: unknown) { + const message = error instanceof ApiError ? error.message : "Unable to update work-order status."; + setStatus(message); + } finally { + setIsUpdatingStatus(false); + } + } + + async function handleIssueSubmit(event: React.FormEvent) { + event.preventDefault(); + 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."); + } catch (error: unknown) { + const message = error instanceof ApiError ? error.message : "Unable to post material issue."; + setStatus(message); + } finally { + setIsPostingIssue(false); + } + } + + async function handleCompletionSubmit(event: React.FormEvent) { + event.preventDefault(); + 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."); + } catch (error: unknown) { + const message = error instanceof ApiError ? error.message : "Unable to post completion."; + setStatus(message); + } finally { + setIsPostingCompletion(false); + } + } + + if (!workOrder) { + return
{status}
; + } + + return ( +
+
+
+
+

Work Order

+

{workOrder.workOrderNumber}

+

{workOrder.itemSku} - {workOrder.itemName}

+
+
+
+ Back to work orders + {workOrder.projectId ? Open project : 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"}
+

Due Date

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

Execution Context

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

Work Instructions

+

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

+
+
+ {canManage ? ( +
+
+

Material Issue

+
+ +
+ + + +
+