Renumber circuit groups explicitly
This commit is contained in:
@@ -390,6 +390,11 @@ 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.
|
||||
Eine separate, ausdrücklich bestätigte Editoraktion erzeugt dagegen einen
|
||||
vollständigen `circuit-group.renumber`-Plan für genau eine Kategorie. Sie
|
||||
nummeriert deren Gruppen gemäß aktueller Reihenfolge ab eins und ändert
|
||||
Gruppenpräfixe, optionale Gruppen-Schutz-BMK und Stromkreis-BMK gemeinsam;
|
||||
die Stromkreisnummer hinter dem letzten Punkt bleibt erhalten.
|
||||
`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
|
||||
|
||||
@@ -40,6 +40,9 @@ requirements and intended sequencing, not proof of implementation.
|
||||
- [x] Phase E3b2b: circuit-protection defaults on insert and editor modal.
|
||||
- [x] Phase E4a: persistent same-category group reorder controls.
|
||||
- [ ] Phase E4b: explicit group renumber, circuit moves and populated-delete warning.
|
||||
- [x] Explicit same-category group renumbering updates prefixes and all child BMK atomically.
|
||||
- [ ] Circuit moves between same-category groups.
|
||||
- [ ] Populated-group delete warning and explicit subtree deletion.
|
||||
- [ ] Phase E: editor projection and editing.
|
||||
- [ ] Phase F: documentation and full GUI verification.
|
||||
- [ ] Keep full electrical sizing and cable-dimensioning rules separate until
|
||||
|
||||
@@ -52,8 +52,10 @@ import {
|
||||
buildCircuitSectionRenumberAssignments,
|
||||
} from "../utils/circuit-section-renumber-command";
|
||||
import {
|
||||
buildCircuitGroupRenumberPlan,
|
||||
buildCircuitGroupReorderAssignments,
|
||||
buildNewCircuitGroupSnapshot,
|
||||
canRenumberCircuitGroups,
|
||||
canDeleteCircuitGroup,
|
||||
renameCircuitGroupSnapshot,
|
||||
toCircuitGroupSnapshot,
|
||||
@@ -97,6 +99,7 @@ import {
|
||||
reorderCircuitSectionCommand,
|
||||
reorderCircuitSectionsCommand,
|
||||
reorderCircuitGroupsCommand,
|
||||
renumberCircuitGroupsCommand,
|
||||
renumberCircuitSectionCommand,
|
||||
redoProjectCommand,
|
||||
updateCircuitById,
|
||||
@@ -126,7 +129,10 @@ import type {
|
||||
import { DistributionBoardComponentModal } from "./distribution-board-component-modal";
|
||||
import { CircuitGroupModal } from "./circuit-group-modal";
|
||||
import { CircuitProtectionModal } from "./circuit-protection-modal";
|
||||
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group";
|
||||
import {
|
||||
circuitGroupCategoryLabels,
|
||||
type CircuitGroupCategory,
|
||||
} from "../../shared/constants/circuit-group";
|
||||
|
||||
type SaveDirection = "stay" | "next" | "prev";
|
||||
type StartEditMode = "selectExisting" | "replaceWithTypedChar";
|
||||
@@ -1267,6 +1273,44 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
});
|
||||
}
|
||||
|
||||
async function handleRenumberCircuitGroups(
|
||||
category: CircuitGroupCategory
|
||||
) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
let plan: ReturnType<typeof buildCircuitGroupRenumberPlan>;
|
||||
try {
|
||||
plan = buildCircuitGroupRenumberPlan(data.sections, category);
|
||||
} catch (err) {
|
||||
setError(normalizeUiError(err));
|
||||
return;
|
||||
}
|
||||
if (!plan) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!confirm(
|
||||
`Alle Gruppen der Kategorie „${circuitGroupCategoryLabels[category]}“ gemäß ihrer aktuellen Reihenfolge neu nummerieren? Gruppennummern, Präfixe und zugehörige BMK werden dabei gemeinsam geändert.`
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
await runCommand({
|
||||
label: "Stromkreisgruppen neu nummerieren",
|
||||
redo: async () => {
|
||||
const result = await renumberCircuitGroupsCommand(
|
||||
projectId,
|
||||
getExpectedProjectRevision(),
|
||||
circuitListId,
|
||||
plan
|
||||
);
|
||||
applyProjectCommandResult(result);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSaveCircuitProtection(
|
||||
protection: CircuitTreeProtectionDeviceDto
|
||||
) {
|
||||
@@ -3543,6 +3587,32 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
>
|
||||
Gruppe nach unten
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
disabled={
|
||||
hasActiveSortOrFilter ||
|
||||
!section.category ||
|
||||
!canRenumberCircuitGroups(
|
||||
data.sections,
|
||||
section.category
|
||||
)
|
||||
}
|
||||
onClick={() =>
|
||||
section.category
|
||||
? void handleRenumberCircuitGroups(
|
||||
section.category
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
title={
|
||||
hasActiveSortOrFilter
|
||||
? "Vor der Gruppenneunummerierung Sortierung und Filter zurücksetzen."
|
||||
: "Alle Gruppen dieser Kategorie gemäß ihrer Reihenfolge neu nummerieren"
|
||||
}
|
||||
>
|
||||
Gruppen-BMK neu nummerieren
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
|
||||
@@ -62,6 +62,9 @@ import type {
|
||||
import type {
|
||||
CircuitProtectionSnapshot,
|
||||
} from "../../domain/models/circuit-protection-project-command.model";
|
||||
import type {
|
||||
CircuitGroupRenumberPlan,
|
||||
} from "../../domain/services/circuit-group-renumbering";
|
||||
|
||||
async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(url, {
|
||||
@@ -452,6 +455,24 @@ export function reorderCircuitGroupsCommand(
|
||||
);
|
||||
}
|
||||
|
||||
export function renumberCircuitGroupsCommand(
|
||||
projectId: string,
|
||||
expectedRevision: number,
|
||||
circuitListId: string,
|
||||
plan: CircuitGroupRenumberPlan
|
||||
) {
|
||||
return executeProjectCommand(
|
||||
projectId,
|
||||
expectedRevision,
|
||||
{
|
||||
schemaVersion: 1,
|
||||
type: "circuit-group.renumber",
|
||||
payload: { circuitListId, groups: plan.groups },
|
||||
},
|
||||
"Stromkreisgruppen neu nummerieren"
|
||||
);
|
||||
}
|
||||
|
||||
export function updateCircuitProtectionCommand(
|
||||
projectId: string,
|
||||
expectedRevision: number,
|
||||
|
||||
@@ -10,6 +10,9 @@ import type { CircuitTreeSectionDto } from "../types";
|
||||
import type {
|
||||
CircuitGroupReorderAssignment,
|
||||
} from "../../domain/models/circuit-group-structure-project-command.model";
|
||||
import type {
|
||||
CircuitGroupRenumberPlan,
|
||||
} from "../../domain/services/circuit-group-renumbering";
|
||||
|
||||
export function toCircuitGroupSnapshot(
|
||||
section: CircuitTreeSectionDto,
|
||||
@@ -115,6 +118,75 @@ export function buildCircuitGroupReorderAssignments(
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildCircuitGroupRenumberPlan(
|
||||
sections: readonly CircuitTreeSectionDto[],
|
||||
category: CircuitGroupCategory
|
||||
): CircuitGroupRenumberPlan | null {
|
||||
const groups = getOrderedCategoryGroups(sections, category);
|
||||
if (!canRenumberCircuitGroups(sections, category)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
groups: groups.map((group, index) => {
|
||||
const targetGroupNumber = index + 1;
|
||||
const expectedPrefix = formatGroupPrefix(
|
||||
category,
|
||||
group.groupNumber
|
||||
);
|
||||
if (group.prefix !== expectedPrefix) {
|
||||
throw new Error(
|
||||
`Das Präfix der Gruppe „${group.displayName}“ passt nicht zu ihrer Gruppennummer.`
|
||||
);
|
||||
}
|
||||
return {
|
||||
groupId: group.id,
|
||||
category,
|
||||
expectedGroupNumber: group.groupNumber,
|
||||
targetGroupNumber,
|
||||
expectedPrefix,
|
||||
targetPrefix: formatGroupPrefix(category, targetGroupNumber),
|
||||
circuits: group.circuits.map((circuit) => {
|
||||
const circuitNumber = parseCircuitNumber(
|
||||
circuit.equipmentIdentifier,
|
||||
category,
|
||||
group.groupNumber
|
||||
);
|
||||
return {
|
||||
circuitId: circuit.id,
|
||||
expectedEquipmentIdentifier: circuit.equipmentIdentifier,
|
||||
targetEquipmentIdentifier: formatCircuitIdentifier(
|
||||
category,
|
||||
targetGroupNumber,
|
||||
circuitNumber
|
||||
),
|
||||
};
|
||||
}),
|
||||
components: group.components.map((component) => ({
|
||||
componentId: component.id,
|
||||
expectedEquipmentIdentifier: component.equipmentIdentifier,
|
||||
targetEquipmentIdentifier: formatGroupComponentIdentifier(
|
||||
category,
|
||||
targetGroupNumber,
|
||||
component.role
|
||||
),
|
||||
})),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function canRenumberCircuitGroups(
|
||||
sections: readonly CircuitTreeSectionDto[],
|
||||
category: CircuitGroupCategory
|
||||
) {
|
||||
const groups = getOrderedCategoryGroups(sections, category);
|
||||
return (
|
||||
groups.length > 0 &&
|
||||
groups.some((group, index) => group.groupNumber !== index + 1)
|
||||
);
|
||||
}
|
||||
|
||||
function getNewGroupSortOrder(
|
||||
category: CircuitGroupCategory,
|
||||
sections: readonly (CircuitTreeSectionDto & {
|
||||
@@ -141,3 +213,77 @@ function getNewGroupSortOrder(
|
||||
? previous.sortOrder + (next.sortOrder - previous.sortOrder) / 2
|
||||
: previous.sortOrder + 10;
|
||||
}
|
||||
|
||||
function formatGroupPrefix(
|
||||
category: CircuitGroupCategory,
|
||||
groupNumber: number
|
||||
) {
|
||||
return `-${circuitGroupCategoryNumbers[category]}F${groupNumber}.`;
|
||||
}
|
||||
|
||||
function getOrderedCategoryGroups(
|
||||
sections: readonly CircuitTreeSectionDto[],
|
||||
category: CircuitGroupCategory
|
||||
) {
|
||||
return sections
|
||||
.filter(
|
||||
(section): section is CircuitTreeSectionDto & {
|
||||
category: CircuitGroupCategory;
|
||||
groupNumber: number;
|
||||
} =>
|
||||
section.category === category &&
|
||||
Number.isInteger(section.groupNumber) &&
|
||||
(section.groupNumber ?? 0) > 0
|
||||
)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
left.sortOrder - right.sortOrder ||
|
||||
left.id.localeCompare(right.id)
|
||||
);
|
||||
}
|
||||
|
||||
function formatCircuitIdentifier(
|
||||
category: CircuitGroupCategory,
|
||||
groupNumber: number,
|
||||
circuitNumber: number
|
||||
) {
|
||||
return `${formatGroupPrefix(category, groupNumber)}${circuitNumber}`;
|
||||
}
|
||||
|
||||
function parseCircuitNumber(
|
||||
equipmentIdentifier: string,
|
||||
category: CircuitGroupCategory,
|
||||
groupNumber: number
|
||||
) {
|
||||
const prefix = formatGroupPrefix(category, groupNumber);
|
||||
const suffix = equipmentIdentifier.startsWith(prefix)
|
||||
? equipmentIdentifier.slice(prefix.length)
|
||||
: "";
|
||||
const circuitNumber = Number(suffix);
|
||||
if (
|
||||
!/^[1-9]\d*$/.test(suffix) ||
|
||||
!Number.isSafeInteger(circuitNumber)
|
||||
) {
|
||||
throw new Error(
|
||||
`Das Stromkreis-BMK „${equipmentIdentifier}“ passt nicht zu seiner Gruppe.`
|
||||
);
|
||||
}
|
||||
return circuitNumber;
|
||||
}
|
||||
|
||||
function formatGroupComponentIdentifier(
|
||||
category: CircuitGroupCategory,
|
||||
groupNumber: number,
|
||||
role: CircuitTreeSectionDto["components"][number]["role"]
|
||||
) {
|
||||
const categoryNumber = circuitGroupCategoryNumbers[category];
|
||||
if (role === "group_upstream_protection") {
|
||||
return `-${categoryNumber}F${groupNumber}.0`;
|
||||
}
|
||||
if (role === "group_residual_current_protection") {
|
||||
return `-${categoryNumber}Q${groupNumber}.0`;
|
||||
}
|
||||
throw new Error(
|
||||
"Die Stromkreisgruppe enthält eine nicht unterstützte Verteilerkomponente."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,8 +16,10 @@ 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 {
|
||||
buildCircuitGroupRenumberPlan,
|
||||
buildCircuitGroupReorderAssignments,
|
||||
buildNewCircuitGroupSnapshot,
|
||||
canRenumberCircuitGroups,
|
||||
canDeleteCircuitGroup,
|
||||
renameCircuitGroupSnapshot,
|
||||
} from "../src/frontend/utils/circuit-group-editing.js";
|
||||
@@ -118,6 +120,105 @@ describe("circuit group numbering", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("renumbers a complete category by group order and preserves circuit suffixes", () => {
|
||||
const sections = [
|
||||
{
|
||||
...groupSection("lighting-3", "lighting", 3, 10),
|
||||
prefix: "-1F3.",
|
||||
circuits: [
|
||||
{
|
||||
id: "circuit-7",
|
||||
circuitListId: "list-1",
|
||||
sectionId: "lighting-3",
|
||||
equipmentIdentifier: "-1F3.7",
|
||||
sortOrder: 10,
|
||||
isReserve: false,
|
||||
circuitTotalPower: 0,
|
||||
deviceRows: [],
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
id: "rcd-3",
|
||||
circuitListId: "list-1",
|
||||
sectionId: "lighting-3",
|
||||
equipmentIdentifier: "-1Q3.0",
|
||||
name: "Gruppen-FI",
|
||||
role: "group_residual_current_protection" as const,
|
||||
placement: "group" as const,
|
||||
sortOrder: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...groupSection("lighting-1", "lighting", 1, 20),
|
||||
prefix: "-1F1.",
|
||||
},
|
||||
groupSection("single-1", "single_phase", 1, 30),
|
||||
];
|
||||
|
||||
assert.deepEqual(buildCircuitGroupRenumberPlan(sections, "lighting"), {
|
||||
groups: [
|
||||
{
|
||||
groupId: "lighting-3",
|
||||
category: "lighting",
|
||||
expectedGroupNumber: 3,
|
||||
targetGroupNumber: 1,
|
||||
expectedPrefix: "-1F3.",
|
||||
targetPrefix: "-1F1.",
|
||||
circuits: [
|
||||
{
|
||||
circuitId: "circuit-7",
|
||||
expectedEquipmentIdentifier: "-1F3.7",
|
||||
targetEquipmentIdentifier: "-1F1.7",
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
componentId: "rcd-3",
|
||||
expectedEquipmentIdentifier: "-1Q3.0",
|
||||
targetEquipmentIdentifier: "-1Q1.0",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupId: "lighting-1",
|
||||
category: "lighting",
|
||||
expectedGroupNumber: 1,
|
||||
targetGroupNumber: 2,
|
||||
expectedPrefix: "-1F1.",
|
||||
targetPrefix: "-1F2.",
|
||||
circuits: [],
|
||||
components: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.equal(
|
||||
buildCircuitGroupRenumberPlan(
|
||||
[
|
||||
groupSection("lighting-1", "lighting", 1, 10),
|
||||
groupSection("lighting-2", "lighting", 2, 20),
|
||||
],
|
||||
"lighting"
|
||||
),
|
||||
null
|
||||
);
|
||||
assert.equal(
|
||||
canRenumberCircuitGroups(sections, "lighting"),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
canRenumberCircuitGroups(
|
||||
[
|
||||
groupSection("lighting-1", "lighting", 1, 10),
|
||||
groupSection("lighting-2", "lighting", 2, 20),
|
||||
],
|
||||
"lighting"
|
||||
),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("formats the agreed identifiers including the leading hyphen", () => {
|
||||
assert.equal(
|
||||
formatGroupUpstreamProtectionIdentifier("lighting", 1),
|
||||
|
||||
Reference in New Issue
Block a user