From e2254d020e54c6e8a0df6185aafa6a11a6a29d02 Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 15 Mar 2026 11:30:10 -0500 Subject: [PATCH] manufacturing stabilization --- AGENTS.md | 11 +- CHANGELOG.md | 8 +- INSTRUCTIONS.md | 3 +- README.md | 13 +- ROADMAP.md | 20 +- client/src/lib/api.ts | 3 +- client/src/modules/crm/CrmDetailPage.tsx | 45 +++ .../src/modules/dashboard/DashboardPage.tsx | 80 ++++- .../manufacturing/WorkOrderFormPage.tsx | 17 +- .../modules/projects/ProjectDetailPage.tsx | 34 +++ .../src/modules/projects/ProjectFormPage.tsx | 286 ++++++++++++++++-- .../modules/purchasing/PurchaseDetailPage.tsx | 11 +- .../modules/purchasing/PurchaseFormPage.tsx | 17 +- server/src/modules/purchasing/router.ts | 1 + server/src/modules/purchasing/service.ts | 3 +- 15 files changed, 492 insertions(+), 60 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d9a2dcb..ac62b18 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,7 @@ MRP Codex is a modular Manufacturing Resource Planning platform intended to be a - CRM customers/vendors, hierarchy, contacts, lifecycle metadata, and attachments - inventory items, BOMs, warehouses, locations, transactions, item attachments, and item pricing - sales quotes, sales orders, and purchase orders +- purchase-order supporting documents and vendor-side purchasing visibility - 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 @@ -116,11 +117,11 @@ If implementation changes invalidate those docs, update them in the same change Near-term priorities are: -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 +1. Sales approvals and document revision history +2. Planning and gantt scheduling with live project/manufacturing data +3. Inventory transfers, reservations, and deeper stock controls +4. Broader audit-trail coverage and operational diagnostics +5. Code-splitting and bundle-size reduction 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 2ca39d3..63a314a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,12 +13,18 @@ This file is the running release and change log for MRP Codex. Keep it updated w - 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 +- Purchase-order supporting documents for vendor invoices, acknowledgements, certifications, and procurement backup files +- Vendor-detail purchasing visibility with recent purchase-order activity and PO launch shortcuts ### Changed - The dashboard now treats Projects as a live first-class module alongside CRM, inventory, sales, and shipping - 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 +- Project editing now uses searchable pickers for customer, owner, quote, sales-order, and shipment linkage instead of static operational dropdowns +- Project detail now surfaces linked work orders and can launch pre-seeded manufacturing records +- Purchase-order detail now links back to the vendor CRM record and supports direct supporting-document management on the PO itself +- Vendor CRM detail now exposes purchasing activity and can launch pre-seeded purchase orders +- Roadmap and project docs now treat sales approvals and document revision history as the next active priority after the vendor-document and stabilization pass ## 2026-03-15 diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md index c995f00..c8195ce 100644 --- a/INSTRUCTIONS.md +++ b/INSTRUCTIONS.md @@ -20,6 +20,7 @@ This repository implements the platform foundation milestone: - purchase orders restricted to inventory items flagged as purchasable - purchase receiving foundation with inventory posting and receipt history - branded sales and purchasing PDFs through the shared Puppeteer document pipeline +- purchase-order supporting documents and vendor-side purchasing visibility - 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 @@ -58,8 +59,8 @@ This repository implements the platform foundation milestone: ## Next roadmap candidates -- 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 - inventory transfers, reservations, and deeper stock controls - broader audit and operations maturity +- code-splitting and bundle-size reduction diff --git a/README.md b/README.md index a4907df..1a99d0d 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Current foundation scope includes: - purchase orders with searchable vendor and SKU entry, restricted to purchasable inventory items - purchase receiving with warehouse/location posting and receipt history against purchase orders - branded quote, sales-order, and purchase-order PDFs through the shared backend document pipeline +- 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, notes, and attachments - manufacturing work orders with project linkage, material issue posting, completion posting, and work-order attachments @@ -43,11 +44,11 @@ Planned cross-module execution areas: Near-term priorities: -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 +1. Sales approvals and revision history +2. Planning and gantt scheduling with live project/manufacturing data +3. Inventory transfers, reservations, and deeper stock controls +4. Broader audit-trail coverage and operational diagnostics +5. Code-splitting and bundle-size reduction Revisit / deferred items: @@ -246,8 +247,8 @@ The current purchasing foundation supports: QOL direction: -- vendor invoice/supporting-document attachments - richer dashboard widgets for vendor queues and inbound material exceptions +- vendor-side exception tracking around acknowledgements, invoice matching, and receipt discrepancies This module introduces `purchasing.read` and `purchasing.write` permissions. After updating the code, restart the server against the migrated database so bootstrap can upsert the new permissions onto the default administrator role. diff --git a/ROADMAP.md b/ROADMAP.md index 5447ea4..4b464f7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -43,6 +43,8 @@ MRP Codex is being built as a streamlined, modular manufacturing resource planni - 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 +- Vendor invoice/supporting-document attachments directly on purchase orders +- Vendor-detail purchasing visibility with recent purchase-order activity - 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 @@ -56,7 +58,7 @@ MRP Codex is being built as a streamlined, modular manufacturing resource planni - Prisma migration execution is committed and documented, but local Windows Node 24 schema-engine behavior remains inconsistent; use Node 22 or Docker for migration execution - The frontend bundle is functional but should be code-split later, especially around the gantt module - CRM reporting is now functional, but broader account-role depth and downstream document rollups can still evolve later -- The current sales/purchasing/shipping foundation still does not include approvals, revisions, vendor-side attachment handling, or deeper carrier integration +- The current sales/purchasing/shipping foundation still does not include approvals, revisions, vendor exception 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 @@ -115,6 +117,11 @@ QOL subfeatures: - Branded PDF templates rendered through Puppeteer - Attachments for vendor invoices and supporting documents +Foundation slice shipped: + +- Purchase-order supporting documents through the shared attachment pipeline +- Vendor-detail purchasing visibility for recent purchase-order activity + QOL subfeatures: - Line duplication, drag ordering, and keyboard-first line editing @@ -251,7 +258,6 @@ QOL subfeatures: - Local Windows Prisma migration reliability still needs a cleaner documented workflow or tooling wrapper - Frontend bundle splitting is still deferred; the Vite chunk-size warning remains - Sales approvals and document revision history were planned but not yet built -- Vendor invoice/supporting-document attachments still need to be added - Inventory transactions exist, but transfers, reservations, and more advanced stock controls still need follow-up - CRM document rollups and broader account-role depth were deferred until more downstream modules exist - Audit-trail depth is still thin outside the current record/update flows @@ -269,8 +275,8 @@ QOL subfeatures: ## Near-term priority order -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 +1. Sales approvals and document revision history +2. Planning and scheduling with live project/manufacturing data +3. Inventory transfers, reservations, and deeper stock controls +4. Broader audit-trail coverage and operational diagnostics +5. Code-splitting and bundle-size reduction diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index 0fc80e4..0a7fb54 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -566,11 +566,12 @@ export const api = { token ); }, - getPurchaseOrders(token: string, filters?: { q?: string; status?: PurchaseOrderStatus }) { + getPurchaseOrders(token: string, filters?: { q?: string; status?: PurchaseOrderStatus; vendorId?: string }) { return request( `/api/v1/purchasing/orders${buildQueryString({ q: filters?.q, status: filters?.status, + vendorId: filters?.vendorId, })}`, undefined, token diff --git a/client/src/modules/crm/CrmDetailPage.tsx b/client/src/modules/crm/CrmDetailPage.tsx index 14a762f..8ba0fee 100644 --- a/client/src/modules/crm/CrmDetailPage.tsx +++ b/client/src/modules/crm/CrmDetailPage.tsx @@ -1,5 +1,6 @@ import { permissions } from "@mrp/shared"; import type { CrmContactDto, CrmContactEntryInput, CrmRecordDetailDto } from "@mrp/shared/dist/crm/types.js"; +import type { PurchaseOrderSummaryDto } from "@mrp/shared"; import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; @@ -23,6 +24,7 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) { const recordId = entity === "customer" ? customerId : vendorId; const config = crmConfigs[entity]; const [record, setRecord] = useState(null); + const [relatedPurchaseOrders, setRelatedPurchaseOrders] = useState([]); const [status, setStatus] = useState(`Loading ${config.singularLabel.toLowerCase()}...`); const [contactEntryForm, setContactEntryForm] = useState(emptyCrmContactEntryInput); const [contactEntryStatus, setContactEntryStatus] = useState("Add a timeline entry for this account."); @@ -42,7 +44,13 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) { setRecord(nextRecord); setStatus(`${config.singularLabel} record loaded.`); setContactEntryStatus("Add a timeline entry for this account."); + if (entity === "vendor") { + return api.getPurchaseOrders(token, { vendorId: nextRecord.id }); + } + + return []; }) + .then((purchaseOrders) => setRelatedPurchaseOrders(purchaseOrders)) .catch((error: unknown) => { const message = error instanceof ApiError ? error.message : `Unable to load ${config.singularLabel.toLowerCase()}.`; setStatus(message); @@ -250,6 +258,43 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) { ) : null} + {entity === "vendor" ? ( +
+
+
+

