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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="block text-xs font-medium text-slate-600">
|
||||
Cost override
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500">$</span>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
step={0.01}
|
||||
value={connection.cost ?? ''}
|
||||
onChange={(e) =>
|
||||
updateConnection(connection.id, {
|
||||
cost: e.target.value === '' ? undefined : Number(e.target.value),
|
||||
})
|
||||
}
|
||||
className="w-full rounded border border-slate-300 px-2 py-1 text-sm"
|
||||
placeholder={
|
||||
cableCostPerUnit !== undefined && connection.distance
|
||||
? `computed: $${(cableCostPerUnit * connection.distance).toFixed(2)}`
|
||||
: 'e.g. 24.99'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-1 text-[10px] text-slate-400">
|
||||
Overrides the cable type's per-unit cost for this run specifically. Leave blank to use cost/unit × distance.
|
||||
</p>
|
||||
</label>
|
||||
|
||||
<label className="block text-xs font-medium text-slate-600">
|
||||
Label
|
||||
<input
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function DeviceInspector({ deviceId }: { deviceId: string }) {
|
||||
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={device.model ?? ''}
|
||||
@@ -57,6 +57,23 @@ export default function DeviceInspector({ deviceId }: { deviceId: string }) {
|
||||
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">
|
||||
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={device.cost ?? ''}
|
||||
onChange={(e) =>
|
||||
updateDevice(device.id, { cost: e.target.value === '' ? undefined : Number(e.target.value) })
|
||||
}
|
||||
className="w-full rounded border border-slate-300 px-2 py-1 text-sm"
|
||||
placeholder="e.g. 249.00"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<label className="col-span-2 text-xs font-medium text-slate-600">
|
||||
Notes
|
||||
<textarea
|
||||
|
||||
@@ -21,7 +21,13 @@ function CableTypeForm({
|
||||
initial?: CableType
|
||||
portTypes: PortType[]
|
||||
onCancel: () => void
|
||||
onSave: (fields: { name: string; endAPortTypeId: string; endBPortTypeId: string; unit: CableType['unit'] }) => void
|
||||
onSave: (fields: {
|
||||
name: string
|
||||
endAPortTypeId: string
|
||||
endBPortTypeId: string
|
||||
unit: CableType['unit']
|
||||
costPerUnit?: number
|
||||
}) => void
|
||||
}) {
|
||||
const initialEndA = initial ? portTypes.find((pt) => pt.family === initial.family) : undefined
|
||||
const initialEndB = initial ? portTypes.find((pt) => pt.family === (initial.family2 ?? initial.family)) : undefined
|
||||
@@ -30,12 +36,19 @@ function CableTypeForm({
|
||||
const [endAPortTypeId, setEndAPortTypeId] = useState(initialEndA?.id ?? portTypes[0]?.id ?? '')
|
||||
const [endBPortTypeId, setEndBPortTypeId] = useState(initialEndB?.id ?? portTypes[0]?.id ?? '')
|
||||
const [unit, setUnit] = useState<CableType['unit']>(initial?.unit ?? 'ft')
|
||||
const [costPerUnit, setCostPerUnit] = useState(initial?.costPerUnit?.toString() ?? '')
|
||||
|
||||
const isAdapter = endAPortTypeId !== endBPortTypeId
|
||||
|
||||
const handleSave = () => {
|
||||
if (!name.trim() || !endAPortTypeId || !endBPortTypeId) return
|
||||
onSave({ name: name.trim(), endAPortTypeId, endBPortTypeId, unit })
|
||||
onSave({
|
||||
name: name.trim(),
|
||||
endAPortTypeId,
|
||||
endBPortTypeId,
|
||||
unit,
|
||||
costPerUnit: costPerUnit.trim() === '' ? undefined : Number(costPerUnit),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -87,6 +100,19 @@ function CableTypeForm({
|
||||
? 'Different connector on each end — this becomes an adapter cable, letting those two port types wire together even if nothing else declares them compatible.'
|
||||
: 'Same connector on each end — a regular cable for that connector type.'}
|
||||
</p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="shrink-0 text-[10px] font-medium text-slate-500">Suggested cost/{unit}</span>
|
||||
<span className="text-xs text-slate-400">$</span>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
step={0.01}
|
||||
value={costPerUnit}
|
||||
onChange={(e) => setCostPerUnit(e.target.value)}
|
||||
placeholder="optional"
|
||||
className="w-0 min-w-0 flex-1 rounded border border-slate-300 px-1.5 py-1 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button onClick={onCancel} className="text-[11px] text-slate-500">
|
||||
Cancel
|
||||
@@ -126,7 +152,13 @@ export default function CableTypesPanel() {
|
||||
return a === b ? a : `${a} ↔ ${b}`
|
||||
}
|
||||
|
||||
const handleCreate = (fields: { name: string; endAPortTypeId: string; endBPortTypeId: string; unit: CableType['unit'] }) => {
|
||||
const handleCreate = (fields: {
|
||||
name: string
|
||||
endAPortTypeId: string
|
||||
endBPortTypeId: string
|
||||
unit: CableType['unit']
|
||||
costPerUnit?: number
|
||||
}) => {
|
||||
const endA = portTypes.find((pt) => pt.id === fields.endAPortTypeId)
|
||||
const endB = portTypes.find((pt) => pt.id === fields.endBPortTypeId)
|
||||
if (!endA || !endB) return
|
||||
@@ -135,13 +167,20 @@ export default function CableTypesPanel() {
|
||||
family: endA.family,
|
||||
family2: endA.family === endB.family ? undefined : endB.family,
|
||||
unit: fields.unit,
|
||||
costPerUnit: fields.costPerUnit,
|
||||
})
|
||||
setCreating(false)
|
||||
}
|
||||
|
||||
const handleUpdate = (
|
||||
id: string,
|
||||
fields: { name: string; endAPortTypeId: string; endBPortTypeId: string; unit: CableType['unit'] },
|
||||
fields: {
|
||||
name: string
|
||||
endAPortTypeId: string
|
||||
endBPortTypeId: string
|
||||
unit: CableType['unit']
|
||||
costPerUnit?: number
|
||||
},
|
||||
) => {
|
||||
const endA = portTypes.find((pt) => pt.id === fields.endAPortTypeId)
|
||||
const endB = portTypes.find((pt) => pt.id === fields.endBPortTypeId)
|
||||
@@ -151,6 +190,7 @@ export default function CableTypesPanel() {
|
||||
family: endA.family,
|
||||
family2: endA.family === endB.family ? undefined : endB.family,
|
||||
unit: fields.unit,
|
||||
costPerUnit: fields.costPerUnit,
|
||||
})
|
||||
setEditingId(null)
|
||||
}
|
||||
@@ -197,6 +237,7 @@ export default function CableTypesPanel() {
|
||||
<div className="font-medium text-slate-700">{ct.name}</div>
|
||||
<div className="truncate text-[10px] text-slate-400">
|
||||
{describeEnds(ct)} · {ct.unit}
|
||||
{typeof ct.costPerUnit === 'number' && ` · $${ct.costPerUnit.toFixed(2)}/${ct.unit}`}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -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>
|
||||
|
||||
+63
-3
@@ -1,5 +1,5 @@
|
||||
import { allCableTypes } from './project'
|
||||
import type { Device, Project } from './types'
|
||||
import type { CableType, Connection, Device, Project } from './types'
|
||||
|
||||
export interface BomCableLine {
|
||||
cableTypeId: string
|
||||
@@ -9,6 +9,11 @@ export interface BomCableLine {
|
||||
/** Sum of user-entered distances; connections with no distance are excluded. */
|
||||
totalLength: number
|
||||
connectionsMissingLength: number
|
||||
/** Sum of each connection's cost (its own override, or `costPerUnit *
|
||||
* distance` when both are known); connections with no computable cost are
|
||||
* excluded rather than counted as zero. */
|
||||
totalCost: number
|
||||
connectionsMissingCost: number
|
||||
}
|
||||
|
||||
export interface BomDeviceLine {
|
||||
@@ -18,17 +23,49 @@ export interface BomDeviceLine {
|
||||
manufacturer?: string
|
||||
model?: string
|
||||
count: number
|
||||
/** Sum of `device.cost` across instances; devices with no cost set are
|
||||
* excluded rather than counted as zero. */
|
||||
totalCost: number
|
||||
devicesMissingCost: number
|
||||
}
|
||||
|
||||
export interface Bom {
|
||||
cables: BomCableLine[]
|
||||
devices: BomDeviceLine[]
|
||||
/** Sum of all cable line totals (missing-cost connections already excluded). */
|
||||
totalCableCost: number
|
||||
/** Sum of all device line totals (missing-cost devices already excluded). */
|
||||
totalDeviceCost: number
|
||||
/** `totalCableCost + totalDeviceCost`. Missing costs are excluded, not
|
||||
* treated as zero, so this is a lower bound whenever anything is flagged
|
||||
* as missing below. */
|
||||
grandTotalCost: number
|
||||
}
|
||||
|
||||
function deviceGroupKey(device: Device): string {
|
||||
return device.templateId ?? `${device.category}:${device.manufacturer ?? ''}:${device.model ?? device.name}`
|
||||
}
|
||||
|
||||
/** A connection's cost for BOM purposes: its own override if set, else
|
||||
* `costPerUnit * distance` if both the cable type's rate and the
|
||||
* connection's distance are known. Undefined means "not computable" —
|
||||
* callers must exclude it from totals rather than treating it as zero. */
|
||||
function connectionCost(connection: Connection, cableType: CableType | undefined): number | undefined {
|
||||
if (typeof connection.cost === 'number' && !Number.isNaN(connection.cost)) {
|
||||
return connection.cost
|
||||
}
|
||||
if (
|
||||
cableType &&
|
||||
typeof cableType.costPerUnit === 'number' &&
|
||||
!Number.isNaN(cableType.costPerUnit) &&
|
||||
typeof connection.distance === 'number' &&
|
||||
!Number.isNaN(connection.distance)
|
||||
) {
|
||||
return cableType.costPerUnit * connection.distance
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function computeBom(project: Project): Bom {
|
||||
const cableTypes = new Map(allCableTypes(project).map((ct) => [ct.id, ct]))
|
||||
|
||||
@@ -44,6 +81,8 @@ export function computeBom(project: Project): Bom {
|
||||
count: 0,
|
||||
totalLength: 0,
|
||||
connectionsMissingLength: 0,
|
||||
totalCost: 0,
|
||||
connectionsMissingCost: 0,
|
||||
}
|
||||
existing.count += 1
|
||||
if (typeof connection.distance === 'number' && !Number.isNaN(connection.distance)) {
|
||||
@@ -51,6 +90,12 @@ export function computeBom(project: Project): Bom {
|
||||
} else {
|
||||
existing.connectionsMissingLength += 1
|
||||
}
|
||||
const cost = connectionCost(connection, cableType)
|
||||
if (cost !== undefined) {
|
||||
existing.totalCost += cost
|
||||
} else {
|
||||
existing.connectionsMissingCost += 1
|
||||
}
|
||||
cableLines.set(connection.cableTypeId, existing)
|
||||
}
|
||||
|
||||
@@ -64,13 +109,28 @@ export function computeBom(project: Project): Bom {
|
||||
manufacturer: device.manufacturer,
|
||||
model: device.model,
|
||||
count: 0,
|
||||
totalCost: 0,
|
||||
devicesMissingCost: 0,
|
||||
}
|
||||
existing.count += 1
|
||||
if (typeof device.cost === 'number' && !Number.isNaN(device.cost)) {
|
||||
existing.totalCost += device.cost
|
||||
} else {
|
||||
existing.devicesMissingCost += 1
|
||||
}
|
||||
deviceLines.set(key, existing)
|
||||
}
|
||||
|
||||
const cables = [...cableLines.values()].sort((a, b) => a.cableTypeName.localeCompare(b.cableTypeName))
|
||||
const devices = [...deviceLines.values()].sort((a, b) => a.name.localeCompare(b.name))
|
||||
const totalCableCost = cables.reduce((sum, line) => sum + line.totalCost, 0)
|
||||
const totalDeviceCost = devices.reduce((sum, line) => sum + line.totalCost, 0)
|
||||
|
||||
return {
|
||||
cables: [...cableLines.values()].sort((a, b) => a.cableTypeName.localeCompare(b.cableTypeName)),
|
||||
devices: [...deviceLines.values()].sort((a, b) => a.name.localeCompare(b.name)),
|
||||
cables,
|
||||
devices,
|
||||
totalCableCost,
|
||||
totalDeviceCost,
|
||||
grandTotalCost: totalCableCost + totalDeviceCost,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +172,7 @@ export function createDeviceFromTemplate(
|
||||
manufacturer: template.manufacturer,
|
||||
model: template.model,
|
||||
ports: template.ports.map((port) => ({ ...port, id: uuid() })),
|
||||
cost: template.cost,
|
||||
position,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,9 @@ export interface DeviceTemplate {
|
||||
manufacturer?: string
|
||||
model?: string
|
||||
ports: Port[]
|
||||
/** Suggested default cost, copied onto each placed Device at creation time
|
||||
* (same as ports) and independently editable per instance afterward. */
|
||||
cost?: number
|
||||
builtIn?: boolean
|
||||
}
|
||||
|
||||
@@ -77,6 +80,10 @@ export interface Device {
|
||||
model?: string
|
||||
notes?: string
|
||||
ports: Port[]
|
||||
/** Copied from the template's suggested cost when placed (like ports),
|
||||
* then independently editable — editing an instance never mutates the
|
||||
* shared template. */
|
||||
cost?: number
|
||||
position: { x: number; y: number }
|
||||
}
|
||||
|
||||
@@ -95,6 +102,9 @@ export interface CableType {
|
||||
* not just where this particular cable is actually used). */
|
||||
family2?: string
|
||||
unit: 'ft' | 'm'
|
||||
/** Suggested default cost per unit length (in `unit`). Each Connection can
|
||||
* override the computed cost for its specific run (`Connection.cost`),
|
||||
* which always wins — public catalog prices drift and vary by vendor. */
|
||||
costPerUnit?: number
|
||||
custom?: boolean
|
||||
}
|
||||
@@ -110,6 +120,11 @@ export interface Connection {
|
||||
/** User-entered run length, in the CableType's unit. Optional because
|
||||
* real-world layout isn't modeled (no floor plan / scale). */
|
||||
distance?: number
|
||||
/** Override for this specific run's total cost — takes precedence over
|
||||
* computing from the CableType's `costPerUnit * distance`, since a
|
||||
* diagram-level price should always win over the library's suggested
|
||||
* default. */
|
||||
cost?: number
|
||||
label?: string
|
||||
notes?: string
|
||||
}
|
||||
|
||||
@@ -28,7 +28,10 @@ interface ProjectStoreState {
|
||||
|
||||
addDeviceFromTemplate: (templateId: string, position: { x: number; y: number }) => void
|
||||
updateDevicePosition: (id: string, position: { x: number; y: number }) => void
|
||||
updateDevice: (id: string, patch: Partial<Pick<Device, 'name' | 'category' | 'manufacturer' | 'model' | 'notes'>>) => void
|
||||
updateDevice: (
|
||||
id: string,
|
||||
patch: Partial<Pick<Device, 'name' | 'category' | 'manufacturer' | 'model' | 'notes' | 'cost'>>,
|
||||
) => void
|
||||
removeDevice: (id: string) => void
|
||||
|
||||
/** Returns the new port's id, so the caller can move keyboard focus to it. */
|
||||
@@ -46,7 +49,10 @@ interface ProjectStoreState {
|
||||
targetPortId: string
|
||||
cableTypeId: string
|
||||
}) => boolean
|
||||
updateConnection: (id: string, patch: Partial<Pick<Connection, 'cableTypeId' | 'distance' | 'label' | 'notes'>>) => void
|
||||
updateConnection: (
|
||||
id: string,
|
||||
patch: Partial<Pick<Connection, 'cableTypeId' | 'distance' | 'cost' | 'label' | 'notes'>>,
|
||||
) => void
|
||||
removeConnection: (id: string) => void
|
||||
|
||||
addCustomDeviceTemplate: (template: Omit<DeviceTemplate, 'id' | 'builtIn'>) => void
|
||||
|
||||
Reference in New Issue
Block a user