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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
This commit is contained in:
@@ -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 (
|
||||
<div className="space-y-4 p-3">
|
||||
<section>
|
||||
@@ -14,6 +21,13 @@ export default function BomPanel() {
|
||||
<p className="text-xs italic text-slate-400">No devices on the canvas yet.</p>
|
||||
) : (
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-left text-[10px] uppercase tracking-wide text-slate-400">
|
||||
<th className="pb-1 font-medium">Device</th>
|
||||
<th className="pb-1 text-right font-medium">Count</th>
|
||||
<th className="pb-1 text-right font-medium">Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bom.devices.map((line) => (
|
||||
<tr key={line.key} className="border-b border-slate-100 last:border-0">
|
||||
@@ -26,6 +40,14 @@ export default function BomPanel() {
|
||||
)}
|
||||
</td>
|
||||
<td className="py-1 text-right font-medium text-slate-600">×{line.count}</td>
|
||||
<td className="py-1 text-right text-slate-600">
|
||||
{line.totalCost > 0 ? formatCost(line.totalCost) : '—'}
|
||||
{line.devicesMissingCost > 0 && (
|
||||
<span className="ml-1 text-[10px] text-amber-500" title="Some devices are missing a cost">
|
||||
({line.devicesMissingCost} unset)
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -44,6 +66,7 @@ export default function BomPanel() {
|
||||
<th className="pb-1 font-medium">Cable</th>
|
||||
<th className="pb-1 text-right font-medium">Count</th>
|
||||
<th className="pb-1 text-right font-medium">Total length</th>
|
||||
<th className="pb-1 text-right font-medium">Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -59,12 +82,34 @@ export default function BomPanel() {
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-1 text-right text-slate-600">
|
||||
{line.totalCost > 0 ? formatCost(line.totalCost) : '—'}
|
||||
{line.connectionsMissingCost > 0 && (
|
||||
<span className="ml-1 text-[10px] text-amber-500" title="Some cables are missing a computable cost">
|
||||
({line.connectionsMissingCost} unset)
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{(bom.devices.length > 0 || bom.cables.length > 0) && (
|
||||
<section className="border-t border-slate-200 pt-2">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="font-semibold uppercase tracking-wide text-slate-500">Grand total</span>
|
||||
<span className="font-semibold text-slate-700">{formatCost(bom.grandTotalCost)}</span>
|
||||
</div>
|
||||
{hasMissingCost && (
|
||||
<p className="mt-0.5 text-[10px] text-amber-500">
|
||||
Some items are missing a cost and are excluded from this total.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user