cost rollups
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">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 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)]">
|
||||
<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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user