diff --git a/AGENTS.md b/AGENTS.md index 932ce26..f9cc350 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,8 @@ 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 -- shipping shipments and packing-slip PDFs +- 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 - Puppeteer PDF foundation - single-container Docker deployment @@ -105,7 +106,7 @@ If implementation changes invalidate those docs, update them in the same change - Purchase-order item lookup must only expose inventory items flagged as purchasable - 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 should be treated as a first-class future domain that anchors long-running program execution across CRM, sales, inventory, purchasing, shipping, and planning +- 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 - 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 @@ -114,11 +115,11 @@ If implementation changes invalidate those docs, update them in the same change Near-term priorities are: -1. Shipping labels, bills of lading, and logistics attachments -2. Projects and program management -3. Manufacturing execution -4. Vendor invoice/supporting-document attachments and broader vendor-side operational depth -5. Sales approvals and document revision history +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 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 3ae64a6..aedc9af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,15 @@ This file is the running release and change log for MRP Codex. Keep it updated w ### Added -- No unreleased entries recorded yet. +- Projects domain foundation with customer, owner, due date, priority, notes, and attachment support +- 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 ### Changed -- No unreleased entries recorded yet. +- 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 ## 2026-03-15 diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md index 34a8943..ad7ac38 100644 --- a/INSTRUCTIONS.md +++ b/INSTRUCTIONS.md @@ -20,7 +20,8 @@ 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 -- shipping shipments linked to sales orders and packing-slip PDFs +- 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 - Dockerized single-container deployment - Puppeteer PDF pipeline foundation @@ -36,7 +37,7 @@ This repository implements the platform foundation milestone: 8. Maintain the denser UI baseline on active screens; avoid reintroducing oversized `px-4 py-3` style controls, tall action bars, or overly loose card spacing without a specific reason. 9. Treat the landing page as `Dashboard`: a metric-oriented, modular command surface that should accumulate reusable operational panels over time. 10. Purchase-order item selection must be restricted to inventory items where `isPurchasable = true`. -11. Treat future `Projects` work as a first-class cross-module domain tying together CRM, sales, inventory, purchasing, shipping, and planning; do not bury it as a one-off manufacturing subfeature. +11. Treat `Projects` as a first-class cross-module domain tying together CRM, sales, inventory, purchasing, shipping, and planning; do not bury it as a one-off manufacturing subfeature. 12. Keep `Projects`, `Manufacturing`, and `Planning` distinct: projects are long-running program records, manufacturing is execution, and planning is scheduling/visibility. 13. New top-level modules added to the app shell should include a matching SVG icon in navigation so the module list remains visually scannable. @@ -56,10 +57,9 @@ This repository implements the platform foundation milestone: ## Next roadmap candidates -- shipping labels, bills of lading, and logistics attachments -- projects and program management - 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 +- inventory transfers, reservations, and deeper stock controls - broader audit and operations maturity diff --git a/README.md b/README.md index ffcddc7..469ed52 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ 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 -- shipping shipments linked to sales orders with packing-slip PDFs +- 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 - file storage and PDF rendering ## Product Map @@ -31,21 +32,21 @@ Current completed foundation areas: - inventory foundation - sales and purchasing foundation - shipping foundation +- projects foundation - branding, attachments, auth/RBAC, and PDF infrastructure Planned cross-module execution areas: -- projects and program management - manufacturing execution - planning and gantt scheduling Near-term priorities: -1. Shipping labels, bills of lading, and logistics attachments -2. Projects and program management -3. Manufacturing execution -4. Vendor invoice/supporting-document attachments and broader vendor-facing operational depth -5. Sales approvals and revision history +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 Revisit / deferred items: @@ -54,7 +55,6 @@ Revisit / deferred items: - sales approvals and revision history - inventory transfers, reservations, and deeper stock controls - deeper audit-trail coverage -- projects are not yet first-class records even though planning/manufacturing flows will need them Dashboard direction: @@ -63,7 +63,8 @@ Dashboard direction: - new modules should add reusable dashboard cards/panels instead of replacing the whole layout - 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 should eventually feed dashboard widgets for active jobs, overdue milestones, shortages, and shipment readiness +- projects now feed dashboard widgets for active programs, overdue work, and risk +- future project widgets should deepen milestones, shortages, and shipment readiness Navigation direction: @@ -73,18 +74,21 @@ Navigation direction: ## Projects Direction -Projects should become the long-running program and delivery layer tying together commercial work, engineering context, purchasing, manufacturing, shipping, and customer-facing execution. +Projects are now the long-running program and delivery layer for cross-module execution. The current slice ships project records with customer linkage, owner assignment, priority, due dates, notes, commercial document links, shipment links, attachments, and dashboard visibility. -Planned interactions: +Current interactions: - CRM: each project should link to a customer account and relevant contacts -- Sales: quotes and sales orders should be able to spawn or attach to projects +- Sales: quotes and sales orders can already attach to projects +- Shipping: shipments tied to project deliverables are visible from the project record +- Dashboard: projects now contribute status, risk, backlog, and overdue widgets + +Next expansion areas: + - Inventory: projects should reference item/BOM scope and later expose shortages or allocations - Purchasing: project material demand should be visible to purchasing and receiving workflows -- Shipping: shipments tied to project deliverables should be visible from the project record - Manufacturing: work orders should link back to projects without turning projects into the manufacturing module - Planning: project milestones and execution dates should feed gantt scheduling and dependency views -- Dashboard: projects should contribute status, risk, backlog, and milestone widgets ## Manufacturing Direction @@ -252,12 +256,12 @@ The current shipping foundation supports: - shipment quick status actions from the shipment detail page - related-shipment visibility from the sales-order detail page - branded packing-slip PDF rendering from shipment detail pages +- branded shipping-label and bill-of-lading PDF rendering from shipment detail pages +- logistics attachments directly on shipment records QOL direction: -- shipment labels -- bills of lading -- logistics attachments and reprint/history actions +- reprint/history actions for generated logistics PDFs - partial-shipment and split-shipment UX This module introduces `shipping.read` and `shipping.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. @@ -303,17 +307,18 @@ As of March 14, 2026, the latest committed domain migrations include: - inventory default price support - sales totals and commercial fields - shipping foundation +- projects 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, 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, 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 client build still emits a Vite chunk-size warning because the app has not been code-split yet. ## PDF Generation -Puppeteer is used by the backend to render HTML templates into professional PDFs. The current PDF surface includes the branded company-profile preview, sales quotes, sales orders, purchase orders, and shipment packing slips. The Docker image includes Chromium runtime dependencies required for headless execution. +Puppeteer is used by the backend to render HTML templates into professional PDFs. The current PDF surface includes the branded company-profile preview, sales quotes, sales orders, purchase orders, shipment packing slips, shipping labels, and bills of lading. The Docker image includes Chromium runtime dependencies required for headless execution. diff --git a/ROADMAP.md b/ROADMAP.md index d908862..0139ee0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -38,7 +38,10 @@ MRP Codex is being built as a streamlined, modular manufacturing resource planni - Purchase receiving foundation with warehouse/location posting, receipt history, and per-line received quantity tracking - Branded sales quote, sales order, and purchase-order PDF templates through the shared Puppeteer pipeline - Shipping shipment records linked to sales orders -- Packing-slip PDF rendering for shipments +- Packing-slip, shipping-label, and bill-of-lading PDF rendering for shipments +- 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 - 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 @@ -52,8 +55,9 @@ 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, shipment labels, or vendor-side attachment handling +- 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 ## Dashboard Plan @@ -61,6 +65,7 @@ MRP Codex is being built as a streamlined, modular manufacturing resource planni - 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 +- Continue expanding the new project widgets into milestone, blockage, and shipment-readiness 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 @@ -128,7 +133,7 @@ QOL subfeatures: QOL subfeatures: -- Shipment labels and printer-friendly document actions +- Printer-friendly reprint and history actions for logistics documents - Partial shipment workflow and split-shipment visibility - Better tracking-link UX and carrier-specific shortcuts - Packing verification and ship-confirm checkpoints @@ -137,8 +142,13 @@ QOL subfeatures: ### Phase 5: Projects and program management +Foundation slice shipped: + - Project records with customer linkage, status, owner, priority, due dates, and notes -- Project-to-sales-order and quote linkage so commercial commitments can roll into delivery programs +- Project-to-quote, sales-order, and shipment linkage for delivery context +- Project attachments through the shared file pipeline +- Project list/detail/create/edit flows and dashboard visibility + - Project document hub for drawings, support files, correspondence, and revision references - Milestones, checkpoints, and non-manufacturing work packages for long-running execution tracking - Project-level commercial, material, schedule, and delivery rollups @@ -231,13 +241,11 @@ QOL subfeatures: - 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 -- Shipping is now linked to sales orders, but labels, bills of lading, and logistics attachments are still pending - 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 - 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 -- Projects are not yet implemented as first-class long-running program records - Manufacturing execution is not yet separated cleanly from planning/scheduling in the current future-state docs and implementation ## Cross-cutting improvements @@ -251,8 +259,8 @@ QOL subfeatures: ## Near-term priority order -1. Shipping labels, bills of lading, and logistics attachments -2. Projects and program management -3. Manufacturing execution -4. Vendor invoice/supporting-document attachments and broader vendor-side operational depth -5. Sales approvals and document revision history +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 diff --git a/STRUCTURE.md b/STRUCTURE.md index f5ad246..3a0f85f 100644 --- a/STRUCTURE.md +++ b/STRUCTURE.md @@ -27,7 +27,7 @@ - Purchase-order item pickers must only surface inventory items flagged as purchasable. - Shipping, sales, and future purchasing PDFs should be rendered through the backend documents module and shared Puppeteer pipeline rather than ad hoc frontend-only exports. - Preserve the current dense operations UI style on active module pages: compact controls, tighter card padding, and shorter empty states unless a screen has a clear reason to be more spacious. -- Treat `projects` as its own long-lived domain under both client and server when implemented; it should integrate with CRM, sales, inventory, purchasing, shipping, and planning rather than living inside only one of those modules. +- Treat `projects` as its own long-lived domain under both client and server. It should continue integrating with CRM, sales, inventory, purchasing, shipping, and planning rather than living inside only one of those modules. - Treat `manufacturing` as a separate long-lived domain from `projects`; work orders, routings, labor capture, WIP, and shop-floor execution should not be modeled only as project fields. - Treat `planning` as the scheduling/visibility layer that consumes project and manufacturing data rather than replacing either domain. - When adding a new top-level module to the shell, add a lightweight SVG icon in the navigation config so desktop and mobile nav stay aligned. diff --git a/UNRAID.md b/UNRAID.md index c7f26f9..b712a39 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, packing slips, the inventory `defaultPrice` field, and purchasable-only purchase-order item selection. 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, and the new projects domain. 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 f771a2a..744b5e0 100644 --- a/client/src/components/AppShell.tsx +++ b/client/src/components/AppShell.tsx @@ -15,6 +15,7 @@ const links = [ { to: "/sales/orders", label: "Sales Orders", icon: }, { to: "/purchasing/orders", label: "Purchase Orders", icon: }, { to: "/shipping/shipments", label: "Shipments", icon: }, + { to: "/projects", label: "Projects", icon: }, { to: "/planning/gantt", label: "Gantt", icon: }, ]; @@ -156,6 +157,19 @@ function GanttIcon() { ); } +function ProjectsIcon() { + return ( + + + + + + + + + ); +} + export function AppShell() { const { user, logout } = useAuth(); diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index 9a4e655..4981769 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -33,6 +33,17 @@ import type { WarehouseLocationOptionDto, WarehouseSummaryDto, } from "@mrp/shared/dist/inventory/types.js"; +import type { + ProjectCustomerOptionDto, + ProjectDetailDto, + ProjectDocumentOptionDto, + ProjectInput, + ProjectOwnerOptionDto, + ProjectPriority, + ProjectShipmentOptionDto, + ProjectStatus, + ProjectSummaryDto, +} from "@mrp/shared/dist/projects/types.js"; import type { SalesCustomerOptionDto, SalesDocumentDetailDto, @@ -381,6 +392,58 @@ export const api = { token ); }, + getProjects( + token: string, + filters?: { q?: string; status?: ProjectStatus; priority?: ProjectPriority; customerId?: string; ownerId?: string } + ) { + return request( + `/api/v1/projects${buildQueryString({ + q: filters?.q, + status: filters?.status, + priority: filters?.priority, + customerId: filters?.customerId, + ownerId: filters?.ownerId, + })}`, + undefined, + token + ); + }, + getProject(token: string, projectId: string) { + return request(`/api/v1/projects/${projectId}`, undefined, token); + }, + createProject(token: string, payload: ProjectInput) { + return request("/api/v1/projects", { method: "POST", body: JSON.stringify(payload) }, token); + }, + updateProject(token: string, projectId: string, payload: ProjectInput) { + return request(`/api/v1/projects/${projectId}`, { method: "PUT", body: JSON.stringify(payload) }, token); + }, + getProjectCustomerOptions(token: string) { + return request("/api/v1/projects/customers/options", undefined, token); + }, + getProjectOwnerOptions(token: string) { + return request("/api/v1/projects/owners/options", undefined, token); + }, + getProjectQuoteOptions(token: string, customerId?: string) { + return request( + `/api/v1/projects/quotes/options${buildQueryString({ customerId })}`, + undefined, + token + ); + }, + getProjectOrderOptions(token: string, customerId?: string) { + return request( + `/api/v1/projects/orders/options${buildQueryString({ customerId })}`, + undefined, + token + ); + }, + getProjectShipmentOptions(token: string, customerId?: string) { + return request( + `/api/v1/projects/shipments/options${buildQueryString({ customerId })}`, + undefined, + token + ); + }, getGanttDemo(token: string) { return request<{ tasks: GanttTaskDto[]; links: GanttLinkDto[] }>("/api/v1/gantt/demo", undefined, token); }, @@ -521,6 +584,32 @@ export const api = { return response.blob(); }, + async getShipmentLabelPdf(token: string, shipmentId: string) { + const response = await fetch(`/api/v1/documents/shipping/shipments/${shipmentId}/shipping-label.pdf`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new ApiError("Unable to render shipping label PDF.", "SHIPPING_LABEL_FAILED"); + } + + return response.blob(); + }, + async getShipmentBillOfLadingPdf(token: string, shipmentId: string) { + const response = await fetch(`/api/v1/documents/shipping/shipments/${shipmentId}/bill-of-lading.pdf`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new ApiError("Unable to render bill of lading PDF.", "BILL_OF_LADING_FAILED"); + } + + return response.blob(); + }, async getQuotePdf(token: string, quoteId: string) { const response = await fetch(`/api/v1/documents/sales/quotes/${quoteId}/document.pdf`, { headers: { diff --git a/client/src/main.tsx b/client/src/main.tsx index 3291474..5654193 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -21,6 +21,9 @@ import { InventoryItemsPage } from "./modules/inventory/InventoryItemsPage"; import { PurchaseDetailPage } from "./modules/purchasing/PurchaseDetailPage"; import { PurchaseFormPage } from "./modules/purchasing/PurchaseFormPage"; import { PurchaseListPage } from "./modules/purchasing/PurchaseListPage"; +import { ProjectDetailPage } from "./modules/projects/ProjectDetailPage"; +import { ProjectFormPage } from "./modules/projects/ProjectFormPage"; +import { ProjectsPage } from "./modules/projects/ProjectsPage"; import { WarehouseDetailPage } from "./modules/inventory/WarehouseDetailPage"; import { WarehouseFormPage } from "./modules/inventory/WarehouseFormPage"; import { WarehousesPage } from "./modules/inventory/WarehousesPage"; @@ -66,6 +69,13 @@ const router = createBrowserRouter([ { path: "/inventory/warehouses/:warehouseId", element: }, ], }, + { + element: , + children: [ + { path: "/projects", element: }, + { path: "/projects/:projectId", element: }, + ], + }, { element: , children: [ @@ -98,6 +108,13 @@ const router = createBrowserRouter([ { path: "/crm/vendors/:vendorId/edit", element: }, ], }, + { + element: , + children: [ + { path: "/projects/new", element: }, + { path: "/projects/:projectId/edit", element: }, + ], + }, { element: , children: [ diff --git a/client/src/modules/dashboard/DashboardPage.tsx b/client/src/modules/dashboard/DashboardPage.tsx index 04409dd..bca0211 100644 --- a/client/src/modules/dashboard/DashboardPage.tsx +++ b/client/src/modules/dashboard/DashboardPage.tsx @@ -13,6 +13,7 @@ interface DashboardSnapshot { quotes: Awaited> | null; orders: Awaited> | null; shipments: Awaited> | null; + projects: Awaited> | null; refreshedAt: string; } @@ -50,6 +51,7 @@ export function DashboardPage() { const [snapshot, setSnapshot] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + const canWriteProjects = hasPermission(user?.permissions, permissions.projectsWrite); useEffect(() => { if (!token || !user) { @@ -67,6 +69,7 @@ export function DashboardPage() { const canReadInventory = hasPermission(user.permissions, permissions.inventoryRead); const canReadSales = hasPermission(user.permissions, permissions.salesRead); const canReadShipping = hasPermission(user.permissions, permissions.shippingRead); + const canReadProjects = hasPermission(user.permissions, permissions.projectsRead); async function loadSnapshot() { const results = await Promise.allSettled([ @@ -77,6 +80,7 @@ export function DashboardPage() { canReadSales ? api.getQuotes(authToken) : Promise.resolve(null), canReadSales ? api.getSalesOrders(authToken) : Promise.resolve(null), canReadShipping ? api.getShipments(authToken) : Promise.resolve(null), + canReadProjects ? api.getProjects(authToken) : Promise.resolve(null), ]); if (!isMounted) { @@ -97,6 +101,7 @@ export function DashboardPage() { 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, refreshedAt: new Date().toISOString(), }); setIsLoading(false); @@ -124,12 +129,14 @@ export function DashboardPage() { const quotes = snapshot?.quotes ?? []; const orders = snapshot?.orders ?? []; const shipments = snapshot?.shipments ?? []; + const projects = snapshot?.projects ?? []; const accessibleModules = [ snapshot?.customers !== null || snapshot?.vendors !== null, snapshot?.items !== null || snapshot?.warehouses !== null, snapshot?.quotes !== null || snapshot?.orders !== null, snapshot?.shipments !== null, + snapshot?.projects !== null, ].filter(Boolean).length; const customerCount = customers.length; @@ -157,6 +164,17 @@ export function DashboardPage() { const inTransitCount = shipments.filter((shipment) => shipment.status === "SHIPPED").length; const deliveredCount = shipments.filter((shipment) => shipment.status === "DELIVERED").length; + const projectCount = projects.length; + const activeProjectCount = projects.filter((project) => project.status === "ACTIVE").length; + const atRiskProjectCount = projects.filter((project) => project.status === "AT_RISK").length; + const overdueProjectCount = projects.filter((project) => { + if (!project.dueDate || project.status === "COMPLETE") { + return false; + } + + return new Date(project.dueDate).getTime() < Date.now(); + }).length; + const lastActivityAt = [ ...customers.map((customer) => customer.updatedAt), ...vendors.map((vendor) => vendor.updatedAt), @@ -165,6 +183,7 @@ export function DashboardPage() { ...quotes.map((quote) => quote.updatedAt), ...orders.map((order) => order.updatedAt), ...shipments.map((shipment) => shipment.updatedAt), + ...projects.map((project) => project.updatedAt), ] .sort() .at(-1) ?? null; @@ -206,6 +225,15 @@ export function DashboardPage() { : "Shipping metrics are permission-gated.", tone: "border-brand/30 bg-brand/10 text-brand", }, + { + label: "Project Load", + value: snapshot?.projects !== null ? `${activeProjectCount}` : "No access", + detail: + snapshot?.projects !== null + ? `${atRiskProjectCount} at risk and ${overdueProjectCount} overdue` + : "Project metrics are permission-gated.", + tone: "border-violet-400/30 bg-violet-500/12 text-violet-700 dark:text-violet-300", + }, ]; const modulePanels = [ @@ -277,13 +305,31 @@ export function DashboardPage() { { label: "Open packing flow", to: "/sales/orders" }, ], }, + { + title: "Projects", + eyebrow: "Program Control", + summary: + snapshot?.projects !== null + ? "Project records now tie customers, commercial documents, shipment context, and delivery ownership into one operational surface." + : "Project read permission is required to surface program metrics here.", + metrics: [ + { label: "Active", value: snapshot?.projects !== null ? `${activeProjectCount}` : "No access" }, + { label: "At risk", value: snapshot?.projects !== null ? `${atRiskProjectCount}` : "No access" }, + { label: "Overdue", value: snapshot?.projects !== null ? `${overdueProjectCount}` : "No access" }, + ], + links: [ + { label: "Open projects", to: "/projects" }, + ...(canWriteProjects ? [{ label: "New project", to: "/projects/new" }] : []), + ], + }, ]; const futureModules = [ - "Purchase-order queue and supplier receipts", + "Vendor invoice attachments and supplier exception queues", "Stock transfers, allocations, and cycle counts", - "Shipping labels, bills of lading, and delivery exceptions", - "Manufacturing schedule, work orders, and bottleneck metrics", + "Manufacturing work orders, routings, and bottleneck metrics", + "Planning timeline, milestones, and dependency views", + "Audit trails, diagnostics, and system health checks", ]; return ( @@ -304,8 +350,8 @@ export function DashboardPage() {

