cost rollups

This commit is contained in:
2026-03-17 19:17:12 -05:00
parent f772ccacc7
commit cdbd54b8cc
6 changed files with 116 additions and 3 deletions

View File

@@ -6,7 +6,7 @@ This file is the running release and change log for CODEXIUM. Keep it updated wh
### Added ### 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 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 - 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 - Inventory SKU master builder with family-level sequence codes, branch-aware taxonomy management, and generated SKU previews on the item form

View File

@@ -91,7 +91,7 @@ Navigation direction:
## Projects 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: Current interactions:

View File

@@ -34,7 +34,7 @@ This file tracks roadmap phases, slices, and major foundations that have already
- Logistics attachments directly on shipment records - Logistics attachments directly on shipment records
- Projects foundation with customer, quote, sales-order, shipment, owner, due-date, notes, and attachment linkage - 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 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 - 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 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 - Manufacturing stations, item routing templates, and automatic work-order operation planning for gantt scheduling

View File

@@ -159,6 +159,12 @@ export function ProjectDetailPage() {
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Readiness Score</p><div className={`mt-2 text-base font-bold ${riskTone}`}>{readinessScore}%</div><div className="mt-1 text-xs text-muted">{project.cockpit.risk.riskLevel} risk - {project.cockpit.risk.shortageItemCount} shortage item(s)</div></article> <article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Readiness Score</p><div className={`mt-2 text-base font-bold ${riskTone}`}>{readinessScore}%</div><div className="mt-1 text-xs text-muted">{project.cockpit.risk.riskLevel} risk - {project.cockpit.risk.shortageItemCount} shortage item(s)</div></article>
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Material Spend</p><div className="mt-2 text-base font-bold text-text">${project.cockpit.purchasing.linkedLineValue.toFixed(2)}</div><div className="mt-1 text-xs text-muted">{project.cockpit.purchasing.vendorCount} vendor(s) across {project.cockpit.purchasing.linkedLineCount} linked line(s)</div></article> <article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Material Spend</p><div className="mt-2 text-base font-bold text-text">${project.cockpit.purchasing.linkedLineValue.toFixed(2)}</div><div className="mt-1 text-xs text-muted">{project.cockpit.purchasing.vendorCount} vendor(s) across {project.cockpit.purchasing.linkedLineCount} linked line(s)</div></article>
</div> </div>
<div className="mt-5 grid gap-3 xl:grid-cols-4">
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Booked Revenue</p><div className="mt-2 text-base font-bold text-text">{formatCurrency(project.cockpit.costs.bookedRevenue)}</div><div className="mt-1 text-xs text-muted">Quoted baseline {formatCurrency(project.cockpit.costs.quotedRevenue)}</div></article>
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Purchase Commitment</p><div className="mt-2 text-base font-bold text-text">${project.cockpit.costs.linkedPurchaseCommitment.toFixed(2)}</div><div className="mt-1 text-xs text-muted">Linked PO line value already committed</div></article>
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Planned Material Cost</p><div className="mt-2 text-base font-bold text-text">${project.cockpit.costs.plannedMaterialCost.toFixed(2)}</div><div className="mt-1 text-xs text-muted">Issued so far ${project.cockpit.costs.issuedMaterialCost.toFixed(2)}</div></article>
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"><p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Build Load</p><div className="mt-2 text-base font-bold text-text">{project.cockpit.costs.completedBuildQuantity}/{project.cockpit.costs.buildQuantity}</div><div className="mt-1 text-xs text-muted">{project.cockpit.costs.plannedOperationHours.toFixed(1)} planned operation hours</div></article>
</div>
<div className="mt-5 grid gap-3 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]"> <div className="mt-5 grid gap-3 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3"> <article className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Next Checkpoints</p> <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Next Checkpoints</p>

View File

@@ -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) { function roundMoney(value: number) {
return Math.round(value * 100) / 100; return Math.round(value * 100) / 100;
} }
@@ -327,6 +349,44 @@ async function buildProjectCockpit(record: ProjectRecord, rollups: ProjectRollup
: Promise.resolve([]), : Promise.resolve([]),
record.salesOrder ? getSalesOrderPlanningById(record.salesOrder.id) : Promise.resolve(null), 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 typedPurchaseOrders = purchaseOrders as ProjectPurchaseOrderRecord[];
const typedReceiptLines = receiptLines as ProjectReceiptLineRecord[]; const typedReceiptLines = receiptLines as ProjectReceiptLineRecord[];
@@ -334,6 +394,7 @@ async function buildProjectCockpit(record: ProjectRecord, rollups: ProjectRollup
const typedSalesOrder = salesOrder as ProjectSalesDocumentRecord | null; const typedSalesOrder = salesOrder as ProjectSalesDocumentRecord | null;
const typedShipment = shipment as ProjectShipmentRecord | null; const typedShipment = shipment as ProjectShipmentRecord | null;
const typedPlanning = planning as SalesOrderPlanningDto | null; const typedPlanning = planning as SalesOrderPlanningDto | null;
const typedWorkOrderCosts = workOrderCosts as ProjectCostWorkOrderRecord[];
const purchaseOrdersSummary: ProjectCockpitPurchaseOrderDto[] = typedPurchaseOrders.map((purchaseOrder) => { const purchaseOrdersSummary: ProjectCockpitPurchaseOrderDto[] = typedPurchaseOrders.map((purchaseOrder) => {
const linkedLineCount = purchaseOrder.lines.length; const linkedLineCount = purchaseOrder.lines.length;
@@ -427,6 +488,30 @@ async function buildProjectCockpit(record: ProjectRecord, rollups: ProjectRollup
const commercialQuoteTotal = typedQuote ? calculateSalesDocumentTotal(typedQuote) : null; const commercialQuoteTotal = typedQuote ? calculateSalesDocumentTotal(typedQuote) : null;
const commercialOrderTotal = typedSalesOrder ? calculateSalesDocumentTotal(typedSalesOrder) : 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 { return {
commercial: { 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()) .sort((left, right) => new Date(right.receivedAt).getTime() - new Date(left.receivedAt).getTime())
.slice(0, 5), .slice(0, 5),
}, },
costs: {
quotedRevenue: commercialQuoteTotal,
bookedRevenue: commercialOrderTotal,
linkedPurchaseCommitment: linkedLineValue,
plannedMaterialCost,
issuedMaterialCost,
plannedOperationHours,
buildQuantity,
completedBuildQuantity,
},
delivery: { delivery: {
shipmentNumber: typedShipment?.shipmentNumber ?? null, shipmentNumber: typedShipment?.shipmentNumber ?? null,
shipmentStatus: typedShipment?.status ?? null, shipmentStatus: typedShipment?.status ?? null,

View File

@@ -129,6 +129,17 @@ export interface ProjectCockpitPurchasingDto {
recentReceipts: ProjectCockpitReceiptDto[]; 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 { export interface ProjectCockpitDeliveryDto {
shipmentNumber: string | null; shipmentNumber: string | null;
shipmentStatus: string | null; shipmentStatus: string | null;
@@ -154,6 +165,7 @@ export interface ProjectCockpitRiskDto {
export interface ProjectCockpitDto { export interface ProjectCockpitDto {
commercial: ProjectCockpitCommercialDto; commercial: ProjectCockpitCommercialDto;
purchasing: ProjectCockpitPurchasingDto; purchasing: ProjectCockpitPurchasingDto;
costs: ProjectCockpitCostDto;
delivery: ProjectCockpitDeliveryDto; delivery: ProjectCockpitDeliveryDto;
risk: ProjectCockpitRiskDto; risk: ProjectCockpitRiskDto;
} }