import { Fragment, useMemo, useState } from 'react' import { computeBom } from '../../domain/bom' import { useCatalogStore } from '../../state/catalogStore' import { useProjectStore } from '../../state/projectStore' import Chevron from '../common/Chevron' function formatCost(value: number): string { return `$${value.toFixed(2)}` } export default function BomPanel() { const project = useProjectStore((s) => s.project) const catalog = useCatalogStore((s) => s.catalog) const bom = useMemo(() => computeBom(project, catalog), [project, catalog]) // Collapsed by default, per cable type — most diagrams only need the // per-type totals; the length breakdown is there when you're ready to // actually go buy cables. const [expandedCableTypeIds, setExpandedCableTypeIds] = useState>(new Set()) const toggleCableType = (cableTypeId: string) => { setExpandedCableTypeIds((prev) => { const next = new Set(prev) if (next.has(cableTypeId)) next.delete(cableTypeId) else next.add(cableTypeId) return next }) } const hasMissingCost = bom.devices.some((line) => line.devicesMissingCost > 0) || bom.cables.some((line) => line.connectionsMissingCost > 0) return (

Devices

{bom.devices.length === 0 ? (

No devices on the canvas yet.

) : ( {bom.devices.map((line) => ( ))}
Device Count Cost
{line.name}
{(line.manufacturer || line.model) && (
{[line.manufacturer, line.model].filter(Boolean).join(' ')}
)}
×{line.count} {line.totalCost > 0 ? formatCost(line.totalCost) : '—'} {line.devicesMissingCost > 0 && ( ({line.devicesMissingCost} unset) )}
)}

Cables

{bom.cables.length === 0 ? (

No cables wired up yet.

) : ( {bom.cables.map((line) => { const expandable = line.lengths.length > 1 const expanded = expandable && expandedCableTypeIds.has(line.cableTypeId) return ( {expanded && line.lengths.map((lengthLine) => ( ))} ) })}
Cable Count Total length Cost
{expandable ? ( ) : ( {line.cableTypeName} )} ×{line.count} {line.totalLength > 0 ? `${line.totalLength}${line.unit}` : '—'} {line.connectionsMissingLength > 0 && ( ({line.connectionsMissingLength} unset) )} {line.totalCost > 0 ? formatCost(line.totalCost) : '—'} {line.connectionsMissingCost > 0 && ( ({line.connectionsMissingCost} unset) )}
{lengthLine.distance !== undefined ? `${lengthLine.distance}${line.unit}` : 'No length set'} ×{lengthLine.count} {lengthLine.totalLength > 0 ? `${lengthLine.totalLength}${line.unit}` : '—'} {lengthLine.totalCost > 0 ? formatCost(lengthLine.totalCost) : '—'} {lengthLine.connectionsMissingCost > 0 && ( ({lengthLine.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.

)}
)}
) }