From 7993f16a7629cddbf1cdee09bb6b9f0f6c042758 Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 17 Mar 2026 07:40:12 -0500 Subject: [PATCH] projects --- CHANGELOG.md | 1 + README.md | 2 +- SHIPPED.md | 1 + .../modules/projects/ProjectDetailPage.tsx | 163 +++++++++++++++++- 4 files changed, 158 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dd9354..f4acbe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ This file is the running release and change log for CODEXIUM. Keep it updated wh ### Added +- Project cockpit section on project detail pages for commercial, supply, execution, and delivery rollups - Project milestones with status, due dates, notes, and edit-time sequencing inside the project workflow - Project-side milestone and work-order rollups surfaced on project list and detail pages - Inventory SKU master builder with family-level sequence codes, branch-aware taxonomy management, and generated SKU previews on the item form diff --git a/README.md b/README.md index cbb271f..cbe5c78 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Navigation direction: ## Projects Direction -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, milestones, project-side milestone/work-order rollups, notes, commercial document links, shipment links, attachments, and dashboard visibility. +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, milestones, project-side milestone/work-order rollups, cockpit-style commercial/supply/execution/delivery visibility, notes, commercial document links, shipment links, attachments, and dashboard visibility. Current interactions: diff --git a/SHIPPED.md b/SHIPPED.md index a48ae9a..5598653 100644 --- a/SHIPPED.md +++ b/SHIPPED.md @@ -34,6 +34,7 @@ This file tracks roadmap phases, slices, and major foundations that have already - Logistics attachments directly on shipment records - Projects foundation with customer, quote, sales-order, shipment, owner, due-date, notes, and attachment linkage - Project milestones and project-side milestone/work-order rollups +- Project cockpit section on detail pages for commercial, supply, execution, and delivery visibility - 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 - Manufacturing stations, item routing templates, and automatic work-order operation planning for gantt scheduling diff --git a/client/src/modules/projects/ProjectDetailPage.tsx b/client/src/modules/projects/ProjectDetailPage.tsx index 031693e..48b482a 100644 --- a/client/src/modules/projects/ProjectDetailPage.tsx +++ b/client/src/modules/projects/ProjectDetailPage.tsx @@ -1,6 +1,8 @@ import { permissions } from "@mrp/shared"; import type { ProjectDetailDto } from "@mrp/shared/dist/projects/types.js"; import type { SalesOrderPlanningDto } from "@mrp/shared/dist/sales/types.js"; +import type { SalesDocumentDetailDto } from "@mrp/shared/dist/sales/types.js"; +import type { ShipmentDetailDto } from "@mrp/shared/dist/shipping/types.js"; import type { WorkOrderSummaryDto } from "@mrp/shared"; import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; @@ -18,6 +20,9 @@ export function ProjectDetailPage() { const [project, setProject] = useState(null); const [workOrders, setWorkOrders] = useState([]); const [planning, setPlanning] = useState(null); + const [quote, setQuote] = useState(null); + const [salesOrder, setSalesOrder] = useState(null); + const [shipment, setShipment] = useState(null); const [status, setStatus] = useState("Loading project..."); const canManage = user?.permissions.includes(permissions.projectsWrite) ?? false; @@ -28,17 +33,23 @@ export function ProjectDetailPage() { } api.getProject(token, projectId) - .then((nextProject) => { + .then(async (nextProject) => { setProject(nextProject); setStatus("Project loaded."); - if (nextProject.salesOrderId) { - api.getSalesOrderPlanning(token, nextProject.salesOrderId).then(setPlanning).catch(() => setPlanning(null)); - } else { - setPlanning(null); - } - return api.getWorkOrders(token, { projectId: nextProject.id }); + const [nextPlanning, nextWorkOrders, nextQuote, nextSalesOrder, nextShipment] = await Promise.all([ + nextProject.salesOrderId ? api.getSalesOrderPlanning(token, nextProject.salesOrderId).catch(() => null) : Promise.resolve(null), + api.getWorkOrders(token, { projectId: nextProject.id }), + nextProject.salesQuoteId ? api.getQuote(token, nextProject.salesQuoteId).catch(() => null) : Promise.resolve(null), + nextProject.salesOrderId ? api.getSalesOrder(token, nextProject.salesOrderId).catch(() => null) : Promise.resolve(null), + nextProject.shipmentId ? api.getShipment(token, nextProject.shipmentId).catch(() => null) : Promise.resolve(null), + ]); + + setPlanning(nextPlanning); + setWorkOrders(nextWorkOrders); + setQuote(nextQuote); + setSalesOrder(nextSalesOrder); + setShipment(nextShipment); }) - .then((nextWorkOrders) => setWorkOrders(nextWorkOrders)) .catch((error: unknown) => { const message = error instanceof ApiError ? error.message : "Unable to load project."; setStatus(message); @@ -49,6 +60,48 @@ export function ProjectDetailPage() { return
{status}
; } + const sortedMilestones = [...project.milestones].sort((left, right) => { + if (left.status === "COMPLETE" && right.status !== "COMPLETE") { + return 1; + } + if (left.status !== "COMPLETE" && right.status === "COMPLETE") { + return -1; + } + + if (left.dueDate && right.dueDate) { + return new Date(left.dueDate).getTime() - new Date(right.dueDate).getTime(); + } + if (left.dueDate) { + return -1; + } + if (right.dueDate) { + return 1; + } + + return left.sortOrder - right.sortOrder; + }); + const nextMilestone = sortedMilestones.find((milestone) => milestone.status !== "COMPLETE") ?? null; + const activeWorkOrders = workOrders.filter((workOrder) => workOrder.status === "RELEASED" || workOrder.status === "IN_PROGRESS" || workOrder.status === "ON_HOLD"); + const nextWorkOrder = [...activeWorkOrders] + .sort((left, right) => { + if (left.dueDate && right.dueDate) { + return new Date(left.dueDate).getTime() - new Date(right.dueDate).getTime(); + } + if (left.dueDate) { + return -1; + } + if (right.dueDate) { + return 1; + } + return left.workOrderNumber.localeCompare(right.workOrderNumber); + })[0] ?? null; + const materialExceptionItems = planning + ? planning.items.filter((item) => item.uncoveredQuantity > 0 || item.recommendedBuildQuantity > 0 || item.recommendedPurchaseQuantity > 0).slice(0, 5) + : []; + const completionPercent = project.rollups.milestoneCount > 0 + ? Math.round((project.rollups.completedMilestoneCount / project.rollups.milestoneCount) * 100) + : 0; + return (
@@ -95,6 +148,100 @@ export function ProjectDetailPage() {
{project.rollups.completedWorkOrderCount} complete
+
+
+
+

Project Cockpit

+

Cross-functional execution view

+

Commercial, supply, execution, and delivery signals for this program in one place.

+
+
+
Milestone Progress
+
{completionPercent}%
+
+
+
+
+

Commercial

+
+ {salesOrder ? `$${salesOrder.total.toFixed(2)}` : quote ? `$${quote.total.toFixed(2)}` : "Not linked"} +
+
+ {salesOrder ? `${salesOrder.documentNumber} · ${salesOrder.status}` : quote ? `${quote.documentNumber} · ${quote.status}` : "Link a quote or sales order"} +
+
+
+

Supply

+
{planning ? planning.summary.uncoveredItemCount : 0} shortage items
+
+ {planning ? `Build ${planning.summary.totalBuildQuantity} · Buy ${planning.summary.totalPurchaseQuantity}` : "No sales-order planning linked"} +
+
+
+

Execution

+
{project.rollups.activeWorkOrderCount} active work orders
+
+ {nextWorkOrder ? `${nextWorkOrder.workOrderNumber} due ${nextWorkOrder.dueDate ? new Date(nextWorkOrder.dueDate).toLocaleDateString() : "unscheduled"}` : "No active work order due date"} +
+
+
+

Delivery

+
{shipment ? shipment.status.replace("_", " ") : "Not linked"}
+
+ {shipment ? `${shipment.shipmentNumber} · ${shipment.packageCount} package(s)` : "Link a shipment to track delivery"} +
+
+
+
+
+

Next Checkpoints

+
+
+
Milestone
+
{nextMilestone ? nextMilestone.title : "All milestones complete"}
+
+ {nextMilestone + ? `${nextMilestone.status.replace("_", " ")} · ${nextMilestone.dueDate ? new Date(nextMilestone.dueDate).toLocaleDateString() : "No due date"}` + : "No open milestone remains."} +
+
+
+
Work Order
+
{nextWorkOrder ? nextWorkOrder.workOrderNumber : "No active work orders"}
+
+ {nextWorkOrder + ? `${nextWorkOrder.itemSku} · ${nextWorkOrder.completedQuantity}/${nextWorkOrder.quantity} complete` + : "Launch or link a work order to populate execution checkpoints."} +
+
+
+
+
+

Material Watchlist

+ {materialExceptionItems.length === 0 ? ( +
+ No current build/buy exception items from linked sales-order planning. +
+ ) : ( +
+ {materialExceptionItems.map((item) => ( +
+
+
+
{item.itemSku}
+
{item.itemName}
+
+
+ Build {item.recommendedBuildQuantity} · Buy {item.recommendedPurchaseQuantity} · Uncovered {item.uncoveredQuantity} +
+
+
+ ))} +
+ )} +
+
+

Customer Linkage