diff --git a/docs/circuit-list-editor-architecture.md b/docs/circuit-list-editor-architecture.md index 95f53d1..b7b4866 100644 --- a/docs/circuit-list-editor-architecture.md +++ b/docs/circuit-list-editor-architecture.md @@ -26,6 +26,7 @@ The pure grid modules have no React state and are covered by focused unit tests. - protection data - cable data - reserve state + - voltage and optional control requirement for future sizing - circuit-level remark/status - `CircuitDeviceRow` - Load/device line inside a circuit. diff --git a/docs/spec/03-data-model-concept.md b/docs/spec/03-data-model-concept.md index 346f328..545c450 100644 --- a/docs/spec/03-data-model-concept.md +++ b/docs/spec/03-data-model-concept.md @@ -69,6 +69,7 @@ User-facing fields: - `rcdAssignment` optional - `terminalDesignation` optional - `voltage` optional +- `controlRequirement` optional, for example DALI or KNX - `status` optional - `isReserve` optional - `remark` optional diff --git a/scripts/db-verify-circuit-schema.js b/scripts/db-verify-circuit-schema.js index b2117c4..50bba9e 100644 --- a/scripts/db-verify-circuit-schema.js +++ b/scripts/db-verify-circuit-schema.js @@ -33,11 +33,17 @@ const projectDeviceColumns = new Set( const missingProjectDeviceColumns = requiredProjectDeviceColumns.filter( (name) => !projectDeviceColumns.has(name) ); +const requiredCircuitColumns = ["control_requirement"]; +const circuitColumns = new Set( + db.prepare("PRAGMA table_info(circuits)").all().map((column) => column.name) +); +const missingCircuitColumns = requiredCircuitColumns.filter((name) => !circuitColumns.has(name)); console.log("Database:", dbPath); console.log("Required tables:", requiredTables.join(", ")); console.log("Existing tables:", [...existing].join(", ") || "(none)"); console.log("Required project-device columns:", requiredProjectDeviceColumns.join(", ")); +console.log("Required circuit columns:", requiredCircuitColumns.join(", ")); if (missing.length > 0) { console.error("Missing tables:", missing.join(", ")); @@ -49,4 +55,9 @@ if (missingProjectDeviceColumns.length > 0) { process.exit(1); } +if (missingCircuitColumns.length > 0) { + console.error("Missing circuit columns:", missingCircuitColumns.join(", ")); + process.exit(1); +} + console.log("Circuit-first schema verification passed."); diff --git a/src/db/migrations/0010_circuit_control_requirement.sql b/src/db/migrations/0010_circuit_control_requirement.sql new file mode 100644 index 0000000..c51fdf8 --- /dev/null +++ b/src/db/migrations/0010_circuit_control_requirement.sql @@ -0,0 +1 @@ +ALTER TABLE `circuits` ADD COLUMN `control_requirement` text; diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 8b9e712..2dfa722 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1784743200000, "tag": "0009_project_device_circuit_fields", "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1784746800000, + "tag": "0010_circuit_control_requirement", + "breakpoints": true } ] } diff --git a/src/db/repositories/circuit.repository.ts b/src/db/repositories/circuit.repository.ts index d8b3262..b43c491 100644 --- a/src/db/repositories/circuit.repository.ts +++ b/src/db/repositories/circuit.repository.ts @@ -30,6 +30,7 @@ export class CircuitRepository { cableCrossSection?: string; cableLength?: number; voltage?: number; + controlRequirement?: string; remark?: string; rcdAssignment?: string; terminalDesignation?: string; @@ -53,6 +54,7 @@ export class CircuitRepository { rcdAssignment: input.rcdAssignment ?? null, terminalDesignation: input.terminalDesignation ?? null, voltage: input.voltage ?? null, + controlRequirement: input.controlRequirement ?? null, status: input.status ?? null, isReserve: input.isReserve ? 1 : 0, remark: input.remark ?? null, @@ -76,6 +78,7 @@ export class CircuitRepository { rcdAssignment?: string; terminalDesignation?: string; voltage?: number; + controlRequirement?: string; status?: string; isReserve: boolean; remark?: string; @@ -97,6 +100,7 @@ export class CircuitRepository { rcdAssignment: input.rcdAssignment ?? null, terminalDesignation: input.terminalDesignation ?? null, voltage: input.voltage ?? null, + controlRequirement: input.controlRequirement ?? null, status: input.status ?? null, isReserve: input.isReserve ? 1 : 0, remark: input.remark ?? null, diff --git a/src/db/schema/circuits.ts b/src/db/schema/circuits.ts index b65fb97..27b4683 100644 --- a/src/db/schema/circuits.ts +++ b/src/db/schema/circuits.ts @@ -24,6 +24,7 @@ export const circuits = sqliteTable( rcdAssignment: text("rcd_assignment"), terminalDesignation: text("terminal_designation"), voltage: real("voltage"), + controlRequirement: text("control_requirement"), status: text("status"), isReserve: integer("is_reserve").notNull().default(0), remark: text("remark"), diff --git a/src/domain/models/circuit-tree.model.ts b/src/domain/models/circuit-tree.model.ts index 144d37a..6420c0b 100644 --- a/src/domain/models/circuit-tree.model.ts +++ b/src/domain/models/circuit-tree.model.ts @@ -38,6 +38,7 @@ export interface CircuitTreeCircuit { rcdAssignment?: string; terminalDesignation?: string; voltage?: number; + controlRequirement?: string; status?: string; isReserve: boolean; remark?: string; diff --git a/src/domain/models/circuit.model.ts b/src/domain/models/circuit.model.ts index 4e7b25e..a0bca18 100644 --- a/src/domain/models/circuit.model.ts +++ b/src/domain/models/circuit.model.ts @@ -14,6 +14,7 @@ export interface Circuit { rcdAssignment?: string; terminalDesignation?: string; voltage?: number; + controlRequirement?: string; status?: string; isReserve: boolean; remark?: string; diff --git a/src/domain/services/circuit-write.service.ts b/src/domain/services/circuit-write.service.ts index 7d28b46..baf6fca 100644 --- a/src/domain/services/circuit-write.service.ts +++ b/src/domain/services/circuit-write.service.ts @@ -113,6 +113,7 @@ export class CircuitWriteService { rcdAssignment: input.rcdAssignment, terminalDesignation: input.terminalDesignation, voltage: input.voltage, + controlRequirement: input.controlRequirement, status: input.status, isReserve: input.isReserve ?? true, remark: input.remark, @@ -147,6 +148,7 @@ export class CircuitWriteService { rcdAssignment: input.rcdAssignment ?? current.rcdAssignment ?? undefined, terminalDesignation: input.terminalDesignation ?? current.terminalDesignation ?? undefined, voltage: input.voltage ?? current.voltage ?? undefined, + controlRequirement: input.controlRequirement ?? current.controlRequirement ?? undefined, status: input.status ?? current.status ?? undefined, isReserve: input.isReserve ?? Boolean(current.isReserve), remark: input.remark ?? current.remark ?? undefined, @@ -208,6 +210,7 @@ export class CircuitWriteService { rcdAssignment: circuit.rcdAssignment ?? undefined, terminalDesignation: circuit.terminalDesignation ?? undefined, voltage: circuit.voltage ?? undefined, + controlRequirement: circuit.controlRequirement ?? undefined, status: circuit.status ?? undefined, isReserve: false, remark: circuit.remark ?? undefined, @@ -287,6 +290,7 @@ export class CircuitWriteService { rcdAssignment: circuit.rcdAssignment ?? undefined, terminalDesignation: circuit.terminalDesignation ?? undefined, voltage: circuit.voltage ?? undefined, + controlRequirement: circuit.controlRequirement ?? undefined, status: circuit.status ?? undefined, isReserve: true, remark: circuit.remark ?? undefined, @@ -363,6 +367,7 @@ export class CircuitWriteService { rcdAssignment: sourceCircuit.rcdAssignment ?? undefined, terminalDesignation: sourceCircuit.terminalDesignation ?? undefined, voltage: sourceCircuit.voltage ?? undefined, + controlRequirement: sourceCircuit.controlRequirement ?? undefined, status: sourceCircuit.status ?? undefined, isReserve: true, remark: sourceCircuit.remark ?? undefined, @@ -385,6 +390,7 @@ export class CircuitWriteService { rcdAssignment: targetCircuit.rcdAssignment ?? undefined, terminalDesignation: targetCircuit.terminalDesignation ?? undefined, voltage: targetCircuit.voltage ?? undefined, + controlRequirement: targetCircuit.controlRequirement ?? undefined, status: targetCircuit.status ?? undefined, isReserve: false, remark: targetCircuit.remark ?? undefined, @@ -485,6 +491,7 @@ export class CircuitWriteService { rcdAssignment: sourceCircuit.rcdAssignment ?? undefined, terminalDesignation: sourceCircuit.terminalDesignation ?? undefined, voltage: sourceCircuit.voltage ?? undefined, + controlRequirement: sourceCircuit.controlRequirement ?? undefined, status: sourceCircuit.status ?? undefined, isReserve: true, remark: sourceCircuit.remark ?? undefined, @@ -507,6 +514,7 @@ export class CircuitWriteService { rcdAssignment: targetCircuit.rcdAssignment ?? undefined, terminalDesignation: targetCircuit.terminalDesignation ?? undefined, voltage: targetCircuit.voltage ?? undefined, + controlRequirement: targetCircuit.controlRequirement ?? undefined, status: targetCircuit.status ?? undefined, isReserve: false, remark: targetCircuit.remark ?? undefined, @@ -622,6 +630,7 @@ export class CircuitWriteService { rcdAssignment: circuit.rcdAssignment ?? undefined, terminalDesignation: circuit.terminalDesignation ?? undefined, voltage: circuit.voltage ?? undefined, + controlRequirement: circuit.controlRequirement ?? undefined, status: circuit.status ?? undefined, isReserve: Boolean(circuit.isReserve), remark: circuit.remark ?? undefined, diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index d31dc2e..33efa0a 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -104,6 +104,7 @@ interface CircuitSnapshot { rcdAssignment?: string; terminalDesignation?: string; voltage?: number; + controlRequirement?: string; status?: string; isReserve: boolean; remark?: string; @@ -938,7 +939,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const payload: Record = {}; if (key === "protectionSummary" || key === "cableSummary") { payload.remark = draft.trim() === "" ? undefined : draft; - } else if (["protectionRatedCurrent", "cableLength"].includes(key)) { + } else if (["protectionRatedCurrent", "cableLength", "voltage"].includes(key)) { payload[key] = parseNumeric(key, draft); } else if (key === "isReserve") { const normalized = draft.trim().toLowerCase(); @@ -1510,6 +1511,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str rcdAssignment: found.rcdAssignment ?? undefined, terminalDesignation: found.terminalDesignation ?? undefined, voltage: found.voltage ?? undefined, + controlRequirement: found.controlRequirement ?? undefined, status: found.status ?? undefined, isReserve: found.isReserve, remark: found.remark ?? undefined, @@ -1534,6 +1536,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str rcdAssignment: snapshot.rcdAssignment, terminalDesignation: snapshot.terminalDesignation, voltage: snapshot.voltage, + controlRequirement: snapshot.controlRequirement, status: snapshot.status, isReserve: snapshot.isReserve, remark: snapshot.remark, diff --git a/src/frontend/types.ts b/src/frontend/types.ts index a969a6d..4829e17 100644 --- a/src/frontend/types.ts +++ b/src/frontend/types.ts @@ -270,6 +270,7 @@ export interface CircuitTreeCircuitDto { rcdAssignment?: string; terminalDesignation?: string; voltage?: number; + controlRequirement?: string; status?: string; isReserve: boolean; remark?: string; @@ -317,6 +318,7 @@ export interface CreateCircuitInputDto { rcdAssignment?: string; terminalDesignation?: string; voltage?: number; + controlRequirement?: string; status?: string; isReserve?: boolean; remark?: string; diff --git a/src/frontend/utils/circuit-grid-model.ts b/src/frontend/utils/circuit-grid-model.ts index f486cd2..e1326c5 100644 --- a/src/frontend/utils/circuit-grid-model.ts +++ b/src/frontend/utils/circuit-grid-model.ts @@ -29,6 +29,8 @@ export type CellKey = | "cableLength" | "rcdAssignment" | "terminalDesignation" + | "voltage" + | "controlRequirement" | "status" | "isReserve"; @@ -81,6 +83,8 @@ export const allColumns: ColumnDef[] = [ { key: "cableLength", label: "Cable length", numeric: true }, { key: "rcdAssignment", label: "RCD assignment" }, { key: "terminalDesignation", label: "Terminal designation" }, + { key: "voltage", label: "Voltage", numeric: true }, + { key: "controlRequirement", label: "Control requirement" }, { key: "status", label: "Status" }, { key: "isReserve", label: "Reserve" }, ]; @@ -119,6 +123,8 @@ export const circuitOnlyColumns = new Set([ "cableLength", "rcdAssignment", "terminalDesignation", + "voltage", + "controlRequirement", "status", "isReserve", ]); @@ -154,6 +160,8 @@ const circuitFieldKeys = new Set([ "cableSummary", "rcdAssignment", "terminalDesignation", + "voltage", + "controlRequirement", "status", "isReserve", "remark", @@ -226,6 +234,8 @@ export function getCircuitValue(circuit: CircuitTreeCircuitDto, key: CellKey): G case "cableLength": return circuit.cableLength; case "rcdAssignment": return circuit.rcdAssignment; case "terminalDesignation": return circuit.terminalDesignation; + case "voltage": return circuit.voltage; + case "controlRequirement": return circuit.controlRequirement; case "status": return circuit.status; case "isReserve": return circuit.isReserve; case "remark": return circuit.remark; diff --git a/src/server/controllers/circuit-tree.controller.ts b/src/server/controllers/circuit-tree.controller.ts index 155fe4a..398a528 100644 --- a/src/server/controllers/circuit-tree.controller.ts +++ b/src/server/controllers/circuit-tree.controller.ts @@ -109,6 +109,7 @@ export async function getCircuitTree(req: Request, res: Response) { rcdAssignment: circuit.rcdAssignment ?? undefined, terminalDesignation: circuit.terminalDesignation ?? undefined, voltage: circuit.voltage ?? undefined, + controlRequirement: circuit.controlRequirement ?? undefined, status: circuit.status ?? undefined, isReserve: Boolean(circuit.isReserve), remark: circuit.remark ?? undefined, diff --git a/src/shared/validation/circuit.schemas.ts b/src/shared/validation/circuit.schemas.ts index b78afcd..b248a37 100644 --- a/src/shared/validation/circuit.schemas.ts +++ b/src/shared/validation/circuit.schemas.ts @@ -14,6 +14,7 @@ export const createCircuitSchema = z.object({ rcdAssignment: z.string().optional(), terminalDesignation: z.string().optional(), voltage: z.number().positive().optional(), + controlRequirement: z.string().optional(), status: z.string().optional(), isReserve: z.boolean().optional(), remark: z.string().optional(), diff --git a/tests/circuit-grid-model.test.ts b/tests/circuit-grid-model.test.ts index 790d7b5..6c0af13 100644 --- a/tests/circuit-grid-model.test.ts +++ b/tests/circuit-grid-model.test.ts @@ -38,6 +38,8 @@ const circuit: CircuitTreeCircuitDto = { cableType: "NYM-J", cableCrossSection: "1.5 mm²", cableLength: 20, + voltage: 230, + controlRequirement: "DALI", isReserve: false, remark: "Circuit remark", circuitTotalPower: 0.08, @@ -58,6 +60,8 @@ describe("circuit grid model", () => { assert.equal(getCellKind("reserveCircuit", "remark"), "deviceField"); assert.equal(getCellKind("placeholder", "displayName"), "deviceField"); assert.equal(getCellKind("deviceRow", "equipmentIdentifier"), "readonly"); + assert.equal(getCellKind("circuitSummary", "controlRequirement"), "circuitField"); + assert.equal(getCellKind("deviceRow", "controlRequirement"), "readonly"); assert.equal(getCellKind("circuitCompact", "rowTotalPower"), "computed"); }); @@ -65,6 +69,8 @@ describe("circuit grid model", () => { assert.equal(getDeviceValue(device, "roomSummary"), "1.01 Office"); assert.equal(getDeviceValue(device, "rowTotalPower"), 0.08); assert.equal(getCircuitValue(circuit, "protectionSummary"), "MCB 16A B"); + assert.equal(getCircuitValue(circuit, "voltage"), 230); + assert.equal(getCircuitValue(circuit, "controlRequirement"), "DALI"); assert.equal(getCircuitValue(circuit, "cableSummary"), "NYM-J, 1.5 mm², 20 m"); }); diff --git a/tests/circuit-write.rules.test.ts b/tests/circuit-write.rules.test.ts index 9e28966..eb9c33a 100644 --- a/tests/circuit-write.rules.test.ts +++ b/tests/circuit-write.rules.test.ts @@ -3,8 +3,58 @@ import { describe, it } from "node:test"; import { CircuitWriteService } from "../src/domain/services/circuit-write.service.js"; import { CircuitRepository } from "../src/db/repositories/circuit.repository.js"; import { db } from "../src/db/client.js"; +import { createCircuitSchema } from "../src/shared/validation/circuit.schemas.js"; describe("circuit write service rules", () => { + it("accepts future sizing inputs without calculating sizing suggestions", () => { + const parsed = createCircuitSchema.safeParse({ + sectionId: "s1", + equipmentIdentifier: "-1F1", + sortOrder: 10, + voltage: 230, + controlRequirement: "DALI", + }); + + assert.equal(parsed.success, true); + }); + + it("passes voltage and control requirements through circuit updates", async () => { + let updatePayload: Record = {}; + const circuit = { + id: "c1", + circuitListId: "l1", + sectionId: "s1", + equipmentIdentifier: "-1F1", + sortOrder: 10, + isReserve: 0, + voltage: 230, + controlRequirement: "none", + }; + const service = new CircuitWriteService({ + circuitSectionRepository: { + async findById() { + return { id: "s1", circuitListId: "l1" } as never; + }, + } as never, + circuitRepository: { + async findById() { + return circuit as never; + }, + async existsByEquipmentIdentifier() { + return false; + }, + async update(_id: string, payload: Record) { + updatePayload = payload; + }, + } as never, + }); + + await service.updateCircuit("c1", { voltage: 400, controlRequirement: "DALI" }); + + assert.equal(updatePayload.voltage, 400); + assert.equal(updatePayload.controlRequirement, "DALI"); + }); + it("rejects duplicate equipment identifiers in same circuit list", async () => { const service = new CircuitWriteService({ circuitListRepository: {