Purchasing Activity

+

Recent purchase orders

+
+
+ {canManage ? ( + + New purchase order + + ) : null} + + Open purchasing + +
+
+ {relatedPurchaseOrders.length === 0 ? ( +
No purchase orders exist for this vendor yet.
+ ) : ( +
+ {relatedPurchaseOrders.slice(0, 8).map((order) => ( + +
+
+
{order.documentNumber}
+
{new Date(order.issueDate).toLocaleDateString()} · {order.lineCount} lines
+
+
${order.total.toFixed(2)}
+
+ + ))} +
+ )} +
+ ) : null} > | null; items: Awaited> | null; warehouses: Awaited> | null; + purchaseOrders: Awaited> | null; workOrders: Awaited> | null; quotes: Awaited> | null; orders: Awaited> | null; @@ -69,6 +70,7 @@ export function DashboardPage() { const canReadCrm = hasPermission(user.permissions, permissions.crmRead); const canReadInventory = hasPermission(user.permissions, permissions.inventoryRead); + const canReadPurchasing = hasPermission(user.permissions, permissions.purchasingRead); const canReadManufacturing = hasPermission(user.permissions, permissions.manufacturingRead); const canReadSales = hasPermission(user.permissions, permissions.salesRead); const canReadShipping = hasPermission(user.permissions, permissions.shippingRead); @@ -80,6 +82,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), + canReadPurchasing ? api.getPurchaseOrders(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), @@ -102,11 +105,12 @@ 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, - 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, + purchaseOrders: results[4].status === "fulfilled" ? results[4].value : null, + workOrders: results[5].status === "fulfilled" ? results[5].value : null, + quotes: results[6].status === "fulfilled" ? results[6].value : null, + orders: results[7].status === "fulfilled" ? results[7].value : null, + shipments: results[8].status === "fulfilled" ? results[8].value : null, + projects: results[9].status === "fulfilled" ? results[9].value : null, refreshedAt: new Date().toISOString(), }); setIsLoading(false); @@ -131,6 +135,7 @@ export function DashboardPage() { const vendors = snapshot?.vendors ?? []; const items = snapshot?.items ?? []; const warehouses = snapshot?.warehouses ?? []; + const purchaseOrders = snapshot?.purchaseOrders ?? []; const workOrders = snapshot?.workOrders ?? []; const quotes = snapshot?.quotes ?? []; const orders = snapshot?.orders ?? []; @@ -140,6 +145,7 @@ export function DashboardPage() { const accessibleModules = [ snapshot?.customers !== null || snapshot?.vendors !== null, snapshot?.items !== null || snapshot?.warehouses !== null, + snapshot?.purchaseOrders !== null, snapshot?.workOrders !== null, snapshot?.quotes !== null || snapshot?.orders !== null, snapshot?.shipments !== null, @@ -159,6 +165,11 @@ export function DashboardPage() { const warehouseCount = warehouses.length; const locationCount = sumNumber(warehouses.map((warehouse) => warehouse.locationCount)); + const purchaseOrderCount = purchaseOrders.length; + const openPurchaseOrderCount = purchaseOrders.filter((order) => order.status !== "CLOSED").length; + const issuedPurchaseOrderCount = purchaseOrders.filter((order) => order.status === "ISSUED" || order.status === "APPROVED").length; + const purchaseOrderValue = sumNumber(purchaseOrders.map((order) => order.total)); + 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; @@ -192,6 +203,7 @@ export function DashboardPage() { ...vendors.map((vendor) => vendor.updatedAt), ...items.map((item) => item.updatedAt), ...warehouses.map((warehouse) => warehouse.updatedAt), + ...purchaseOrders.map((order) => order.updatedAt), ...workOrders.map((workOrder) => workOrder.updatedAt), ...quotes.map((quote) => quote.updatedAt), ...orders.map((order) => order.updatedAt), @@ -220,6 +232,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: "Purchasing Queue", + value: snapshot?.purchaseOrders !== null ? `${openPurchaseOrderCount}` : "No access", + detail: + snapshot?.purchaseOrders !== null + ? `${issuedPurchaseOrderCount} issued/approved and ${formatCurrency(purchaseOrderValue)} committed` + : "Purchasing metrics are permission-gated.", + tone: "border-teal-400/30 bg-teal-500/12 text-teal-700 dark:text-teal-300", + }, { label: "Manufacturing Load", value: snapshot?.workOrders !== null ? `${activeWorkOrderCount}` : "No access", @@ -293,6 +314,22 @@ export function DashboardPage() { { label: "Open warehouses", to: "/inventory/warehouses" }, ], }, + { + title: "Purchasing", + eyebrow: "Inbound Supply", + summary: + snapshot?.purchaseOrders !== null + ? "Purchase orders, open commitments, and current inbound procurement load are now visible from the dashboard." + : "Purchasing read permission is required to surface procurement metrics here.", + metrics: [ + { label: "Open POs", value: snapshot?.purchaseOrders !== null ? `${openPurchaseOrderCount}` : "No access" }, + { label: "Issued", value: snapshot?.purchaseOrders !== null ? `${issuedPurchaseOrderCount}` : "No access" }, + { label: "Committed", value: snapshot?.purchaseOrders !== null ? formatCurrency(purchaseOrderValue) : "No access" }, + ], + links: [ + { label: "Open purchase orders", to: "/purchasing/orders" }, + ], + }, { title: "Manufacturing", eyebrow: "Execution Load", @@ -364,9 +401,9 @@ export function DashboardPage() { ]; const futureModules = [ - "Vendor invoice attachments and supplier exception queues", "Stock transfers, allocations, and cycle counts", "Planning timeline, milestones, and dependency views", + "Sales approvals, revisions, and change history", "Audit trails, diagnostics, and system health checks", ]; @@ -388,8 +425,8 @@ export function DashboardPage() {

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

@@ -415,6 +452,9 @@ export function DashboardPage() { Open inventory + + Open purchasing + Open projects @@ -437,7 +477,7 @@ export function DashboardPage() {
-
+
{metricCards.map((card) => (

{card.label}

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

{panel.eyebrow}

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

Inventory Watch

Master data pressure points

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

Purchasing Watch

+

Inbound supply and commitment load

+
+
+ Total purchase orders + {snapshot?.purchaseOrders !== null ? `${purchaseOrderCount}` : "No access"} +
+
+ Open queue + {snapshot?.purchaseOrders !== null ? `${openPurchaseOrderCount}` : "No access"} +
+
+ Committed value + {snapshot?.purchaseOrders !== null ? formatCurrency(purchaseOrderValue) : "No access"} +
+
+

Manufacturing Watch

Build execution and due-date pressure

diff --git a/client/src/modules/manufacturing/WorkOrderFormPage.tsx b/client/src/modules/manufacturing/WorkOrderFormPage.tsx index cc2d3e7..0602822 100644 --- a/client/src/modules/manufacturing/WorkOrderFormPage.tsx +++ b/client/src/modules/manufacturing/WorkOrderFormPage.tsx @@ -5,7 +5,7 @@ import type { } from "@mrp/shared"; import type { WarehouseLocationOptionDto } from "@mrp/shared/dist/inventory/types.js"; import { useEffect, useMemo, useState } from "react"; -import { Link, useNavigate, useParams } from "react-router-dom"; +import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom"; import { useAuth } from "../../auth/AuthProvider"; import { api, ApiError } from "../../lib/api"; @@ -15,6 +15,8 @@ export function WorkOrderFormPage({ mode }: { mode: "create" | "edit" }) { const { token } = useAuth(); const navigate = useNavigate(); const { workOrderId } = useParams(); + const [searchParams] = useSearchParams(); + const seededProjectId = searchParams.get("projectId"); const [form, setForm] = useState(emptyWorkOrderInput); const [itemOptions, setItemOptions] = useState([]); const [projectOptions, setProjectOptions] = useState([]); @@ -32,9 +34,18 @@ export function WorkOrderFormPage({ mode }: { mode: "create" | "edit" }) { } api.getManufacturingItemOptions(token).then(setItemOptions).catch(() => setItemOptions([])); - api.getManufacturingProjectOptions(token).then(setProjectOptions).catch(() => setProjectOptions([])); + api.getManufacturingProjectOptions(token).then((options) => { + setProjectOptions(options); + if (mode === "create" && seededProjectId) { + const seededProject = options.find((option) => option.id === seededProjectId); + if (seededProject) { + setForm((current) => ({ ...current, projectId: seededProject.id })); + setProjectSearchTerm(`${seededProject.projectNumber} - ${seededProject.name}`); + } + } + }).catch(() => setProjectOptions([])); api.getWarehouseLocationOptions(token).then(setLocationOptions).catch(() => setLocationOptions([])); - }, [token]); + }, [mode, seededProjectId, token]); useEffect(() => { if (!token || mode !== "edit" || !workOrderId) { diff --git a/client/src/modules/projects/ProjectDetailPage.tsx b/client/src/modules/projects/ProjectDetailPage.tsx index c00b2ad..5ce2e68 100644 --- a/client/src/modules/projects/ProjectDetailPage.tsx +++ b/client/src/modules/projects/ProjectDetailPage.tsx @@ -1,5 +1,6 @@ import { permissions } from "@mrp/shared"; import type { ProjectDetailDto } from "@mrp/shared/dist/projects/types.js"; +import type { WorkOrderSummaryDto } from "@mrp/shared"; import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; @@ -13,6 +14,7 @@ export function ProjectDetailPage() { const { token, user } = useAuth(); const { projectId } = useParams(); const [project, setProject] = useState(null); + const [workOrders, setWorkOrders] = useState([]); const [status, setStatus] = useState("Loading project..."); const canManage = user?.permissions.includes(permissions.projectsWrite) ?? false; @@ -26,7 +28,9 @@ export function ProjectDetailPage() { .then((nextProject) => { setProject(nextProject); setStatus("Project loaded."); + return api.getWorkOrders(token, { projectId: nextProject.id }); }) + .then((nextWorkOrders) => setWorkOrders(nextWorkOrders)) .catch((error: unknown) => { const message = error instanceof ApiError ? error.message : "Unable to load project."; setStatus(message); @@ -93,6 +97,36 @@ export function ProjectDetailPage() {
+
+
+
+

Manufacturing Links

+

Work orders already linked to this project.

+
+ {canManage ? ( + + New work order + + ) : null} +
+ {workOrders.length === 0 ? ( +
No work orders are linked to this project yet.
+ ) : ( +
+ {workOrders.map((workOrder) => ( + +
+
+
{workOrder.workOrderNumber}
+
{workOrder.itemSku} · {workOrder.completedQuantity}/{workOrder.quantity} complete
+
+
{workOrder.status.replace("_", " ")}
+
+ + ))} +
+ )} +
([]); const [orderOptions, setOrderOptions] = useState([]); const [shipmentOptions, setShipmentOptions] = useState([]); + const [customerSearchTerm, setCustomerSearchTerm] = useState(""); + const [ownerSearchTerm, setOwnerSearchTerm] = useState(""); + const [quoteSearchTerm, setQuoteSearchTerm] = useState(""); + const [orderSearchTerm, setOrderSearchTerm] = useState(""); + const [shipmentSearchTerm, setShipmentSearchTerm] = useState(""); + const [customerPickerOpen, setCustomerPickerOpen] = useState(false); + const [ownerPickerOpen, setOwnerPickerOpen] = useState(false); + const [quotePickerOpen, setQuotePickerOpen] = useState(false); + const [orderPickerOpen, setOrderPickerOpen] = useState(false); + const [shipmentPickerOpen, setShipmentPickerOpen] = useState(false); const [status, setStatus] = useState(mode === "create" ? "Create a new project." : "Loading project..."); const [isSaving, setIsSaving] = useState(false); @@ -66,6 +76,11 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) { dueDate: project.dueDate, notes: project.notes, }); + setCustomerSearchTerm(project.customerName); + setOwnerSearchTerm(project.ownerName ?? ""); + setQuoteSearchTerm(project.salesQuoteNumber ?? ""); + setOrderSearchTerm(project.salesOrderNumber ?? ""); + setShipmentSearchTerm(project.shipmentNumber ?? ""); setStatus("Project loaded."); }) .catch((error: unknown) => { @@ -88,6 +103,20 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) { })); } + function restoreSearchTerms() { + const selectedCustomer = customerOptions.find((customer) => customer.id === form.customerId); + const selectedOwner = ownerOptions.find((owner) => owner.id === form.ownerId); + const selectedQuote = quoteOptions.find((quote) => quote.id === form.salesQuoteId); + const selectedOrder = orderOptions.find((order) => order.id === form.salesOrderId); + const selectedShipment = shipmentOptions.find((shipment) => shipment.id === form.shipmentId); + + setCustomerSearchTerm(selectedCustomer?.name ?? ""); + setOwnerSearchTerm(selectedOwner?.fullName ?? ""); + setQuoteSearchTerm(selectedQuote?.documentNumber ?? ""); + setOrderSearchTerm(selectedOrder?.documentNumber ?? ""); + setShipmentSearchTerm(selectedShipment?.shipmentNumber ?? ""); + } + async function handleSubmit(event: React.FormEvent) { event.preventDefault(); if (!token) { @@ -128,10 +157,47 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
@@ -149,10 +215,55 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
+
{status}
); diff --git a/client/src/modules/purchasing/PurchaseFormPage.tsx b/client/src/modules/purchasing/PurchaseFormPage.tsx index 600a1b5..3733e10 100644 --- a/client/src/modules/purchasing/PurchaseFormPage.tsx +++ b/client/src/modules/purchasing/PurchaseFormPage.tsx @@ -1,6 +1,6 @@ import type { InventoryItemOptionDto, PurchaseLineInput, PurchaseOrderInput, PurchaseVendorOptionDto } from "@mrp/shared"; import { useEffect, useState } from "react"; -import { Link, useNavigate, useParams } from "react-router-dom"; +import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom"; import { useAuth } from "../../auth/AuthProvider"; import { api, ApiError } from "../../lib/api"; @@ -11,6 +11,8 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) { const { token } = useAuth(); const navigate = useNavigate(); const { orderId } = useParams(); + const [searchParams] = useSearchParams(); + const seededVendorId = searchParams.get("vendorId"); const [form, setForm] = useState(emptyPurchaseOrderInput); const [status, setStatus] = useState(mode === "create" ? "Create a new purchase order." : "Loading purchase order..."); const [vendors, setVendors] = useState([]); @@ -30,9 +32,18 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) { return; } - api.getPurchaseVendors(token).then(setVendors).catch(() => setVendors([])); + api.getPurchaseVendors(token).then((nextVendors) => { + setVendors(nextVendors); + if (mode === "create" && seededVendorId) { + const seededVendor = nextVendors.find((vendor) => vendor.id === seededVendorId); + if (seededVendor) { + setForm((current: PurchaseOrderInput) => ({ ...current, vendorId: seededVendor.id })); + setVendorSearchTerm(seededVendor.name); + } + } + }).catch(() => setVendors([])); api.getInventoryItemOptions(token).then((options) => setItemOptions(options.filter((option: InventoryItemOptionDto) => option.isPurchasable))).catch(() => setItemOptions([])); - }, [token]); + }, [mode, seededVendorId, token]); useEffect(() => { if (!token || mode !== "edit" || !orderId) { diff --git a/server/src/modules/purchasing/router.ts b/server/src/modules/purchasing/router.ts index 8ebb0e4..020639f 100644 --- a/server/src/modules/purchasing/router.ts +++ b/server/src/modules/purchasing/router.ts @@ -37,6 +37,7 @@ const purchaseOrderSchema = z.object({ const purchaseListQuerySchema = z.object({ q: z.string().optional(), status: z.enum(purchaseOrderStatuses).optional(), + vendorId: z.string().optional(), }); const purchaseStatusUpdateSchema = z.object({ diff --git a/server/src/modules/purchasing/service.ts b/server/src/modules/purchasing/service.ts index 14f3513..d9fe904 100644 --- a/server/src/modules/purchasing/service.ts +++ b/server/src/modules/purchasing/service.ts @@ -480,11 +480,12 @@ export async function listPurchaseVendorOptions(): Promise