- This landing page now reads directly from live CRM, inventory, sales, and shipping 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, 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.

@@ -331,6 +377,9 @@ export function DashboardPage() { Open inventory + + Open projects +
{error ?
{error}
: null}
@@ -347,7 +396,7 @@ export function DashboardPage() { -
+
{metricCards.map((card) => (

{card.label}

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

{panel.eyebrow}

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

Inventory Watch

Master data pressure points

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

Project Watch

+

Program status and delivery pressure

+
+
+ Total projects + {snapshot?.projects !== null ? `${projectCount}` : "No access"} +
+
+ At risk + {snapshot?.projects !== null ? `${atRiskProjectCount}` : "No access"} +
+
+ Overdue + {snapshot?.projects !== null ? `${overdueProjectCount}` : "No access"} +
+
+

Shipping Watch

Execution and delivery status

diff --git a/client/src/modules/projects/ProjectDetailPage.tsx b/client/src/modules/projects/ProjectDetailPage.tsx new file mode 100644 index 0000000..c00b2ad --- /dev/null +++ b/client/src/modules/projects/ProjectDetailPage.tsx @@ -0,0 +1,107 @@ +import { permissions } from "@mrp/shared"; +import type { ProjectDetailDto } from "@mrp/shared/dist/projects/types.js"; +import { useEffect, 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 { ProjectPriorityBadge } from "./ProjectPriorityBadge"; +import { ProjectStatusBadge } from "./ProjectStatusBadge"; + +export function ProjectDetailPage() { + const { token, user } = useAuth(); + const { projectId } = useParams(); + const [project, setProject] = useState(null); + const [status, setStatus] = useState("Loading project..."); + + const canManage = user?.permissions.includes(permissions.projectsWrite) ?? false; + + useEffect(() => { + if (!token || !projectId) { + return; + } + + api.getProject(token, projectId) + .then((nextProject) => { + setProject(nextProject); + setStatus("Project loaded."); + }) + .catch((error: unknown) => { + const message = error instanceof ApiError ? error.message : "Unable to load project."; + setStatus(message); + }); + }, [projectId, token]); + + if (!project) { + return
{status}
; + } + + return ( +
+
+
+
+

Project

+

{project.projectNumber}

+

{project.name}

+
+ + +
+
+
+ Back to projects + {canManage ? Edit project : null} +
+
+
+
+

Customer

{project.customerName}
+

Owner

{project.ownerName || "Unassigned"}
+

Due Date

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

Created

{new Date(project.createdAt).toLocaleDateString()}
+
+
+
+

Customer Linkage

+
+
Account
{project.customerName}
+
Email
{project.customerEmail}
+
Phone
{project.customerPhone}
+
+
+
+

Program Notes

+

{project.notes || "No project notes recorded."}

+
+
+
+

Commercial + Delivery Links

+
+
+
Quote
+
{project.salesQuoteNumber ? {project.salesQuoteNumber} : "Not linked"}
+
+
+
Sales Order
+
{project.salesOrderNumber ? {project.salesOrderNumber} : "Not linked"}
+
+
+
Shipment
+
{project.shipmentNumber ? {project.shipmentNumber} : "Not linked"}
+
+
+
+ +
{status}
+
+ ); +} diff --git a/client/src/modules/projects/ProjectFormPage.tsx b/client/src/modules/projects/ProjectFormPage.tsx new file mode 100644 index 0000000..44b0e1f --- /dev/null +++ b/client/src/modules/projects/ProjectFormPage.tsx @@ -0,0 +1,198 @@ +import type { + ProjectCustomerOptionDto, + ProjectDocumentOptionDto, + ProjectInput, + ProjectOwnerOptionDto, + ProjectShipmentOptionDto, +} from "@mrp/shared/dist/projects/types.js"; +import { useEffect, useState } from "react"; +import { Link, useNavigate, useParams } from "react-router-dom"; + +import { useAuth } from "../../auth/AuthProvider"; +import { api, ApiError } from "../../lib/api"; +import { emptyProjectInput, projectPriorityOptions, projectStatusOptions } from "./config"; + +export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) { + const { token, user } = useAuth(); + const navigate = useNavigate(); + const { projectId } = useParams(); + const [form, setForm] = useState(() => ({ ...emptyProjectInput, ownerId: user?.id ?? null })); + const [customerOptions, setCustomerOptions] = useState([]); + const [ownerOptions, setOwnerOptions] = useState([]); + const [quoteOptions, setQuoteOptions] = useState([]); + const [orderOptions, setOrderOptions] = useState([]); + const [shipmentOptions, setShipmentOptions] = useState([]); + const [status, setStatus] = useState(mode === "create" ? "Create a new project." : "Loading project..."); + const [isSaving, setIsSaving] = useState(false); + + useEffect(() => { + if (!token) { + return; + } + + api.getProjectCustomerOptions(token).then(setCustomerOptions).catch(() => setCustomerOptions([])); + api.getProjectOwnerOptions(token).then(setOwnerOptions).catch(() => setOwnerOptions([])); + }, [token]); + + useEffect(() => { + if (!token || !form.customerId) { + setQuoteOptions([]); + setOrderOptions([]); + setShipmentOptions([]); + return; + } + + api.getProjectQuoteOptions(token, form.customerId).then(setQuoteOptions).catch(() => setQuoteOptions([])); + api.getProjectOrderOptions(token, form.customerId).then(setOrderOptions).catch(() => setOrderOptions([])); + api.getProjectShipmentOptions(token, form.customerId).then(setShipmentOptions).catch(() => setShipmentOptions([])); + }, [form.customerId, token]); + + useEffect(() => { + if (!token || mode !== "edit" || !projectId) { + return; + } + + api.getProject(token, projectId) + .then((project) => { + setForm({ + name: project.name, + status: project.status, + priority: project.priority, + customerId: project.customerId, + salesQuoteId: project.salesQuoteId, + salesOrderId: project.salesOrderId, + shipmentId: project.shipmentId, + ownerId: project.ownerId, + dueDate: project.dueDate, + notes: project.notes, + }); + setStatus("Project loaded."); + }) + .catch((error: unknown) => { + const message = error instanceof ApiError ? error.message : "Unable to load project."; + setStatus(message); + }); + }, [mode, projectId, token]); + + function updateField(key: Key, value: ProjectInput[Key]) { + setForm((current: ProjectInput) => ({ + ...current, + [key]: value, + ...(key === "customerId" + ? { + salesQuoteId: null, + salesOrderId: null, + shipmentId: null, + } + : {}), + })); + } + + async function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + if (!token) { + return; + } + + setIsSaving(true); + setStatus("Saving project..."); + try { + const saved = mode === "create" ? await api.createProject(token, form) : await api.updateProject(token, projectId ?? "", form); + navigate(`/projects/${saved.id}`); + } catch (error: unknown) { + const message = error instanceof ApiError ? error.message : "Unable to save project."; + setStatus(message); + setIsSaving(false); + } + } + + return ( +
+
+
+
+

Projects Editor

+

{mode === "create" ? "New Project" : "Edit Project"}

+

Create a customer-linked program record that can anchor commercial documents, delivery work, and project files.

+
+ + Cancel + +
+
+
+
+ + +
+
+ + + + +
+
+ + + +
+