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:
2026-09-04 15:55:57 -05:00
co-authored by Claude Sonnet 5
parent 7cf786e107
commit e8654a39d6
9 changed files with 242 additions and 11 deletions
@@ -45,6 +45,7 @@ export default function DeviceTemplateEditor({
const [category, setCategory] = useState<string>(template?.category ?? 'other')
const [manufacturer, setManufacturer] = useState(template?.manufacturer ?? '')
const [model, setModel] = useState(template?.model ?? '')
const [cost, setCost] = useState(template?.cost?.toString() ?? '')
const [ports, setPorts] = useState<DraftPort[]>(
() =>
template?.ports.map((p) => ({ key: p.id, name: p.name, direction: p.direction, portTypeId: p.portTypeId })) ?? [
@@ -120,6 +121,7 @@ export default function DeviceTemplateEditor({
category,
manufacturer: manufacturer.trim() || undefined,
model: model.trim() || undefined,
cost: cost.trim() === '' ? undefined : Number(cost),
ports: ports.map((p) => ({ id: p.key, name: p.name.trim() || 'Port', direction: p.direction, portTypeId: p.portTypeId })),
}
if (template) {
@@ -156,7 +158,7 @@ export default function DeviceTemplateEditor({
className="mt-1 w-full rounded border border-slate-300 px-2 py-1 text-sm"
/>
</label>
<label className="col-span-2 text-xs font-medium text-slate-600">
<label className="text-xs font-medium text-slate-600">
Model
<input
value={model}
@@ -164,6 +166,21 @@ export default function DeviceTemplateEditor({
className="mt-1 w-full rounded border border-slate-300 px-2 py-1 text-sm"
/>
</label>
<label className="text-xs font-medium text-slate-600">
Suggested cost
<div className="mt-1 flex items-center gap-1">
<span className="text-xs text-slate-500">$</span>
<input
type="number"
min={0}
step={0.01}
value={cost}
onChange={(e) => setCost(e.target.value)}
className="w-full rounded border border-slate-300 px-2 py-1 text-sm"
placeholder="e.g. 249.00"
/>
</div>
</label>
</div>
<div>