Reorder circuit groups

This commit is contained in:
2026-07-31 07:47:45 +02:00
parent 8898117ed4
commit 0cb280ddb2
7 changed files with 180 additions and 1 deletions
+4
View File
@@ -386,6 +386,10 @@ Gerätezeilen atomar. Delete/Undo erfasst denselben Datensatz vollständig.
Im Editor sind die bisherigen Schutzspalten deshalb nur noch eine
schreibgeschützte Projektion der 1:1-Daten; Änderungen erfolgen über ein
geräteabhängiges Schutzgeräte-Modal und `circuit-protection.update`.
Gruppen lassen sich im Editor schrittweise nur gegenüber einer benachbarten
Gruppe derselben Kategorie verschieben. Der Client sendet dabei immer die
vollständige erwartete und gewünschte Sortierreihenfolge über
`circuit-group.reorder`; Gruppennummer, Präfix und Kind-BMK bleiben unverändert.
`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
@@ -38,7 +38,8 @@ requirements and intended sequencing, not proof of implementation.
- [x] Phase E3b1: create, rename and safely delete empty circuit groups.
- [x] Phase E3b2a: persistent circuit-protection update command.
- [x] Phase E3b2b: circuit-protection defaults on insert and editor modal.
- [ ] Phase E4: group reorder, renumber, circuit moves and populated-delete warning.
- [x] Phase E4a: persistent same-category group reorder controls.
- [ ] Phase E4b: explicit group renumber, circuit moves and populated-delete warning.
- [ ] Phase E: editor projection and editing.
- [ ] Phase F: documentation and full GUI verification.
- [ ] Keep full electrical sizing and cable-dimensioning rules separate until
@@ -789,6 +789,8 @@ Implemented in E1/E2a/E2b/E3a:
together with the circuit and all device rows
- the editor modal exposes only fields valid for the selected protection type;
the old flat protection columns now project the new 1:1 data read-only
- group controls move a group only to the adjacent peer of the same category;
the complete list order is one persistent command and no number or BMK changes
Acceptance:
@@ -52,6 +52,7 @@ import {
buildCircuitSectionRenumberAssignments,
} from "../utils/circuit-section-renumber-command";
import {
buildCircuitGroupReorderAssignments,
buildNewCircuitGroupSnapshot,
canDeleteCircuitGroup,
renameCircuitGroupSnapshot,
@@ -95,6 +96,7 @@ import {
moveCircuitDeviceRowsToNewCircuitCommand,
reorderCircuitSectionCommand,
reorderCircuitSectionsCommand,
reorderCircuitGroupsCommand,
renumberCircuitSectionCommand,
redoProjectCommand,
updateCircuitById,
@@ -1235,6 +1237,36 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
});
}
async function handleMoveCircuitGroup(
groupId: string,
direction: -1 | 1
) {
if (!data) {
return;
}
const assignments = buildCircuitGroupReorderAssignments(
data.sections,
groupId,
direction
);
if (!assignments) {
return;
}
await runCommand({
label: "Stromkreisgruppen sortieren",
redo: async () => {
const result = await reorderCircuitGroupsCommand(
projectId,
getExpectedProjectRevision(),
circuitListId,
assignments
);
applyProjectCommandResult(result);
return null;
},
});
}
async function handleSaveCircuitProtection(
protection: CircuitTreeProtectionDeviceDto
) {
@@ -3477,6 +3509,40 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
>
Gruppe bearbeiten
</button>
<button
type="button"
tabIndex={-1}
disabled={
!buildCircuitGroupReorderAssignments(
data.sections,
section.id,
-1
)
}
onClick={() =>
void handleMoveCircuitGroup(section.id, -1)
}
title="Innerhalb derselben Kategorie nach oben verschieben"
>
Gruppe nach oben
</button>
<button
type="button"
tabIndex={-1}
disabled={
!buildCircuitGroupReorderAssignments(
data.sections,
section.id,
1
)
}
onClick={() =>
void handleMoveCircuitGroup(section.id, 1)
}
title="Innerhalb derselben Kategorie nach unten verschieben"
>
Gruppe nach unten
</button>
<button
type="button"
tabIndex={-1}
+19
View File
@@ -57,6 +57,7 @@ import type {
} from "../../domain/models/distribution-board-component-structure-project-command.model";
import type {
CircuitGroupSnapshot,
CircuitGroupReorderAssignment,
} from "../../domain/models/circuit-group-structure-project-command.model";
import type {
CircuitProtectionSnapshot,
@@ -433,6 +434,24 @@ export function deleteCircuitGroupCommand(
);
}
export function reorderCircuitGroupsCommand(
projectId: string,
expectedRevision: number,
circuitListId: string,
assignments: CircuitGroupReorderAssignment[]
) {
return executeProjectCommand(
projectId,
expectedRevision,
{
schemaVersion: 1,
type: "circuit-group.reorder",
payload: { circuitListId, assignments },
},
"Stromkreisgruppen sortieren"
);
}
export function updateCircuitProtectionCommand(
projectId: string,
expectedRevision: number,
@@ -7,6 +7,9 @@ import {
type CircuitGroupCategory,
} from "../../shared/constants/circuit-group";
import type { CircuitTreeSectionDto } from "../types";
import type {
CircuitGroupReorderAssignment,
} from "../../domain/models/circuit-group-structure-project-command.model";
export function toCircuitGroupSnapshot(
section: CircuitTreeSectionDto,
@@ -77,6 +80,41 @@ export function canDeleteCircuitGroup(
return section.circuits.length === 0 && section.components.length === 0;
}
export function buildCircuitGroupReorderAssignments(
sections: readonly CircuitTreeSectionDto[],
groupId: string,
direction: -1 | 1
): CircuitGroupReorderAssignment[] | null {
const current = sections.find((section) => section.id === groupId);
if (!current?.category) {
return null;
}
const categoryGroups = sections
.filter((section) => section.category === current.category)
.sort(
(left, right) =>
left.sortOrder - right.sortOrder ||
left.id.localeCompare(right.id)
);
const currentIndex = categoryGroups.findIndex(
(section) => section.id === groupId
);
const target = categoryGroups[currentIndex + direction];
if (!target) {
return null;
}
return sections.map((section) => ({
groupId: section.id,
expectedSortOrder: section.sortOrder,
targetSortOrder:
section.id === current.id
? target.sortOrder
: section.id === target.id
? current.sortOrder
: section.sortOrder,
}));
}
function getNewGroupSortOrder(
category: CircuitGroupCategory,
sections: readonly (CircuitTreeSectionDto & {
+49
View File
@@ -16,6 +16,7 @@ import "./circuit-group-move-project-command.repository.test.js";
import "./circuit-group-subtree-snapshot.test.js";
import "./circuit-group-subtree-project-command.repository.test.js";
import {
buildCircuitGroupReorderAssignments,
buildNewCircuitGroupSnapshot,
canDeleteCircuitGroup,
renameCircuitGroupSnapshot,
@@ -69,6 +70,54 @@ describe("circuit group numbering", () => {
);
});
it("reorders complete group lists only between category peers", () => {
const sections = [
groupSection("lighting-1", "lighting", 1, 10),
groupSection("lighting-2", "lighting", 2, 15),
groupSection("single-1", "single_phase", 1, 20),
];
assert.deepEqual(
buildCircuitGroupReorderAssignments(
sections,
"lighting-1",
1
),
[
{
groupId: "lighting-1",
expectedSortOrder: 10,
targetSortOrder: 15,
},
{
groupId: "lighting-2",
expectedSortOrder: 15,
targetSortOrder: 10,
},
{
groupId: "single-1",
expectedSortOrder: 20,
targetSortOrder: 20,
},
]
);
assert.equal(
buildCircuitGroupReorderAssignments(
sections,
"lighting-1",
-1
),
null
);
assert.equal(
buildCircuitGroupReorderAssignments(
sections,
"single-1",
1
),
null
);
});
it("formats the agreed identifiers including the leading hyphen", () => {
assert.equal(
formatGroupUpstreamProtectionIdentifier("lighting", 1),