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:
+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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user