From e8654a39d664cade2e568e894dee69f6d2ea2581 Mon Sep 17 00:00:00 2001 From: aarbit Date: Fri, 4 Sep 2026 15:55:57 -0500 Subject: [PATCH] Add BOM cost tracking Suggested-default cost on CableType/DeviceTemplate, copied onto placed Device instances (same pattern as ports) and overridable per-connection/per-device in the diagram. BOM view shows per-line and grand-total cost, excluding (and flagging) anything without a computable cost rather than treating it as zero. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x --- src/components/bom/BomPanel.tsx | 45 +++++++++++++ .../inspector/ConnectionInspector.tsx | 29 ++++++++ src/components/inspector/DeviceInspector.tsx | 19 +++++- src/components/palette/CableTypeManager.tsx | 49 ++++++++++++-- .../palette/DeviceTemplateEditor.tsx | 19 +++++- src/domain/bom.ts | 66 ++++++++++++++++++- src/domain/project.ts | 1 + src/domain/types.ts | 15 +++++ src/state/projectStore.ts | 10 ++- 9 files changed, 242 insertions(+), 11 deletions(-) diff --git a/src/components/bom/BomPanel.tsx b/src/components/bom/BomPanel.tsx index 67ad905..69cbce1 100644 --- a/src/components/bom/BomPanel.tsx +++ b/src/components/bom/BomPanel.tsx @@ -2,10 +2,17 @@ import { useMemo } from 'react' import { computeBom } from '../../domain/bom' import { useProjectStore } from '../../state/projectStore' +function formatCost(value: number): string { + return `$${value.toFixed(2)}` +} + export default function BomPanel() { const project = useProjectStore((s) => s.project) const bom = useMemo(() => computeBom(project), [project]) + const hasMissingCost = + bom.devices.some((line) => line.devicesMissingCost > 0) || bom.cables.some((line) => line.connectionsMissingCost > 0) + return (
@@ -14,6 +21,13 @@ export default function BomPanel() {

No devices on the canvas yet.

) : ( + + + + + + + {bom.devices.map((line) => ( @@ -26,6 +40,14 @@ export default function BomPanel() { )} + ))} @@ -44,6 +66,7 @@ export default function BomPanel() { + @@ -59,12 +82,34 @@ export default function BomPanel() { )} + ))}
DeviceCountCost
×{line.count} + {line.totalCost > 0 ? formatCost(line.totalCost) : '—'} + {line.devicesMissingCost > 0 && ( + + ({line.devicesMissingCost} unset) + + )} +
Cable Count Total lengthCost
+ {line.totalCost > 0 ? formatCost(line.totalCost) : '—'} + {line.connectionsMissingCost > 0 && ( + + ({line.connectionsMissingCost} unset) + + )} +
)}
+ + {(bom.devices.length > 0 || bom.cables.length > 0) && ( +
+
+ Grand total + {formatCost(bom.grandTotalCost)} +
+ {hasMissingCost && ( +

+ Some items are missing a cost and are excluded from this total. +

+ )} +
+ )}
) } diff --git a/src/components/inspector/ConnectionInspector.tsx b/src/components/inspector/ConnectionInspector.tsx index d1b66da..32aa33f 100644 --- a/src/components/inspector/ConnectionInspector.tsx +++ b/src/components/inspector/ConnectionInspector.tsx @@ -20,6 +20,8 @@ export default function ConnectionInspector({ connectionId }: { connectionId: st return sourcePortType && targetPortType ? cableTypesForConnection(project, sourcePortType, targetPortType) : [] }, [project, sourcePort, targetPort]) + const cableCostPerUnit = availableCableTypes.find((ct) => ct.id === connection?.cableTypeId)?.costPerUnit + if (!connection || !sourceDevice || !targetDevice || !sourcePort || !targetPort) return null return ( @@ -76,6 +78,33 @@ export default function ConnectionInspector({ connectionId }: { connectionId: st + + -