Add manufacturer catalog, category/manufacturer library management, and built-in cable flag
Normalizes manufacturer as a shared catalog entity (like device categories) instead of free text on each device, giving the admin duplicate-detection nudge a reliable signal. Adds full CRUD (including Admin direct-publish, bypassing the submission queue) for categories, manufacturers, port types, and cable types, plus a Categories & Manufacturers library modal and a browse-by-manufacturer/search view in the device palette. Adds Port.builtInCable so a captive/permanently-attached cable (a keyboard's USB lead, a budget AVR's power cord) can be flagged and excluded from the BOM. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
This commit is contained in:
+24
-3
@@ -1,4 +1,4 @@
|
||||
import { allCableTypes } from './diagram'
|
||||
import { allCableTypes, manufacturerName } from './diagram'
|
||||
import type { CableType, Catalog, Connection, Device, Diagram } from './types'
|
||||
|
||||
/** One length within a cable type's group — e.g. "3 × 10ft". `distance`
|
||||
@@ -59,7 +59,7 @@ export interface Bom {
|
||||
}
|
||||
|
||||
function deviceGroupKey(device: Device): string {
|
||||
return device.templateId ?? `${device.category}:${device.manufacturer ?? ''}:${device.model ?? device.name}`
|
||||
return device.templateId ?? `${device.category}:${device.manufacturerId ?? ''}:${device.model ?? device.name}`
|
||||
}
|
||||
|
||||
/** A connection's cost for BOM purposes: its own override if set, else
|
||||
@@ -82,8 +82,25 @@ function connectionCost(connection: Connection, cableType: CableType | undefined
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** Every port id, across all devices, with a built-in/captive cable — a
|
||||
* connection touching either end of one doesn't need a cable purchased
|
||||
* (organized-ideas.md-adjacent decision: the cable's already paid for as
|
||||
* part of the device, like a keyboard's USB cable or a budget AVR's power
|
||||
* cord). Built once per computeBom call rather than searched per
|
||||
* connection. */
|
||||
function builtInPortIds(devices: Device[]): Set<string> {
|
||||
const ids = new Set<string>()
|
||||
for (const device of devices) {
|
||||
for (const port of device.ports) {
|
||||
if (port.builtInCable) ids.add(port.id)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
export function computeBom(diagram: Diagram, catalog: Catalog): Bom {
|
||||
const cableTypes = new Map(allCableTypes(catalog).map((ct) => [ct.id, ct]))
|
||||
const builtIn = builtInPortIds(diagram.devices)
|
||||
|
||||
const cableLines = new Map<string, BomCableLine>()
|
||||
// Nested by cableTypeId, then by distance (undefined key for "no length
|
||||
@@ -93,6 +110,10 @@ export function computeBom(diagram: Diagram, catalog: Catalog): Bom {
|
||||
const lengthLines = new Map<string, Map<number | undefined, BomCableLengthLine>>()
|
||||
|
||||
for (const connection of diagram.connections) {
|
||||
// A built-in cable is already paid for as part of the device — no line
|
||||
// for it here, not even a "missing cost" note; the canvas is where this
|
||||
// connection's built-in-ness is visible, not the shopping list.
|
||||
if (builtIn.has(connection.sourcePortId) || builtIn.has(connection.targetPortId)) continue
|
||||
const cableType = cableTypes.get(connection.cableTypeId)
|
||||
const name = cableType?.name ?? 'Unknown Cable'
|
||||
const unit = cableType?.unit ?? 'ft'
|
||||
@@ -161,7 +182,7 @@ export function computeBom(diagram: Diagram, catalog: Catalog): Bom {
|
||||
key,
|
||||
name: device.name,
|
||||
category: device.category,
|
||||
manufacturer: device.manufacturer,
|
||||
manufacturer: manufacturerName(catalog, device.manufacturerId),
|
||||
model: device.model,
|
||||
count: 0,
|
||||
totalCost: 0,
|
||||
|
||||
+26
-2
@@ -1,5 +1,5 @@
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import type { CableType, Catalog, Device, DeviceCategoryDef, Diagram, DeviceTemplate, PortType } from './types'
|
||||
import type { CableType, Catalog, Device, DeviceCategoryDef, Diagram, DeviceTemplate, Manufacturer, PortType } from './types'
|
||||
|
||||
export function allPortTypes(catalog: Catalog): PortType[] {
|
||||
return catalog.portTypes
|
||||
@@ -35,6 +35,22 @@ export function deviceCategoryName(catalog: Catalog, categoryId: string): string
|
||||
return getDeviceCategory(catalog, categoryId)?.name ?? categoryId
|
||||
}
|
||||
|
||||
export function allManufacturers(catalog: Catalog): Manufacturer[] {
|
||||
return catalog.manufacturers
|
||||
}
|
||||
|
||||
export function getManufacturer(catalog: Catalog, manufacturerId: string): Manufacturer | undefined {
|
||||
return allManufacturers(catalog).find((m) => m.id === manufacturerId)
|
||||
}
|
||||
|
||||
/** Friendly name for a manufacturer id — unlike `deviceCategoryName`,
|
||||
* `manufacturerId` is optional (not every device has a known manufacturer),
|
||||
* so this returns undefined rather than a fallback string when it's unset
|
||||
* or, for older data, references a manufacturer that no longer exists. */
|
||||
export function manufacturerName(catalog: Catalog, manufacturerId: string | undefined): string | undefined {
|
||||
return manufacturerId ? getManufacturer(catalog, manufacturerId)?.name : undefined
|
||||
}
|
||||
|
||||
export function getPortType(catalog: Catalog, portTypeId: string): PortType | undefined {
|
||||
return allPortTypes(catalog).find((pt) => pt.id === portTypeId)
|
||||
}
|
||||
@@ -43,6 +59,14 @@ export function getCableType(catalog: Catalog, cableTypeId: string): CableType |
|
||||
return allCableTypes(catalog).find((ct) => ct.id === cableTypeId)
|
||||
}
|
||||
|
||||
/** Best-effort friendly label for a raw family string: the name of whichever
|
||||
* port type owns that family, or the raw string itself if none matches
|
||||
* (e.g. a family that only ever existed as hand-typed text, from before
|
||||
* FamilySelect started sourcing new families from an existing port type). */
|
||||
export function familyLabel(portTypes: PortType[], family: string): string {
|
||||
return portTypes.find((pt) => pt.family === family)?.name ?? family
|
||||
}
|
||||
|
||||
/** The set of connector families a port type will mate with: its own family
|
||||
* plus anything it explicitly lists as also-compatible. Shared with
|
||||
* `compatibility.ts` so "does A connect to B" and "which cable fits" agree. */
|
||||
@@ -145,7 +169,7 @@ export function createDeviceFromTemplate(template: DeviceTemplate, position: { x
|
||||
name: template.name,
|
||||
templateId: template.id,
|
||||
category: template.category,
|
||||
manufacturer: template.manufacturer,
|
||||
manufacturerId: template.manufacturerId,
|
||||
model: template.model,
|
||||
ports: template.ports.map((port) => ({ ...port, id: uuid() })),
|
||||
cost: template.cost,
|
||||
|
||||
+25
-2
@@ -28,6 +28,16 @@ export interface DeviceCategoryDef {
|
||||
custom?: boolean
|
||||
}
|
||||
|
||||
/** A device manufacturer/brand, normalized the same way as DeviceCategoryDef
|
||||
* (organized-ideas.md §3) rather than free text on each device — gives the
|
||||
* Admin review queue's duplicate-detection nudge a reliable signal
|
||||
* (matching manufacturer + similar model) that name-matching alone can't. */
|
||||
export interface Manufacturer {
|
||||
id: string
|
||||
name: string
|
||||
custom?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A kind of physical connector (HDMI, XLR, Cat6/RJ45, ...). Compatibility
|
||||
* between two ports is decided by comparing `family` (plus optional
|
||||
@@ -54,6 +64,14 @@ export interface Port {
|
||||
name: string
|
||||
direction: PortDirection
|
||||
portTypeId: string
|
||||
/** This port already has a captive/permanently-attached cable (a
|
||||
* keyboard's USB cable, a budget AV receiver's power cord, ...) — a
|
||||
* property of the physical port, not of any one wiring choice, so it
|
||||
* lives here rather than on Connection. Any connection touching a port
|
||||
* with this set doesn't need a separate purchased cable: the BOM omits
|
||||
* it entirely, though the canvas still draws the link (styled
|
||||
* differently) since the wiring topology is still real. */
|
||||
builtInCable?: boolean
|
||||
}
|
||||
|
||||
/** A reusable, non-instantiated device definition (shown in the palette). */
|
||||
@@ -61,7 +79,9 @@ export interface DeviceTemplate {
|
||||
id: string
|
||||
name: string
|
||||
category: DeviceCategory
|
||||
manufacturer?: string
|
||||
/** References a Manufacturer catalog entry — optional since not every
|
||||
* device has a known/chosen manufacturer (e.g. DIY or generic gear). */
|
||||
manufacturerId?: string
|
||||
model?: string
|
||||
ports: Port[]
|
||||
/** Suggested default cost, copied onto each placed Device at creation time
|
||||
@@ -82,7 +102,9 @@ export interface Device {
|
||||
name: string
|
||||
templateId?: string
|
||||
category: DeviceCategory
|
||||
manufacturer?: string
|
||||
/** References a Manufacturer catalog entry, same as the template's —
|
||||
* independently editable per instance afterward, like category/model. */
|
||||
manufacturerId?: string
|
||||
model?: string
|
||||
notes?: string
|
||||
ports: Port[]
|
||||
@@ -156,5 +178,6 @@ export interface Catalog {
|
||||
portTypes: PortType[]
|
||||
cableTypes: CableType[]
|
||||
deviceCategories: DeviceCategoryDef[]
|
||||
manufacturers: Manufacturer[]
|
||||
deviceTemplates: DeviceTemplate[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user