forked from jappel/leistungsbilanz-ts
Add explicit "no protection device" state for circuits
Circuits can now record that they deliberately have no protection device (type NONE, shown as "–"), separate from circuits whose protection is still open. Migration 0017 makes rated_current_a nullable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1f3b903809
commit
38019fe087
27 changed files with 3673 additions and 132 deletions
|
|
@ -160,6 +160,47 @@ describe("circuit grid clipboard", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("copies an explicit no-protection decision onto other circuits", () => {
|
||||
const none = makeCircuit("circuit-5", [makeDevice("row-5", 0.1)], {
|
||||
protectionDevice: { type: "NONE" },
|
||||
});
|
||||
const noneRows = buildVisibleGridRowsWithStructure(
|
||||
[{ ...sections[0], circuits: [none, target] }],
|
||||
{ headerComponents: [], footerComponents: [] }
|
||||
);
|
||||
const [noneRow, targetRow] = ["circuitCompact:circuit-5", "circuitCompact:circuit-2"].map(
|
||||
(rowKey) => noneRows.find((row) => row.rowKey === rowKey)!
|
||||
);
|
||||
const clipboard = buildCellClipboard([noneRow], ["protectionSummary"]);
|
||||
assert.equal(clipboard.text, "–");
|
||||
const plan = planCellWrites([
|
||||
{ row: targetRow, cellKey: "protectionSummary", value: clipboard.cells[0][0] },
|
||||
]);
|
||||
assert.deepEqual(plan.updates.map((update) => update.payload), [
|
||||
{
|
||||
circuitId: "circuit-2",
|
||||
expected: {
|
||||
circuitId: "circuit-2",
|
||||
type: "LS",
|
||||
ratedCurrentA: 10,
|
||||
fuseUtilizationCategory: null,
|
||||
tripCharacteristic: "B",
|
||||
rcdType: null,
|
||||
ratedResidualCurrentMa: null,
|
||||
},
|
||||
target: {
|
||||
circuitId: "circuit-2",
|
||||
type: "NONE",
|
||||
ratedCurrentA: null,
|
||||
fuseUtilizationCategory: null,
|
||||
tripCharacteristic: null,
|
||||
rcdType: null,
|
||||
ratedResidualCurrentMa: null,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("fills one value into every selected row and skips rows that cannot take it", () => {
|
||||
const selectedRowKeys = [
|
||||
"circuitCompact:circuit-1",
|
||||
|
|
|
|||
|
|
@ -197,6 +197,18 @@ describe("circuit grid model", () => {
|
|||
),
|
||||
"FI/LS · 16 A · B · Typ A · 30 mA"
|
||||
);
|
||||
const withoutDevice = {
|
||||
...circuit,
|
||||
protectionDevice: { type: "NONE" as const },
|
||||
};
|
||||
assert.equal(getCircuitValue(withoutDevice, "protectionSummary"), "–");
|
||||
assert.equal(getCircuitValue(withoutDevice, "protectionType"), "–");
|
||||
assert.equal(getCircuitValue(withoutDevice, "protectionRatedCurrent"), undefined);
|
||||
// A circuit without any protection record is still open.
|
||||
assert.equal(
|
||||
getCircuitValue({ ...circuit, protectionDevice: undefined }, "protectionSummary"),
|
||||
undefined
|
||||
);
|
||||
assert.equal(getCircuitValue(circuit, "voltage"), 230);
|
||||
assert.equal(getCircuitValue(circuit, "controlRequirement"), "DALI");
|
||||
assert.equal(getCircuitValue(circuit, "cableSummary"), "NYM-J, 1.5 mm², 20 m");
|
||||
|
|
|
|||
|
|
@ -135,6 +135,70 @@ describe("circuit-protection project command", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("stores an explicit no-protection decision and undoes it to the device", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const repository = new CircuitProtectionProjectCommandRepository(
|
||||
context.db
|
||||
);
|
||||
const history = new ProjectHistoryRepository(context.db);
|
||||
context.db.insert(circuitProtectionDevices).values(protection).run();
|
||||
const none: CircuitProtectionSnapshot = {
|
||||
circuitId: "circuit-1",
|
||||
type: "NONE",
|
||||
ratedCurrentA: null,
|
||||
fuseUtilizationCategory: null,
|
||||
tripCharacteristic: null,
|
||||
rcdType: null,
|
||||
ratedResidualCurrentMa: null,
|
||||
};
|
||||
const executed = repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command: createCircuitProtectionUpdateProjectCommand(
|
||||
"circuit-1",
|
||||
protection,
|
||||
none
|
||||
),
|
||||
});
|
||||
assert.deepEqual(
|
||||
context.db.select().from(circuitProtectionDevices).get(),
|
||||
none
|
||||
);
|
||||
const undo = history.getNextCommand("project-1", "undo");
|
||||
assert.ok(undo);
|
||||
repository.execute({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 1,
|
||||
source: "undo",
|
||||
historyTargetChangeSetId: undo.changeSetId,
|
||||
command: executed.inverse,
|
||||
});
|
||||
assert.deepEqual(
|
||||
context.db.select().from(circuitProtectionDevices).get(),
|
||||
protection
|
||||
);
|
||||
for (const invalid of [
|
||||
{ ...none, ratedCurrentA: 16 },
|
||||
{ ...none, tripCharacteristic: "B" as const },
|
||||
{ ...protection, ratedCurrentA: null },
|
||||
]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
createCircuitProtectionUpdateProjectCommand(
|
||||
"circuit-1",
|
||||
protection,
|
||||
invalid
|
||||
),
|
||||
/configuration is invalid/
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects stale state, foreign ownership and user removal", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -243,6 +243,7 @@ describe("print document model", () => {
|
|||
}),
|
||||
"FI/LS B 16 A, 30 mA Typ A"
|
||||
);
|
||||
assert.equal(formatProtectionDevice({ type: "NONE" }), "–");
|
||||
assert.equal(formatProtectionDevice(undefined), null);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -559,6 +559,44 @@ describe("project state snapshot model", () => {
|
|||
() => parseProjectStateSnapshot(invalidProtection),
|
||||
/protection-device configuration is invalid/
|
||||
);
|
||||
|
||||
const protectedState = protectedSnapshot();
|
||||
const noProtectionRecord = {
|
||||
circuitId: protectedState.circuitProtectionDevices[0]!.circuitId,
|
||||
type: "NONE",
|
||||
ratedCurrentA: null,
|
||||
fuseUtilizationCategory: null,
|
||||
tripCharacteristic: null,
|
||||
rcdType: null,
|
||||
ratedResidualCurrentMa: null,
|
||||
};
|
||||
assert.equal(
|
||||
parseProjectStateSnapshot({
|
||||
...protectedState,
|
||||
circuitProtectionDevices: [noProtectionRecord],
|
||||
}).circuitProtectionDevices[0]?.type,
|
||||
"NONE"
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
parseProjectStateSnapshot({
|
||||
...protectedState,
|
||||
circuitProtectionDevices: [
|
||||
{ ...noProtectionRecord, ratedCurrentA: 16 },
|
||||
],
|
||||
}),
|
||||
/protection-device configuration is invalid/
|
||||
);
|
||||
const componentWithoutDevice = {
|
||||
...protectedState,
|
||||
distributionBoardComponentProtectionDevices: [
|
||||
{
|
||||
...protectedState.distributionBoardComponentProtectionDevices[0]!,
|
||||
type: "NONE",
|
||||
},
|
||||
],
|
||||
};
|
||||
assert.throws(() => parseProjectStateSnapshot(componentWithoutDevice));
|
||||
});
|
||||
|
||||
it("rejects duplicate ids and invalid ownership", () => {
|
||||
|
|
|
|||
|
|
@ -205,6 +205,27 @@ describe("protection device defaults", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("maps the explicit no-protection state to a record without values", () => {
|
||||
const none = getCircuitProtectionEditorInitialValue("single_phase", {
|
||||
type: "NONE",
|
||||
});
|
||||
assert.deepEqual(none, { type: "NONE" });
|
||||
assert.deepEqual(toCircuitProtectionSnapshot("circuit-1", none), {
|
||||
circuitId: "circuit-1",
|
||||
type: "NONE",
|
||||
ratedCurrentA: null,
|
||||
fuseUtilizationCategory: null,
|
||||
tripCharacteristic: null,
|
||||
rcdType: null,
|
||||
ratedResidualCurrentMa: null,
|
||||
});
|
||||
// Distribution-board components never take the circuit-only state.
|
||||
assert.equal(
|
||||
protectionDeviceConfigurationSchema.safeParse({ type: "NONE" }).success,
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("creates the agreed circuit defaults", () => {
|
||||
assert.deepEqual(createDefaultCircuitProtection("lighting"), {
|
||||
type: "LS",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue