forked from jappel/leistungsbilanz-ts
Add per-project socket limit and Revit socket device-type flag
- Project setting maxSocketsPerCircuit (migration 0018, project.update-settings schema 4, snapshot schema 11); older command and snapshot versions keep or upgrade the value to null. - Revit configuration schema 2 marks device types as socket; version 1 stays readable and derives the flag from socketCount family rules. - Settings dialog field and device-type card on the Revit page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
85cbdbf8e0
commit
e48042bde3
35 changed files with 3781 additions and 41 deletions
|
|
@ -29,7 +29,7 @@ import { createRevitXlsxFixture, externalXlsxTestConfiguration, loadXlsxWorkbook
|
|||
|
||||
const deviceTypeA = "10000000-0000-4000-8000-000000000001";
|
||||
const deviceTypeB = "10000000-0000-4000-8000-000000000002";
|
||||
const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: 0.9, costGroup: null } }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 1, cosPhi: null, costGroup: null } }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
|
||||
const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: 0.9, costGroup: null }, isSocket: true }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 1, cosPhi: null, costGroup: null }, isSocket: false }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
|
||||
const catalogs = { floors: [], rooms: [], distributionBoards: [], projectDevices: [] };
|
||||
|
||||
describe("external follow-up import project command", () => {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ import {
|
|||
assertExternalModelConfiguration,
|
||||
createDefaultExternalModelConfiguration,
|
||||
} from "../src/external-model/configuration/external-model-configuration.js";
|
||||
import {
|
||||
resolveSocketDeviceTypeIds,
|
||||
withSocketDeviceTypeFlags,
|
||||
} from "../src/shared/revit/socket-device-types.js";
|
||||
|
||||
const deviceTypeId = "10000000-0000-4000-8000-000000000001";
|
||||
const ruleId = "20000000-0000-4000-8000-000000000001";
|
||||
|
|
@ -25,6 +29,7 @@ function configuration() {
|
|||
name: "Steckdose",
|
||||
category: "single_phase",
|
||||
defaults: { powerPerUnitW: 200, simultaneityFactor: null, cosPhi: 0.95, costGroup: "440" },
|
||||
isSocket: true,
|
||||
});
|
||||
value.familyTypeRules.push({
|
||||
id: ruleId,
|
||||
|
|
@ -61,6 +66,27 @@ describe("external model configuration", () => {
|
|||
assert.throws(() => assertExternalModelConfiguration(unknownType), /unbekannte Ger.*teart/);
|
||||
});
|
||||
|
||||
it("requires the socket flag in version 2 and keeps version 1 readable", () => {
|
||||
const missingFlag = configuration();
|
||||
delete missingFlag.deviceTypes[0]!.isSocket;
|
||||
assert.throws(() => assertExternalModelConfiguration(missingFlag), /unbekannte oder fehlende Felder/);
|
||||
|
||||
const legacy = { ...configuration(), schemaVersion: 1 as const };
|
||||
assert.throws(() => assertExternalModelConfiguration(legacy), /unbekannte oder fehlende Felder/);
|
||||
delete legacy.deviceTypes[0]!.isSocket;
|
||||
assert.doesNotThrow(() => assertExternalModelConfiguration(legacy));
|
||||
|
||||
assert.deepEqual([...resolveSocketDeviceTypeIds(legacy)], [deviceTypeId]);
|
||||
const upgraded = withSocketDeviceTypeFlags(legacy);
|
||||
assert.equal(upgraded.schemaVersion, 2);
|
||||
assert.equal(upgraded.deviceTypes[0]!.isSocket, true);
|
||||
assert.doesNotThrow(() => assertExternalModelConfiguration(upgraded));
|
||||
|
||||
const notSocket = configuration();
|
||||
notSocket.deviceTypes[0]!.isSocket = false;
|
||||
assert.equal(resolveSocketDeviceTypeIds(notSocket).size, 0);
|
||||
});
|
||||
|
||||
it("persists changes without an independent configuration revision", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const deviceTypeA = "10000000-0000-4000-8000-000000000001";
|
|||
const deviceTypeB = "10000000-0000-4000-8000-000000000002";
|
||||
const newWorksharingId = "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE-00000003";
|
||||
const catalogs = { floors: [], rooms: [], distributionBoards: [], projectDevices: [] };
|
||||
const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: 0.9, costGroup: null } }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 1, cosPhi: null, costGroup: null } }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
|
||||
const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: 0.9, costGroup: null }, isSocket: true }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 1, cosPhi: null, costGroup: null }, isSocket: false }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
|
||||
|
||||
async function initialState() {
|
||||
const bytes = await createRevitXlsxFixture();
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ function configured() {
|
|||
...createDefaultExternalModelConfiguration(),
|
||||
...externalXlsxTestConfiguration,
|
||||
deviceTypes: [
|
||||
{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: null, cosPhi: 0.9, costGroup: "440" } },
|
||||
{ id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 0.8, cosPhi: null, costGroup: null } },
|
||||
{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: null, cosPhi: 0.9, costGroup: "440" }, isSocket: true },
|
||||
{ id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 0.8, cosPhi: null, costGroup: null }, isSocket: false },
|
||||
],
|
||||
familyTypeRules: [
|
||||
{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const },
|
||||
|
|
|
|||
|
|
@ -1081,6 +1081,7 @@ describe("project command service", () => {
|
|||
workStage: null,
|
||||
projectLead: null,
|
||||
projectEditor: null,
|
||||
maxSocketsPerCircuit: null,
|
||||
}),
|
||||
});
|
||||
assert.equal(updated.history.currentRevision, 1);
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ function getSettings(context: DatabaseContext) {
|
|||
workStage: projects.workStage,
|
||||
projectLead: projects.projectLead,
|
||||
projectEditor: projects.projectEditor,
|
||||
maxSocketsPerCircuit: projects.maxSocketsPerCircuit,
|
||||
currentRevision: projects.currentRevision,
|
||||
})
|
||||
.from(projects)
|
||||
|
|
@ -91,6 +92,7 @@ function settings(
|
|||
workStage: null,
|
||||
projectLead: null,
|
||||
projectEditor: null,
|
||||
maxSocketsPerCircuit: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
|
@ -231,6 +233,7 @@ describe("project settings project-command repository", () => {
|
|||
workStage: null,
|
||||
projectLead: null,
|
||||
projectEditor: null,
|
||||
maxSocketsPerCircuit: null,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
|
@ -307,6 +310,7 @@ describe("project settings project-command repository", () => {
|
|||
workStage: null,
|
||||
projectLead: null,
|
||||
projectEditor: null,
|
||||
maxSocketsPerCircuit: null,
|
||||
});
|
||||
|
||||
const undoStep = history.getNextCommand("project-1", "undo");
|
||||
|
|
@ -339,6 +343,7 @@ describe("project settings project-command repository", () => {
|
|||
workStage: null,
|
||||
projectLead: null,
|
||||
projectEditor: null,
|
||||
maxSocketsPerCircuit: null,
|
||||
currentRevision: 2,
|
||||
});
|
||||
assert.deepEqual(getDerivedVoltages(context), {
|
||||
|
|
@ -382,6 +387,7 @@ describe("project settings project-command repository", () => {
|
|||
workStage: null,
|
||||
projectLead: null,
|
||||
projectEditor: null,
|
||||
maxSocketsPerCircuit: null,
|
||||
currentRevision: 3,
|
||||
});
|
||||
assert.deepEqual(getDerivedVoltages(context), {
|
||||
|
|
@ -495,7 +501,7 @@ describe("project settings project-command repository", () => {
|
|||
projectLead: "P. Leitung",
|
||||
projectEditor: "B. Arbeiter",
|
||||
});
|
||||
assert.equal(forward.inverse.schemaVersion, 3);
|
||||
assert.equal(forward.inverse.schemaVersion, 4);
|
||||
|
||||
const undoStep = new ProjectHistoryRepository(
|
||||
context.db
|
||||
|
|
@ -543,6 +549,7 @@ describe("project settings project-command repository", () => {
|
|||
workStage: _workStage,
|
||||
projectLead: _projectLead,
|
||||
projectEditor: _projectEditor,
|
||||
maxSocketsPerCircuit: _maxSocketsPerCircuit,
|
||||
...versionTwoPayload
|
||||
} = settings({ name: "Stand aus Version 2" });
|
||||
const forward = repository.executeUpdate({
|
||||
|
|
@ -570,6 +577,83 @@ describe("project settings project-command repository", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("persists the socket limit with undo and keeps it for version 3 commands", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
const repository = new ProjectSettingsProjectCommandRepository(
|
||||
context.db
|
||||
);
|
||||
const readLimit = () =>
|
||||
context.db
|
||||
.select({ maxSocketsPerCircuit: projects.maxSocketsPerCircuit })
|
||||
.from(projects)
|
||||
.where(eq(projects.id, "project-1"))
|
||||
.get()?.maxSocketsPerCircuit;
|
||||
const forward = repository.executeUpdate({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 0,
|
||||
source: "user",
|
||||
command: createProjectSettingsUpdateProjectCommand(
|
||||
settings({ maxSocketsPerCircuit: 8 })
|
||||
),
|
||||
});
|
||||
assert.equal(readLimit(), 8);
|
||||
assert.equal(forward.inverse.schemaVersion, 4);
|
||||
assert.equal(
|
||||
(forward.inverse.payload as { maxSocketsPerCircuit: unknown })
|
||||
.maxSocketsPerCircuit,
|
||||
null
|
||||
);
|
||||
|
||||
const {
|
||||
maxSocketsPerCircuit: _maxSocketsPerCircuit,
|
||||
...versionThreePayload
|
||||
} = settings({ name: "Stand aus Version 3" });
|
||||
const versionThree = repository.executeUpdate({
|
||||
projectId: "project-1",
|
||||
expectedRevision: 1,
|
||||
source: "user",
|
||||
command: {
|
||||
schemaVersion: 3,
|
||||
type: "project.update-settings",
|
||||
payload: versionThreePayload,
|
||||
},
|
||||
});
|
||||
assert.equal(readLimit(), 8);
|
||||
assert.equal(versionThree.inverse.schemaVersion, 3);
|
||||
assert.equal(
|
||||
"maxSocketsPerCircuit" in versionThree.inverse.payload,
|
||||
false
|
||||
);
|
||||
|
||||
const history = new ProjectHistoryRepository(context.db);
|
||||
for (const expectedRevision of [2, 3]) {
|
||||
const undoStep = history.getNextCommand("project-1", "undo");
|
||||
assert.ok(undoStep);
|
||||
repository.executeUpdate({
|
||||
projectId: "project-1",
|
||||
expectedRevision,
|
||||
source: "undo",
|
||||
historyTargetChangeSetId: undoStep.changeSetId,
|
||||
command: undoStep.command as never,
|
||||
});
|
||||
}
|
||||
assert.equal(readLimit(), null);
|
||||
|
||||
for (const invalid of [0, 2.5, 1001]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
createProjectSettingsUpdateProjectCommand(
|
||||
settings({ maxSocketsPerCircuit: invalid })
|
||||
),
|
||||
/invalid values/
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("persists the project supply-type selection and rejects disabling an in-use type", () => {
|
||||
const context = createTestDatabase();
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -474,7 +474,7 @@ describe("project snapshot repository", () => {
|
|||
for (const room of payload.rooms) {
|
||||
delete room.areaM2;
|
||||
}
|
||||
for (const field of ["siteAddress", "workStage", "projectLead", "projectEditor"]) {
|
||||
for (const field of ["siteAddress", "workStage", "projectLead", "projectEditor", "maxSocketsPerCircuit"]) {
|
||||
delete payload.project[field];
|
||||
}
|
||||
for (const board of payload.distributionBoards) {
|
||||
|
|
|
|||
|
|
@ -13,10 +13,19 @@ import { createDefaultExternalCsvConfiguration } from "../src/external-model/csv
|
|||
import { externalCsvTestColumns } from "./fixtures/revit-csv-fixtures.js";
|
||||
import { zodIssues } from "./support/zod-assertions.js";
|
||||
|
||||
/** Removes the fields introduced by snapshot schema version 9. */
|
||||
/** Removes the field introduced by snapshot schema version 11. */
|
||||
function withoutVersionElevenFields<T extends { project: object }>(
|
||||
snapshot: T
|
||||
): T {
|
||||
delete (snapshot.project as Record<string, unknown>).maxSocketsPerCircuit;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/** Removes the fields introduced by snapshot schema versions 9 to 11. */
|
||||
function withoutVersionNineFields<
|
||||
T extends { distributionBoards: object[] },
|
||||
T extends { project: object; distributionBoards: object[] },
|
||||
>(snapshot: T): T {
|
||||
withoutVersionElevenFields(snapshot);
|
||||
const record = snapshot as Record<string, unknown>;
|
||||
record.externalCsvConfiguration = null;
|
||||
delete record.externalModelConfiguration;
|
||||
|
|
@ -29,7 +38,7 @@ function withoutVersionNineFields<
|
|||
|
||||
/** Removes the fields introduced by snapshot schema version 7. */
|
||||
function withoutVersionSevenFields<
|
||||
T extends { floors: object[]; distributionBoards: object[] },
|
||||
T extends { project: object; floors: object[]; distributionBoards: object[] },
|
||||
>(snapshot: T): T {
|
||||
withoutVersionNineFields(snapshot);
|
||||
delete (snapshot as Record<string, unknown>).buildings;
|
||||
|
|
@ -82,6 +91,7 @@ function minimalSnapshot() {
|
|||
workStage: "LPH 5" as string | null,
|
||||
projectLead: "P. Leitung" as string | null,
|
||||
projectEditor: null as string | null,
|
||||
maxSocketsPerCircuit: null as number | null,
|
||||
},
|
||||
distributionBoards: [],
|
||||
externalModelConfiguration: null,
|
||||
|
|
@ -351,6 +361,24 @@ describe("project state snapshot model", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("keeps the socket limit in version 11 and upgrades version 10 without a limit", () => {
|
||||
const current = structuredClone(minimalSnapshot());
|
||||
current.project.maxSocketsPerCircuit = 8;
|
||||
assert.equal(parseProjectStateSnapshot(current).project.maxSocketsPerCircuit, 8);
|
||||
|
||||
const previous = withoutVersionElevenFields(structuredClone(minimalSnapshot()));
|
||||
previous.schemaVersion = 10 as typeof previous.schemaVersion;
|
||||
const upgraded = parseProjectStateSnapshot(previous);
|
||||
assert.equal(upgraded.schemaVersion, projectStateSnapshotSchemaVersion);
|
||||
assert.equal(upgraded.project.maxSocketsPerCircuit, null);
|
||||
|
||||
const invalid = structuredClone(minimalSnapshot());
|
||||
invalid.project.maxSocketsPerCircuit = 0;
|
||||
assert.throws(() => parseProjectStateSnapshot(invalid));
|
||||
const missing = withoutVersionElevenFields(structuredClone(minimalSnapshot()));
|
||||
assert.throws(() => parseProjectStateSnapshot(missing));
|
||||
});
|
||||
|
||||
it("upgrades snapshot version 3 with an empty external model", () => {
|
||||
const previous = withoutVersionSixFields(structuredClone(minimalSnapshot()));
|
||||
previous.schemaVersion = 3 as typeof previous.schemaVersion;
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const circuitId = "circuit-e2e";
|
|||
const rowId = "row-e2e";
|
||||
const deviceTypeA = "10000000-0000-4000-8000-000000000001";
|
||||
const deviceTypeB = "10000000-0000-4000-8000-000000000002";
|
||||
const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: 125, simultaneityFactor: 1, cosPhi: 0.9, costGroup: "440" } }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 0.8, cosPhi: null, costGroup: null } }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
|
||||
const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: 125, simultaneityFactor: 1, cosPhi: 0.9, costGroup: "440" }, isSocket: true }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 0.8, cosPhi: null, costGroup: null }, isSocket: false }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
|
||||
|
||||
describe("Linear XLSX end-to-end", () => {
|
||||
it("survives return, follow-up, snapshot restore, duplicate and restart undo/redo", async () => {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const deviceTypeId = "10000000-0000-4000-8000-000000000001";
|
|||
const configuration = {
|
||||
...createDefaultExternalModelConfiguration(),
|
||||
...externalXlsxTestConfiguration,
|
||||
deviceTypes: [{ id: deviceTypeId, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: 125, simultaneityFactor: 1, cosPhi: 0.9, costGroup: "440" } }],
|
||||
deviceTypes: [{ id: deviceTypeId, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: 125, simultaneityFactor: 1, cosPhi: 0.9, costGroup: "440" }, isSocket: true }],
|
||||
familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId, quantityRule: "socketCount" as const }],
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue