WIP multiple sections
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
CircuitTreeDeviceRowDto,
|
||||
CircuitTreeSectionDto,
|
||||
} from "../src/frontend/types.js";
|
||||
import "./circuit-structure-projection.test.js";
|
||||
|
||||
function makeDevice(
|
||||
id: string,
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user