Add distribution power summaries

This commit is contained in:
2026-07-29 19:02:18 +02:00
parent dcb303284c
commit 9ea081179d
28 changed files with 2668 additions and 77 deletions
+44
View File
@@ -5,11 +5,14 @@ import {
buildCircuitEditPatch,
buildDeviceRowEditPatch,
formatValue,
getCircuitSectionLabel,
getProjectColumnLayoutStorageKey,
isGridEditorControlTarget,
getBlockSortValue,
getCellKind,
getCircuitValue,
getDeviceValue,
parseStoredColumnLayout,
parseNumeric,
} from "../src/frontend/utils/circuit-grid-model.js";
import type { CircuitTreeCircuitDto, CircuitTreeDeviceRowDto } from "../src/frontend/types.js";
@@ -73,6 +76,47 @@ describe("circuit grid model", () => {
assert.equal(allColumns[0].locked, true);
});
it("stores valid column layouts under a project-specific key", () => {
assert.equal(
getProjectColumnLayoutStorageKey("project-1"),
"circuitTreeEditor.columnLayout.v2.project-1"
);
const layout = parseStoredColumnLayout(
JSON.stringify({
order: ["displayName", "equipmentIdentifier"],
visible: [
"displayName",
"equipmentIdentifier",
"unknown",
],
})
);
assert.ok(layout);
assert.equal(layout.order[0], "equipmentIdentifier");
assert.deepEqual(layout.visible, [
"displayName",
"equipmentIdentifier",
]);
assert.equal(parseStoredColumnLayout("{invalid"), null);
});
it("shows stable circuit sections with German labels", () => {
assert.equal(
getCircuitSectionLabel({
key: "lighting",
displayName: "Lighting",
}),
"Licht"
);
assert.equal(
getCircuitSectionLabel({
key: "custom",
displayName: "Sonderbereich",
}),
"Sonderbereich"
);
});
it("maps shared fields to the correct level for every row shape", () => {
assert.equal(getCellKind("circuitCompact", "displayName"), "deviceField");
assert.equal(getCellKind("circuitCompact", "equipmentIdentifier"), "circuitField");
+29
View File
@@ -1,8 +1,11 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
applyDistributionBoardSimultaneityFactor,
calculateCircuitTotalPower,
calculateDistributionBoardTotalPower,
calculateRowTotalPower,
calculateSectionTotalPower,
} from "../src/domain/calculations/circuit-power-calculation.js";
import {
resolveCircuitPhaseType,
@@ -19,6 +22,32 @@ describe("circuit power calculation", () => {
]);
assert.equal(total, 4.5);
});
it("calculates section and distribution-board totals", () => {
const lighting = calculateSectionTotalPower([
{ circuitTotalPower: 1.5 },
{ circuitTotalPower: 2.25 },
]);
const singlePhase = calculateSectionTotalPower([
{ circuitTotalPower: 3 },
]);
const total = calculateDistributionBoardTotalPower([
{ sectionTotalPower: lighting },
{ sectionTotalPower: singlePhase },
{ sectionTotalPower: 0 },
]);
assert.equal(lighting, 3.75);
assert.equal(total, 6.75);
assert.equal(
applyDistributionBoardSimultaneityFactor(total, 0.8),
5.4
);
assert.throws(
() => applyDistributionBoardSimultaneityFactor(total, 1.1),
/between zero and one/
);
});
});
describe("project voltage derivation", () => {
@@ -22,7 +22,10 @@ import {
createDistributionBoardStructureSnapshot,
type DistributionBoardStructureProjectCommand,
} from "../src/domain/models/distribution-board-structure-project-command.model.js";
import { createDistributionBoardUpdateProjectCommand } from "../src/domain/models/distribution-board-project-command.model.js";
import {
createDistributionBoardUpdateProjectCommand,
type DistributionBoardUpdateProjectCommand,
} from "../src/domain/models/distribution-board-project-command.model.js";
import {
createDistributionBoard,
updateDistributionBoard,
@@ -293,7 +296,7 @@ describe("distribution-board structure project command", () => {
}
});
it("updates floor and supply type with persisted undo and validates project ownership", () => {
it("updates floor, supply type and simultaneity factor with persisted undo", () => {
const context = createTestDatabase();
try {
context.db
@@ -335,7 +338,11 @@ describe("distribution-board structure project command", () => {
source: "user",
command: createDistributionBoardUpdateProjectCommand(
structure.distributionBoard.id,
{ floorId: "floor-1", supplyType: "SV" }
{
floorId: "floor-1",
supplyType: "SV",
simultaneityFactor: 0.75,
}
),
});
assert.deepEqual(
@@ -343,10 +350,16 @@ describe("distribution-board structure project command", () => {
.select({
floorId: distributionBoards.floorId,
supplyType: distributionBoards.supplyType,
simultaneityFactor:
distributionBoards.simultaneityFactor,
})
.from(distributionBoards)
.get(),
{ floorId: "floor-1", supplyType: "SV" }
{
floorId: "floor-1",
supplyType: "SV",
simultaneityFactor: 0.75,
}
);
const undoStep = new ProjectHistoryRepository(
@@ -365,10 +378,16 @@ describe("distribution-board structure project command", () => {
.select({
floorId: distributionBoards.floorId,
supplyType: distributionBoards.supplyType,
simultaneityFactor:
distributionBoards.simultaneityFactor,
})
.from(distributionBoards)
.get(),
{ floorId: null, supplyType: "AV" }
{
floorId: null,
supplyType: "AV",
simultaneityFactor: 1,
}
);
assert.throws(
() =>
@@ -395,6 +414,52 @@ describe("distribution-board structure project command", () => {
}
});
it("keeps stored version-one board updates executable", () => {
const context = createTestDatabase();
try {
const repository =
new DistributionBoardStructureProjectCommandRepository(
context.db
);
const structure = createDistributionBoardStructureSnapshot(
"project-1",
"UV Alt",
{ supplyType: "AV" }
);
repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command:
createDistributionBoardInsertProjectCommand(structure),
});
const legacyUpdate = {
schemaVersion: 1,
type: "distribution-board.update",
payload: {
distributionBoardId: structure.distributionBoard.id,
changes: [{ field: "supplyType", value: "SV" }],
},
} as DistributionBoardUpdateProjectCommand;
repository.executeUpdate({
projectId: "project-1",
expectedRevision: 1,
source: "user",
command: legacyUpdate,
});
const board = context.db
.select()
.from(distributionBoards)
.get();
assert.equal(board?.supplyType, "SV");
assert.equal(board?.simultaneityFactor, 1);
} finally {
context.close();
}
});
it("sends distribution-board updates to the revision-safe API", async () => {
const requests: Array<{ url: string; method: string; body: unknown }> = [];
const originalFetch = globalThis.fetch;
@@ -413,7 +478,11 @@ describe("distribution-board structure project command", () => {
await updateDistributionBoard(
"project-1",
"board-1",
{ floorId: "floor-1", supplyType: "USV" },
{
floorId: "floor-1",
supplyType: "USV",
simultaneityFactor: 0.8,
},
9
);
} finally {
@@ -426,6 +495,7 @@ describe("distribution-board structure project command", () => {
body: {
floorId: "floor-1",
supplyType: "USV",
simultaneityFactor: 0.8,
expectedRevision: 9,
},
},
+19 -1
View File
@@ -67,7 +67,11 @@ function createTestDatabase(): DatabaseContext {
.run();
context.db
.update(distributionBoards)
.set({ floorId: "floor-1", supplyType: "SV" })
.set({
floorId: "floor-1",
supplyType: "SV",
simultaneityFactor: 0.65,
})
.where(eq(distributionBoards.id, board.id))
.run();
context.db
@@ -176,6 +180,10 @@ describe("project snapshot repository", () => {
assert.equal(state.distributionBoards.length, 1);
assert.equal(state.distributionBoards[0].floorId, "floor-1");
assert.equal(state.distributionBoards[0].supplyType, "SV");
assert.equal(
state.distributionBoards[0].simultaneityFactor,
0.65
);
assert.equal(state.circuitLists.length, 1);
assert.equal(
state.circuitSections.length,
@@ -293,6 +301,7 @@ describe("project snapshot repository", () => {
>) {
delete board.floorId;
delete board.supplyType;
delete board.simultaneityFactor;
}
const legacyPayloadJson = JSON.stringify(currentPayload);
context.db
@@ -375,6 +384,10 @@ describe("portable project transfer", () => {
copied.state.distributionBoards[0].supplyType,
"SV"
);
assert.equal(
copied.state.distributionBoards[0].simultaneityFactor,
0.65
);
assert.equal(
copied.state.circuits[0].deviceRows[0].legacyConsumerId,
null
@@ -431,6 +444,7 @@ describe("portable project transfer", () => {
for (const board of legacyState.distributionBoards) {
delete board.floorId;
delete board.supplyType;
delete board.simultaneityFactor;
}
const legacyPayloadJson = JSON.stringify(legacyState);
const duplicate = repository.importDuplicate({
@@ -448,6 +462,10 @@ describe("portable project transfer", () => {
assert.ok(copied);
assert.equal(copied.state.distributionBoards[0].floorId, null);
assert.equal(copied.state.distributionBoards[0].supplyType, null);
assert.equal(
copied.state.distributionBoards[0].simultaneityFactor,
1
);
} finally {
context.close();
}
+28
View File
@@ -103,6 +103,7 @@ describe("project state snapshot model", () => {
...previous.distributionBoards[0],
floorId: null,
supplyType: null,
simultaneityFactor: 1,
},
]);
});
@@ -158,6 +159,30 @@ describe("project state snapshot model", () => {
assert.equal(snapshot.projectDevices[0]?.voltageV, 400);
});
it("upgrades version-five distribution boards with a neutral factor", () => {
const previous = {
...minimalSnapshot(),
schemaVersion: 5 as const,
distributionBoards: [
{
id: "board-1",
projectId: "project-1",
name: "UV 1",
floorId: null,
supplyType: "AV" as const,
},
],
};
const snapshot = parseProjectStateSnapshot(previous);
assert.equal(snapshot.schemaVersion, projectStateSnapshotSchemaVersion);
assert.equal(
snapshot.distributionBoards[0]?.simultaneityFactor,
1
);
});
it("rejects duplicate ids and cross-project ownership", () => {
assert.throws(
() =>
@@ -170,6 +195,7 @@ describe("project state snapshot model", () => {
name: "UV 1",
floorId: null,
supplyType: null,
simultaneityFactor: 1,
},
{
id: "board-1",
@@ -177,6 +203,7 @@ describe("project state snapshot model", () => {
name: "UV 2",
floorId: null,
supplyType: null,
simultaneityFactor: 1,
},
],
}),
@@ -211,6 +238,7 @@ describe("project state snapshot model", () => {
name: "UV 1",
floorId: "missing",
supplyType: "AV",
simultaneityFactor: 1,
},
],
}),