WIP multiple sections

This commit is contained in:
2026-07-30 20:27:17 +02:00
parent 7a9530a914
commit ac21d6eb5d
6 changed files with 268 additions and 1 deletions
+5
View File
@@ -350,6 +350,11 @@ enthalten ihre optionale getrennte Schutzgerätekonfiguration. Die bisherigen
flachen Stromkreis-Schutzfelder bleiben während der Übergangsphase additiv
erhalten. Separate aktive Read-Repositories lesen Komponenten- und beide
Schutzgerätetabellen; Schreibzugriffe bleiben ausschließlich in Commands.
`src/frontend/utils/circuit-structure-projection.ts` projiziert diesen Tree
rein und deterministisch in die drei Zonen Kopf, vollständige Gruppen und Fuß.
Innerhalb einer Gruppe folgen auf die Überschrift optionale Schutzkomponenten,
vollständige Stromkreisblöcke und der freie Einfügeplatz. Leere Gruppen bleiben
sichtbar; Komponenten werden stabil nach `sortOrder` und ID sortiert.
`distribution-board.update` versioniert Etage, Netzart und den
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
+2 -1
View File
@@ -32,7 +32,8 @@ requirements and intended sequencing, not proof of implementation.
- [x] Phase D3a: complete validated group-subtree snapshot and warning summary.
- [x] Phase D3b: persistent populated-group delete/restore command.
- [x] Phase E1: tree read model for components, groups and protection devices.
- [ ] Phase E2: header/group/footer editor projection.
- [x] Phase E2a: pure header/group/footer structure projection.
- [ ] Phase E2b: render the structure projection in the editor grid.
- [ ] Phase E: editor projection and editing.
- [ ] Phase F: documentation and full GUI verification.
- [ ] Keep full electrical sizing and cable-dimensioning rules separate until
@@ -739,6 +739,9 @@ Acceptance:
### E. Editor Projection and Editing
Status: In progress. The complete tree read model E1 and pure structural
projection E2a are complete. Grid rendering and editing remain pending.
- render fixed header components
- render group components and circuit blocks
- render sortable auxiliary footer components
@@ -746,6 +749,17 @@ Acceptance:
- add type-dependent protection editing
- keep spreadsheet selection, keyboard and drag behavior coherent
Implemented in E1/E2a:
- the tree response exposes fixed header, group and auxiliary footer
components plus both one-to-one protection layers
- a pure projection produces the stable order header, complete groups and
footer without mixing zones
- each group keeps its heading, optional protection components, complete
circuit blocks and free placeholder together
- empty groups remain visible and circuit device rows remain owned by their
circuit block
Acceptance:
- the visible order is header, groups, footer
@@ -0,0 +1,102 @@
import type {
CircuitTreeCircuitDto,
CircuitTreeComponentDto,
CircuitTreeResponseDto,
CircuitTreeSectionDto,
} from "../types.js";
export type CircuitStructureProjectionRow =
| {
rowKey: string;
rowType: "headerComponent" | "footerComponent";
zone: "header" | "footer";
component: CircuitTreeComponentDto;
}
| {
rowKey: string;
rowType: "groupHeader" | "groupPlaceholder";
zone: "group";
section: CircuitTreeSectionDto;
}
| {
rowKey: string;
rowType: "groupComponent";
zone: "group";
section: CircuitTreeSectionDto;
component: CircuitTreeComponentDto;
}
| {
rowKey: string;
rowType: "circuitBlock";
zone: "group";
section: CircuitTreeSectionDto;
circuit: CircuitTreeCircuitDto;
};
export function buildCircuitStructureProjection(
tree: Pick<
CircuitTreeResponseDto,
"headerComponents" | "sections" | "footerComponents"
>
): CircuitStructureProjectionRow[] {
const rows: CircuitStructureProjectionRow[] = [];
for (const component of ordered(tree.headerComponents)) {
rows.push({
rowKey: `header-component:${component.id}`,
rowType: "headerComponent",
zone: "header",
component,
});
}
for (const section of ordered(tree.sections)) {
rows.push({
rowKey: `group:${section.id}`,
rowType: "groupHeader",
zone: "group",
section,
});
for (const component of ordered(section.components)) {
rows.push({
rowKey: `group-component:${component.id}`,
rowType: "groupComponent",
zone: "group",
section,
component,
});
}
for (const circuit of section.circuits) {
rows.push({
rowKey: `circuit-block:${circuit.id}`,
rowType: "circuitBlock",
zone: "group",
section,
circuit,
});
}
rows.push({
rowKey: `group-placeholder:${section.id}`,
rowType: "groupPlaceholder",
zone: "group",
section,
});
}
for (const component of ordered(tree.footerComponents)) {
rows.push({
rowKey: `footer-component:${component.id}`,
rowType: "footerComponent",
zone: "footer",
component,
});
}
return rows;
}
function ordered<T extends { id: string; sortOrder: number }>(
values: readonly T[]
) {
return [...values].sort(
(left, right) =>
left.sortOrder - right.sortOrder ||
left.id.localeCompare(right.id)
);
}
+1
View File
@@ -10,6 +10,7 @@ import type {
CircuitTreeDeviceRowDto,
CircuitTreeSectionDto,
} from "../src/frontend/types.js";
import "./circuit-structure-projection.test.js";
function makeDevice(
id: string,
+144
View File
@@ -0,0 +1,144 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { buildCircuitStructureProjection } from "../src/frontend/utils/circuit-structure-projection.js";
import type {
CircuitTreeCircuitDto,
CircuitTreeComponentDto,
CircuitTreeSectionDto,
} from "../src/frontend/types.js";
function component(
id: string,
placement: CircuitTreeComponentDto["placement"],
sortOrder: number,
sectionId?: string
): CircuitTreeComponentDto {
return {
id,
circuitListId: "list-1",
sectionId,
equipmentIdentifier: `-${id}`,
name: id,
role:
placement === "header"
? "main_switch"
: placement === "footer"
? "auxiliary"
: "group_residual_current_protection",
placement,
sortOrder,
};
}
function circuit(
id: string,
sectionId: string
): CircuitTreeCircuitDto {
return {
id,
circuitListId: "list-1",
sectionId,
equipmentIdentifier: `-${id}`,
sortOrder: 10,
isReserve: true,
circuitTotalPower: 0,
deviceRows: [],
};
}
function section(
id: string,
sortOrder: number
): CircuitTreeSectionDto {
return {
id,
key: id,
displayName: id,
prefix: "-1F1.",
sortOrder,
category: "lighting",
groupNumber: sortOrder / 10,
sectionTotalPower: 0,
components: [
component(`${id}-rcd-late`, "group", 20, id),
component(`${id}-rcd-first`, "group", 10, id),
],
circuits: [circuit(`${id}-circuit`, id)],
};
}
describe("circuit structure projection", () => {
it("projects fixed header, complete groups and auxiliary footer in stable order", () => {
const rows = buildCircuitStructureProjection({
headerComponents: [
component("surge", "header", 20),
component("main", "header", 10),
],
sections: [section("group-2", 20), section("group-1", 10)],
footerComponents: [
component("actor-2", "footer", 20),
component("actor-1", "footer", 10),
],
});
assert.deepEqual(
rows.map((row) => row.rowKey),
[
"header-component:main",
"header-component:surge",
"group:group-1",
"group-component:group-1-rcd-first",
"group-component:group-1-rcd-late",
"circuit-block:group-1-circuit",
"group-placeholder:group-1",
"group:group-2",
"group-component:group-2-rcd-first",
"group-component:group-2-rcd-late",
"circuit-block:group-2-circuit",
"group-placeholder:group-2",
"footer-component:actor-1",
"footer-component:actor-2",
]
);
});
it("keeps structural rows around an empty group", () => {
const empty = section("group-empty", 10);
empty.components = [];
empty.circuits = [];
assert.deepEqual(
buildCircuitStructureProjection({
headerComponents: [],
sections: [empty],
footerComponents: [],
}).map((row) => row.rowType),
["groupHeader", "groupPlaceholder"]
);
});
it("keeps circuit device rows inside their circuit block", () => {
const grouped = section("group-1", 10);
grouped.circuits[0].deviceRows = [
{
id: "device-1",
sortOrder: 10,
name: "Leuchte",
displayName: "Leuchte",
quantity: 1,
powerPerUnit: 0.1,
simultaneityFactor: 1,
rowTotalPower: 0.1,
},
];
const block = buildCircuitStructureProjection({
headerComponents: [],
sections: [grouped],
footerComponents: [],
}).find((row) => row.rowType === "circuitBlock");
assert.equal(
block?.rowType === "circuitBlock"
? block.circuit.deviceRows[0].id
: null,
"device-1"
);
});
});