Add BOM cable length sub-grouping
Per organized-ideas.md §7: cables of the same type sharing the same length now get their own subtotal (count + total length + cost), revealed by expanding that cable type's row rather than only showing one blended total. Closes out the last open item in §7 — cost input, per-diagram override, and the grand total were already in place. - domain/bom.ts: computeBom groups each cable type's connections by distance too (a separate bucket for connections with no length entered), sorted shortest first. - BomPanel: a chevron toggles the breakdown, only shown when a cable type actually has more than one distinct length group (nothing to reveal otherwise). Verified: tsc -b and oxlint clean. No backend involved.
This commit is contained in:
+57
-2
@@ -1,6 +1,18 @@
|
||||
import { allCableTypes } from './project'
|
||||
import type { CableType, Catalog, Connection, Device, Project } from './types'
|
||||
|
||||
/** One length within a cable type's group — e.g. "3 × 10ft". `distance`
|
||||
* is undefined for the bucket of connections that have no length entered
|
||||
* (kept separate rather than folded into a 0ft bucket, same reasoning as
|
||||
* connectionsMissingLength on the parent line). */
|
||||
export interface BomCableLengthLine {
|
||||
distance: number | undefined
|
||||
count: number
|
||||
totalLength: number
|
||||
totalCost: number
|
||||
connectionsMissingCost: number
|
||||
}
|
||||
|
||||
export interface BomCableLine {
|
||||
cableTypeId: string
|
||||
cableTypeName: string
|
||||
@@ -14,6 +26,10 @@ export interface BomCableLine {
|
||||
* excluded rather than counted as zero. */
|
||||
totalCost: number
|
||||
connectionsMissingCost: number
|
||||
/** This type's connections grouped by length, most common use case for
|
||||
* "how many N-foot cables do I need to buy" — see organized-ideas.md §7.
|
||||
* Sorted shortest first, with the no-length-entered bucket (if any) last. */
|
||||
lengths: BomCableLengthLine[]
|
||||
}
|
||||
|
||||
export interface BomDeviceLine {
|
||||
@@ -70,6 +86,12 @@ export function computeBom(project: Project, catalog: Catalog): Bom {
|
||||
const cableTypes = new Map(allCableTypes(catalog).map((ct) => [ct.id, ct]))
|
||||
|
||||
const cableLines = new Map<string, BomCableLine>()
|
||||
// Nested by cableTypeId, then by distance (undefined key for "no length
|
||||
// entered") — a separate map from cableLines since a Map key can't itself
|
||||
// be a nested Map, and it's convenient to keep the length-line objects
|
||||
// addressable by (cableTypeId, distance) while building them up.
|
||||
const lengthLines = new Map<string, Map<number | undefined, BomCableLengthLine>>()
|
||||
|
||||
for (const connection of project.connections) {
|
||||
const cableType = cableTypes.get(connection.cableTypeId)
|
||||
const name = cableType?.name ?? 'Unknown Cable'
|
||||
@@ -83,10 +105,13 @@ export function computeBom(project: Project, catalog: Catalog): Bom {
|
||||
connectionsMissingLength: 0,
|
||||
totalCost: 0,
|
||||
connectionsMissingCost: 0,
|
||||
lengths: [],
|
||||
}
|
||||
existing.count += 1
|
||||
if (typeof connection.distance === 'number' && !Number.isNaN(connection.distance)) {
|
||||
existing.totalLength += connection.distance
|
||||
const distance =
|
||||
typeof connection.distance === 'number' && !Number.isNaN(connection.distance) ? connection.distance : undefined
|
||||
if (distance !== undefined) {
|
||||
existing.totalLength += distance
|
||||
} else {
|
||||
existing.connectionsMissingLength += 1
|
||||
}
|
||||
@@ -97,6 +122,36 @@ export function computeBom(project: Project, catalog: Catalog): Bom {
|
||||
existing.connectionsMissingCost += 1
|
||||
}
|
||||
cableLines.set(connection.cableTypeId, existing)
|
||||
|
||||
const byLength = lengthLines.get(connection.cableTypeId) ?? new Map<number | undefined, BomCableLengthLine>()
|
||||
const lengthLine = byLength.get(distance) ?? {
|
||||
distance,
|
||||
count: 0,
|
||||
totalLength: 0,
|
||||
totalCost: 0,
|
||||
connectionsMissingCost: 0,
|
||||
}
|
||||
lengthLine.count += 1
|
||||
if (distance !== undefined) lengthLine.totalLength += distance
|
||||
if (cost !== undefined) {
|
||||
lengthLine.totalCost += cost
|
||||
} else {
|
||||
lengthLine.connectionsMissingCost += 1
|
||||
}
|
||||
byLength.set(distance, lengthLine)
|
||||
lengthLines.set(connection.cableTypeId, byLength)
|
||||
}
|
||||
|
||||
for (const [cableTypeId, line] of cableLines) {
|
||||
const byLength = [...(lengthLines.get(cableTypeId)?.values() ?? [])]
|
||||
// Shortest first; the no-length-entered bucket (distance undefined)
|
||||
// always sorts last rather than mixing in as "smallest".
|
||||
byLength.sort((a, b) => {
|
||||
if (a.distance === undefined) return 1
|
||||
if (b.distance === undefined) return -1
|
||||
return a.distance - b.distance
|
||||
})
|
||||
line.lengths = byLength
|
||||
}
|
||||
|
||||
const deviceLines = new Map<string, BomDeviceLine>()
|
||||
|
||||
Reference in New Issue
Block a user