From cdbd54b8cc466e6612391bde6a1c222b4e2b51d6 Mon Sep 17 00:00:00 2001 From: jason Date: Tue, 17 Mar 2026 19:17:12 -0500 Subject: [PATCH] cost rollups --- CHANGELOG.md | 2 +- README.md | 2 +- SHIPPED.md | 2 +- .../modules/projects/ProjectDetailPage.tsx | 6 ++ server/src/modules/projects/service.ts | 95 +++++++++++++++++++ shared/src/projects/types.ts | 12 +++ 6 files changed, 116 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff357b8..75f977d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +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, delivery, purchasing, and readiness-risk rollups +- Project cockpit section on project detail pages for commercial, supply, execution, delivery, purchasing, readiness-risk, and project cost snapshot 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 87f7ee1..02f6025 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, cockpit-style commercial/supply/execution/delivery/purchasing visibility, readiness-risk scoring, 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/purchasing visibility, readiness-risk scoring, a cost snapshot from linked purchasing and manufacturing data, notes, commercial document links, shipment links, attachments, and dashboard visibility. Current interactions: diff --git a/SHIPPED.md b/SHIPPED.md index 2c41a17..6606fc8 100644 --- a/SHIPPED.md +++ b/SHIPPED.md @@ -34,7 +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, delivery, purchasing, and readiness-risk visibility +- Project cockpit section on detail pages for commercial, supply, execution, delivery, purchasing, readiness-risk, and cost-snapshot 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 e61068e..3b78459 100644 --- a/client/src/modules/projects/ProjectDetailPage.tsx +++ b/client/src/modules/projects/ProjectDetailPage.tsx @@ -159,6 +159,12 @@ export function ProjectDetailPage() {

Readiness Score

{readinessScore}%
{project.cockpit.risk.riskLevel} risk - {project.cockpit.risk.shortageItemCount} shortage item(s)

Material Spend

${project.cockpit.purchasing.linkedLineValue.toFixed(2)}
{project.cockpit.purchasing.vendorCount} vendor(s) across {project.cockpit.purchasing.linkedLineCount} linked line(s)
+
+

Booked Revenue

{formatCurrency(project.cockpit.costs.bookedRevenue)}
Quoted baseline {formatCurrency(project.cockpit.costs.quotedRevenue)}
+

Purchase Commitment

${project.cockpit.costs.linkedPurchaseCommitment.toFixed(2)}
Linked PO line value already committed
+

Planned Material Cost

${project.cockpit.costs.plannedMaterialCost.toFixed(2)}
Issued so far ${project.cockpit.costs.issuedMaterialCost.toFixed(2)}
+

Build Load

{project.cockpit.costs.completedBuildQuantity}/{project.cockpit.costs.buildQuantity}
{project.cockpit.costs.plannedOperationHours.toFixed(1)} planned operation hours
+

Next Checkpoints

diff --git a/server/src/modules/projects/service.ts b/server/src/modules/projects/service.ts index 426d7aa..e1c0eaa 100644 --- a/server/src/modules/projects/service.ts +++ b/server/src/modules/projects/service.ts @@ -134,6 +134,28 @@ type ProjectReceiptLineRecord = { }; }; +type ProjectCostWorkOrderRecord = { + quantity: number; + completedQuantity: number; + item: { + bomLines: Array<{ + quantity: number; + componentItem: { + defaultCost: number | null; + }; + }>; + }; + operations: Array<{ + plannedMinutes: number; + }>; + materialIssues: Array<{ + quantity: number; + componentItem: { + defaultCost: number | null; + }; + }>; +}; + function roundMoney(value: number) { return Math.round(value * 100) / 100; } @@ -327,6 +349,44 @@ async function buildProjectCockpit(record: ProjectRecord, rollups: ProjectRollup : Promise.resolve([]), record.salesOrder ? getSalesOrderPlanningById(record.salesOrder.id) : Promise.resolve(null), ]); + const workOrderCosts = await prisma.workOrder.findMany({ + where: { + projectId: record.id, + }, + select: { + quantity: true, + completedQuantity: true, + item: { + select: { + bomLines: { + select: { + quantity: true, + componentItem: { + select: { + defaultCost: true, + }, + }, + }, + }, + }, + }, + operations: { + select: { + plannedMinutes: true, + }, + }, + materialIssues: { + select: { + quantity: true, + componentItem: { + select: { + defaultCost: true, + }, + }, + }, + }, + }, + }); const typedPurchaseOrders = purchaseOrders as ProjectPurchaseOrderRecord[]; const typedReceiptLines = receiptLines as ProjectReceiptLineRecord[]; @@ -334,6 +394,7 @@ async function buildProjectCockpit(record: ProjectRecord, rollups: ProjectRollup const typedSalesOrder = salesOrder as ProjectSalesDocumentRecord | null; const typedShipment = shipment as ProjectShipmentRecord | null; const typedPlanning = planning as SalesOrderPlanningDto | null; + const typedWorkOrderCosts = workOrderCosts as ProjectCostWorkOrderRecord[]; const purchaseOrdersSummary: ProjectCockpitPurchaseOrderDto[] = typedPurchaseOrders.map((purchaseOrder) => { const linkedLineCount = purchaseOrder.lines.length; @@ -427,6 +488,30 @@ async function buildProjectCockpit(record: ProjectRecord, rollups: ProjectRollup const commercialQuoteTotal = typedQuote ? calculateSalesDocumentTotal(typedQuote) : null; const commercialOrderTotal = typedSalesOrder ? calculateSalesDocumentTotal(typedSalesOrder) : null; + const plannedMaterialCost = roundMoney( + typedWorkOrderCosts.reduce((sum, workOrder) => ( + sum + workOrder.item.bomLines.reduce( + (workOrderSum, bomLine) => workOrderSum + (bomLine.quantity * workOrder.quantity * (bomLine.componentItem.defaultCost ?? 0)), + 0 + ) + ), 0) + ); + const issuedMaterialCost = roundMoney( + typedWorkOrderCosts.reduce((sum, workOrder) => ( + sum + workOrder.materialIssues.reduce( + (workOrderSum, issue) => workOrderSum + (issue.quantity * (issue.componentItem.defaultCost ?? 0)), + 0 + ) + ), 0) + ); + const plannedOperationHours = roundMoney( + typedWorkOrderCosts.reduce( + (sum, workOrder) => sum + workOrder.operations.reduce((workOrderSum, operation) => workOrderSum + operation.plannedMinutes, 0), + 0 + ) / 60 + ); + const buildQuantity = typedWorkOrderCosts.reduce((sum, workOrder) => sum + workOrder.quantity, 0); + const completedBuildQuantity = typedWorkOrderCosts.reduce((sum, workOrder) => sum + workOrder.completedQuantity, 0); return { commercial: { @@ -459,6 +544,16 @@ async function buildProjectCockpit(record: ProjectRecord, rollups: ProjectRollup .sort((left, right) => new Date(right.receivedAt).getTime() - new Date(left.receivedAt).getTime()) .slice(0, 5), }, + costs: { + quotedRevenue: commercialQuoteTotal, + bookedRevenue: commercialOrderTotal, + linkedPurchaseCommitment: linkedLineValue, + plannedMaterialCost, + issuedMaterialCost, + plannedOperationHours, + buildQuantity, + completedBuildQuantity, + }, delivery: { shipmentNumber: typedShipment?.shipmentNumber ?? null, shipmentStatus: typedShipment?.status ?? null, diff --git a/shared/src/projects/types.ts b/shared/src/projects/types.ts index 766c817..3f465c9 100644 --- a/shared/src/projects/types.ts +++ b/shared/src/projects/types.ts @@ -129,6 +129,17 @@ export interface ProjectCockpitPurchasingDto { recentReceipts: ProjectCockpitReceiptDto[]; } +export interface ProjectCockpitCostDto { + quotedRevenue: number | null; + bookedRevenue: number | null; + linkedPurchaseCommitment: number; + plannedMaterialCost: number; + issuedMaterialCost: number; + plannedOperationHours: number; + buildQuantity: number; + completedBuildQuantity: number; +} + export interface ProjectCockpitDeliveryDto { shipmentNumber: string | null; shipmentStatus: string | null; @@ -154,6 +165,7 @@ export interface ProjectCockpitRiskDto { export interface ProjectCockpitDto { commercial: ProjectCockpitCommercialDto; purchasing: ProjectCockpitPurchasingDto; + costs: ProjectCockpitCostDto; delivery: ProjectCockpitDeliveryDto; risk: ProjectCockpitRiskDto; }