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
|
### 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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user