Move circuits between groups
This commit is contained in:
@@ -395,6 +395,11 @@ vollständigen `circuit-group.renumber`-Plan für genau eine Kategorie. Sie
|
|||||||
nummeriert deren Gruppen gemäß aktueller Reihenfolge ab eins und ändert
|
nummeriert deren Gruppen gemäß aktueller Reihenfolge ab eins und ändert
|
||||||
Gruppenpräfixe, optionale Gruppen-Schutz-BMK und Stromkreis-BMK gemeinsam;
|
Gruppenpräfixe, optionale Gruppen-Schutz-BMK und Stromkreis-BMK gemeinsam;
|
||||||
die Stromkreisnummer hinter dem letzten Punkt bleibt erhalten.
|
die Stromkreisnummer hinter dem letzten Punkt bleibt erhalten.
|
||||||
|
Das vorhandene Stromkreis-Drag-Handle kann außerdem genau einen vollständigen
|
||||||
|
Stromkreis in eine andere Gruppe derselben Kategorie verschieben. Der atomare
|
||||||
|
`circuit.move-group`-Befehl erhält Gerätezeilen und Schutzdaten, setzt die
|
||||||
|
Zielposition und vergibt dort die höchste vorhandene Stromkreisnummer plus eins;
|
||||||
|
Lücken werden nicht automatisch gefüllt.
|
||||||
`distribution-board.update` versioniert Etage, Netzart und den
|
`distribution-board.update` versioniert Etage, Netzart und den
|
||||||
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
|
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
|
||||||
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
|
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ requirements and intended sequencing, not proof of implementation.
|
|||||||
- [x] Phase E4a: persistent same-category group reorder controls.
|
- [x] Phase E4a: persistent same-category group reorder controls.
|
||||||
- [ ] Phase E4b: explicit group renumber, circuit moves and populated-delete warning.
|
- [ ] Phase E4b: explicit group renumber, circuit moves and populated-delete warning.
|
||||||
- [x] Explicit same-category group renumbering updates prefixes and all child BMK atomically.
|
- [x] Explicit same-category group renumbering updates prefixes and all child BMK atomically.
|
||||||
- [ ] Circuit moves between same-category groups.
|
- [x] Single-circuit moves between same-category groups with automatic next-free BMK assignment.
|
||||||
- [ ] Populated-group delete warning and explicit subtree deletion.
|
- [ ] Populated-group delete warning and explicit subtree deletion.
|
||||||
- [ ] Phase E: editor projection and editing.
|
- [ ] Phase E: editor projection and editing.
|
||||||
- [ ] Phase F: documentation and full GUI verification.
|
- [ ] Phase F: documentation and full GUI verification.
|
||||||
|
|||||||
@@ -52,9 +52,11 @@ import {
|
|||||||
buildCircuitSectionRenumberAssignments,
|
buildCircuitSectionRenumberAssignments,
|
||||||
} from "../utils/circuit-section-renumber-command";
|
} from "../utils/circuit-section-renumber-command";
|
||||||
import {
|
import {
|
||||||
|
buildCircuitGroupMovePlan,
|
||||||
buildCircuitGroupRenumberPlan,
|
buildCircuitGroupRenumberPlan,
|
||||||
buildCircuitGroupReorderAssignments,
|
buildCircuitGroupReorderAssignments,
|
||||||
buildNewCircuitGroupSnapshot,
|
buildNewCircuitGroupSnapshot,
|
||||||
|
canMoveCircuitToGroup,
|
||||||
canRenumberCircuitGroups,
|
canRenumberCircuitGroups,
|
||||||
canDeleteCircuitGroup,
|
canDeleteCircuitGroup,
|
||||||
renameCircuitGroupSnapshot,
|
renameCircuitGroupSnapshot,
|
||||||
@@ -96,6 +98,7 @@ import {
|
|||||||
listProjectDevices,
|
listProjectDevices,
|
||||||
moveCircuitDeviceRowsCommand,
|
moveCircuitDeviceRowsCommand,
|
||||||
moveCircuitDeviceRowsToNewCircuitCommand,
|
moveCircuitDeviceRowsToNewCircuitCommand,
|
||||||
|
moveCircuitToGroupCommand,
|
||||||
reorderCircuitSectionCommand,
|
reorderCircuitSectionCommand,
|
||||||
reorderCircuitSectionsCommand,
|
reorderCircuitSectionsCommand,
|
||||||
reorderCircuitGroupsCommand,
|
reorderCircuitGroupsCommand,
|
||||||
@@ -2524,7 +2527,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles circuit drag intent and reorders whole circuit blocks within one section only.
|
// Reorders whole circuit blocks within a group or moves one complete circuit
|
||||||
|
// to another group of the same category.
|
||||||
async function handleCircuitReorderDrop(event: DragEvent<HTMLElement>, intent: CircuitReorderDropIntent) {
|
async function handleCircuitReorderDrop(event: DragEvent<HTMLElement>, intent: CircuitReorderDropIntent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -2538,8 +2542,75 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
setError("Der gezogene Stromkreis fehlt.");
|
setError("Der gezogene Stromkreis fehlt.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const sourceSectionIds = sourceCircuitIds
|
||||||
|
.map((id) => findCircuitSectionId(id))
|
||||||
|
.filter((id): id is string => Boolean(id));
|
||||||
|
const isCrossGroupMove =
|
||||||
|
sourceSectionIds.length === 1 &&
|
||||||
|
sourceSectionIds[0] !== intent.sectionId;
|
||||||
|
if (isCrossGroupMove) {
|
||||||
|
if (
|
||||||
|
sourceCircuitIds.length !== 1 ||
|
||||||
|
!canMoveCircuitToGroup(
|
||||||
|
data?.sections ?? [],
|
||||||
|
sourceCircuitIds[0],
|
||||||
|
intent.sectionId
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setError(
|
||||||
|
"Gruppenübergreifend kann genau ein Stromkreis in eine Gruppe derselben Kategorie verschoben werden."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let plan: ReturnType<typeof buildCircuitGroupMovePlan>;
|
||||||
|
try {
|
||||||
|
plan = buildCircuitGroupMovePlan({
|
||||||
|
sections: data?.sections ?? [],
|
||||||
|
circuitId: sourceCircuitIds[0],
|
||||||
|
targetSectionId: intent.sectionId,
|
||||||
|
placement:
|
||||||
|
intent.kind === "section-end"
|
||||||
|
? { kind: "end" }
|
||||||
|
: {
|
||||||
|
kind:
|
||||||
|
intent.kind === "before-circuit"
|
||||||
|
? "before"
|
||||||
|
: "after",
|
||||||
|
targetCircuitId: intent.targetCircuitId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
setError(normalizeUiError(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selectionIntent: SelectionIntent = {
|
||||||
|
rowKey: `circuitSummary:${plan.circuitId}`,
|
||||||
|
cellKey: "equipmentIdentifier",
|
||||||
|
rowType: "circuitSummary",
|
||||||
|
sectionId: plan.targetSectionId,
|
||||||
|
circuitId: plan.circuitId,
|
||||||
|
};
|
||||||
|
await runCommand({
|
||||||
|
label: "Stromkreis in andere Gruppe verschieben",
|
||||||
|
redo: async () => {
|
||||||
|
const result = await moveCircuitToGroupCommand(
|
||||||
|
projectId,
|
||||||
|
getExpectedProjectRevision(),
|
||||||
|
plan
|
||||||
|
);
|
||||||
|
applyProjectCommandResult(result);
|
||||||
|
pendingSelectedCircuitIdsAfterReload.current = [
|
||||||
|
plan.circuitId,
|
||||||
|
];
|
||||||
|
return selectionIntent;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!intent.valid) {
|
if (!intent.valid) {
|
||||||
setError("Stromkreise können derzeit nicht bereichsübergreifend verschoben werden.");
|
setError(
|
||||||
|
"Stromkreise können nur innerhalb einer Gruppe oder einzeln zwischen Gruppen derselben Kategorie verschoben werden."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const section = data?.sections.find((entry) => entry.id === intent.sectionId);
|
const section = data?.sections.find((entry) => entry.id === intent.sectionId);
|
||||||
@@ -2549,7 +2620,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
}
|
}
|
||||||
const sectionCircuitIds = new Set(section.circuits.map((circuit) => circuit.id));
|
const sectionCircuitIds = new Set(section.circuits.map((circuit) => circuit.id));
|
||||||
if (sourceCircuitIds.some((id) => !sectionCircuitIds.has(id))) {
|
if (sourceCircuitIds.some((id) => !sectionCircuitIds.has(id))) {
|
||||||
setError("Stromkreise können derzeit nicht bereichsübergreifend verschoben werden.");
|
setError(
|
||||||
|
"Die ausgewählten Stromkreise gehören nicht zur Zielgruppe."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const primaryCircuitId = sourceCircuitIds[0];
|
const primaryCircuitId = sourceCircuitIds[0];
|
||||||
@@ -2892,6 +2965,23 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
const activeDraggedCircuitIds =
|
const activeDraggedCircuitIds =
|
||||||
draggingCircuitIds.length > 0 ? draggingCircuitIds : draggingCircuitId ? [draggingCircuitId] : [];
|
draggingCircuitIds.length > 0 ? draggingCircuitIds : draggingCircuitId ? [draggingCircuitId] : [];
|
||||||
const draggingCircuitCount = activeDraggedCircuitIds.length;
|
const draggingCircuitCount = activeDraggedCircuitIds.length;
|
||||||
|
const isCircuitDropTargetValid = (targetSectionId: string) => {
|
||||||
|
const sourceSectionIds = activeDraggedCircuitIds
|
||||||
|
.map((id) => findCircuitSectionId(id))
|
||||||
|
.filter((id): id is string => Boolean(id));
|
||||||
|
return (
|
||||||
|
(sourceSectionIds.length > 0 &&
|
||||||
|
sourceSectionIds.every(
|
||||||
|
(sectionId) => sectionId === targetSectionId
|
||||||
|
)) ||
|
||||||
|
(activeDraggedCircuitIds.length === 1 &&
|
||||||
|
canMoveCircuitToGroup(
|
||||||
|
data.sections,
|
||||||
|
activeDraggedCircuitIds[0],
|
||||||
|
targetSectionId
|
||||||
|
))
|
||||||
|
);
|
||||||
|
};
|
||||||
const selectedProjectDevice = resolveSelectedProjectDevice();
|
const selectedProjectDevice = resolveSelectedProjectDevice();
|
||||||
const suggestedSection = selectedProjectDevice
|
const suggestedSection = selectedProjectDevice
|
||||||
? data.sections.find((section) => section.key === inferProjectDeviceSectionKey(selectedProjectDevice))
|
? data.sections.find((section) => section.key === inferProjectDeviceSectionKey(selectedProjectDevice))
|
||||||
@@ -3479,12 +3569,17 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
}`}
|
}`}
|
||||||
onDragOver={(event) => {
|
onDragOver={(event) => {
|
||||||
if (draggingCircuitCount > 0) {
|
if (draggingCircuitCount > 0) {
|
||||||
|
const valid = isCircuitDropTargetValid(
|
||||||
|
section.id
|
||||||
|
);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.dataTransfer.dropEffect = "none";
|
event.dataTransfer.dropEffect = valid
|
||||||
|
? "move"
|
||||||
|
: "none";
|
||||||
setCircuitReorderIntent({
|
setCircuitReorderIntent({
|
||||||
kind: "section-end",
|
kind: "section-end",
|
||||||
sectionId: section.id,
|
sectionId: section.id,
|
||||||
valid: false,
|
valid,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3519,7 +3614,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
void handleCircuitReorderDrop(event, {
|
void handleCircuitReorderDrop(event, {
|
||||||
kind: "section-end",
|
kind: "section-end",
|
||||||
sectionId: section.id,
|
sectionId: section.id,
|
||||||
valid: false,
|
valid: isCircuitDropTargetValid(section.id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -3803,10 +3898,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
onClick={() => setActiveSectionId(row.sectionId)}
|
onClick={() => setActiveSectionId(row.sectionId)}
|
||||||
onDragOver={(event) => {
|
onDragOver={(event) => {
|
||||||
if (draggingCircuitCount > 0) {
|
if (draggingCircuitCount > 0) {
|
||||||
const sourceSectionIds = activeDraggedCircuitIds
|
const valid = isCircuitDropTargetValid(
|
||||||
.map((id) => findCircuitSectionId(id))
|
row.sectionId
|
||||||
.filter((id): id is string => Boolean(id));
|
);
|
||||||
const valid = sourceSectionIds.length > 0 && sourceSectionIds.every((sectionId) => sectionId === row.sectionId);
|
|
||||||
if (row.rowType === "placeholder") {
|
if (row.rowType === "placeholder") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.dataTransfer.dropEffect = valid ? "move" : "none";
|
event.dataTransfer.dropEffect = valid ? "move" : "none";
|
||||||
@@ -3929,13 +4023,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
onDrop={(event) => {
|
onDrop={(event) => {
|
||||||
if (draggingCircuitCount > 0) {
|
if (draggingCircuitCount > 0) {
|
||||||
if (row.rowType === "placeholder") {
|
if (row.rowType === "placeholder") {
|
||||||
const sourceSectionIds = activeDraggedCircuitIds
|
|
||||||
.map((id) => findCircuitSectionId(id))
|
|
||||||
.filter((id): id is string => Boolean(id));
|
|
||||||
void handleCircuitReorderDrop(event, {
|
void handleCircuitReorderDrop(event, {
|
||||||
kind: "section-end",
|
kind: "section-end",
|
||||||
sectionId: row.sectionId,
|
sectionId: row.sectionId,
|
||||||
valid: sourceSectionIds.length > 0 && sourceSectionIds.every((sectionId) => sectionId === row.sectionId),
|
valid: isCircuitDropTargetValid(row.sectionId),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3943,16 +4034,13 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
if (activeDraggedCircuitIds.includes(row.circuit.id)) {
|
if (activeDraggedCircuitIds.includes(row.circuit.id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sourceSectionIds = activeDraggedCircuitIds
|
|
||||||
.map((id) => findCircuitSectionId(id))
|
|
||||||
.filter((id): id is string => Boolean(id));
|
|
||||||
const rect = (event.currentTarget as HTMLTableRowElement).getBoundingClientRect();
|
const rect = (event.currentTarget as HTMLTableRowElement).getBoundingClientRect();
|
||||||
const isAfter = event.clientY > rect.top + rect.height / 2;
|
const isAfter = event.clientY > rect.top + rect.height / 2;
|
||||||
void handleCircuitReorderDrop(event, {
|
void handleCircuitReorderDrop(event, {
|
||||||
kind: isAfter ? "after-circuit" : "before-circuit",
|
kind: isAfter ? "after-circuit" : "before-circuit",
|
||||||
sectionId: row.sectionId,
|
sectionId: row.sectionId,
|
||||||
targetCircuitId: row.circuit.id,
|
targetCircuitId: row.circuit.id,
|
||||||
valid: sourceSectionIds.length > 0 && sourceSectionIds.every((sectionId) => sectionId === row.sectionId),
|
valid: isCircuitDropTargetValid(row.sectionId),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -4283,7 +4371,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
<span className="drop-hint">
|
<span className="drop-hint">
|
||||||
{circuitReorderIntent.valid
|
{circuitReorderIntent.valid
|
||||||
? `${draggingCircuitCount || 1} Stromkreis(e) ans Bereichsende verschieben`
|
? `${draggingCircuitCount || 1} Stromkreis(e) ans Bereichsende verschieben`
|
||||||
: "Bereichsübergreifendes Verschieben nicht zulässig"}
|
: "Nur ein Stromkreis kann zwischen Gruppen derselben Kategorie verschoben werden"}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{circuitReorderIntent &&
|
{circuitReorderIntent &&
|
||||||
@@ -4294,7 +4382,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
? circuitReorderIntent.kind === "before-circuit"
|
? circuitReorderIntent.kind === "before-circuit"
|
||||||
? `${draggingCircuitCount || 1} Stromkreis(e) vor diesen Stromkreis verschieben`
|
? `${draggingCircuitCount || 1} Stromkreis(e) vor diesen Stromkreis verschieben`
|
||||||
: `${draggingCircuitCount || 1} Stromkreis(e) hinter diesen Stromkreis verschieben`
|
: `${draggingCircuitCount || 1} Stromkreis(e) hinter diesen Stromkreis verschieben`
|
||||||
: "Bereichsübergreifendes Verschieben nicht zulässig"}
|
: "Nur ein Stromkreis kann zwischen Gruppen derselben Kategorie verschoben werden"}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ import type {
|
|||||||
import type {
|
import type {
|
||||||
CircuitGroupRenumberPlan,
|
CircuitGroupRenumberPlan,
|
||||||
} from "../../domain/services/circuit-group-renumbering";
|
} from "../../domain/services/circuit-group-renumbering";
|
||||||
|
import type {
|
||||||
|
CircuitGroupMovePlan,
|
||||||
|
} from "../../domain/services/circuit-group-move-planning";
|
||||||
|
|
||||||
async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@@ -473,6 +476,23 @@ export function renumberCircuitGroupsCommand(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function moveCircuitToGroupCommand(
|
||||||
|
projectId: string,
|
||||||
|
expectedRevision: number,
|
||||||
|
plan: CircuitGroupMovePlan
|
||||||
|
) {
|
||||||
|
return executeProjectCommand(
|
||||||
|
projectId,
|
||||||
|
expectedRevision,
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
type: "circuit.move-group",
|
||||||
|
payload: plan,
|
||||||
|
},
|
||||||
|
"Stromkreis in andere Gruppe verschieben"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function updateCircuitProtectionCommand(
|
export function updateCircuitProtectionCommand(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
expectedRevision: number,
|
expectedRevision: number,
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import type {
|
|||||||
import type {
|
import type {
|
||||||
CircuitGroupRenumberPlan,
|
CircuitGroupRenumberPlan,
|
||||||
} from "../../domain/services/circuit-group-renumbering";
|
} from "../../domain/services/circuit-group-renumbering";
|
||||||
|
import type {
|
||||||
|
CircuitGroupMovePlan,
|
||||||
|
} from "../../domain/services/circuit-group-move-planning";
|
||||||
|
|
||||||
export function toCircuitGroupSnapshot(
|
export function toCircuitGroupSnapshot(
|
||||||
section: CircuitTreeSectionDto,
|
section: CircuitTreeSectionDto,
|
||||||
@@ -187,6 +190,92 @@ export function canRenumberCircuitGroups(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canMoveCircuitToGroup(
|
||||||
|
sections: readonly CircuitTreeSectionDto[],
|
||||||
|
circuitId: string,
|
||||||
|
targetSectionId: string
|
||||||
|
) {
|
||||||
|
const source = sections.find((section) =>
|
||||||
|
section.circuits.some((circuit) => circuit.id === circuitId)
|
||||||
|
);
|
||||||
|
const target = sections.find(
|
||||||
|
(section) => section.id === targetSectionId
|
||||||
|
);
|
||||||
|
return Boolean(
|
||||||
|
source?.category &&
|
||||||
|
target?.category &&
|
||||||
|
source.id !== target.id &&
|
||||||
|
source.category === target.category
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCircuitGroupMovePlan(input: {
|
||||||
|
sections: readonly CircuitTreeSectionDto[];
|
||||||
|
circuitId: string;
|
||||||
|
targetSectionId: string;
|
||||||
|
placement:
|
||||||
|
| { kind: "end" }
|
||||||
|
| {
|
||||||
|
kind: "before" | "after";
|
||||||
|
targetCircuitId: string;
|
||||||
|
};
|
||||||
|
}): CircuitGroupMovePlan {
|
||||||
|
const sourceGroup = input.sections.find((section) =>
|
||||||
|
section.circuits.some(
|
||||||
|
(circuit) => circuit.id === input.circuitId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const targetGroup = input.sections.find(
|
||||||
|
(section) => section.id === input.targetSectionId
|
||||||
|
);
|
||||||
|
const circuit = sourceGroup?.circuits.find(
|
||||||
|
(entry) => entry.id === input.circuitId
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!sourceGroup?.category ||
|
||||||
|
!sourceGroup.groupNumber ||
|
||||||
|
!targetGroup?.category ||
|
||||||
|
!targetGroup.groupNumber ||
|
||||||
|
!circuit ||
|
||||||
|
sourceGroup.id === targetGroup.id ||
|
||||||
|
sourceGroup.category !== targetGroup.category
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Stromkreise können nur zwischen verschiedenen Gruppen derselben Kategorie verschoben werden."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
parseCircuitNumber(
|
||||||
|
circuit.equipmentIdentifier,
|
||||||
|
sourceGroup.category,
|
||||||
|
sourceGroup.groupNumber
|
||||||
|
);
|
||||||
|
const targetCircuitNumbers = targetGroup.circuits.map((entry) =>
|
||||||
|
parseCircuitNumber(
|
||||||
|
entry.equipmentIdentifier,
|
||||||
|
targetGroup.category!,
|
||||||
|
targetGroup.groupNumber!
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
circuitId: circuit.id,
|
||||||
|
circuitListId: circuit.circuitListId,
|
||||||
|
expectedSectionId: sourceGroup.id,
|
||||||
|
targetSectionId: targetGroup.id,
|
||||||
|
expectedEquipmentIdentifier: circuit.equipmentIdentifier,
|
||||||
|
targetEquipmentIdentifier: formatCircuitIdentifier(
|
||||||
|
targetGroup.category,
|
||||||
|
targetGroup.groupNumber,
|
||||||
|
Math.max(0, ...targetCircuitNumbers) + 1
|
||||||
|
),
|
||||||
|
expectedSortOrder: circuit.sortOrder,
|
||||||
|
targetSortOrder: getCircuitMoveSortOrder(
|
||||||
|
targetGroup.circuits,
|
||||||
|
input.placement
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getNewGroupSortOrder(
|
function getNewGroupSortOrder(
|
||||||
category: CircuitGroupCategory,
|
category: CircuitGroupCategory,
|
||||||
sections: readonly (CircuitTreeSectionDto & {
|
sections: readonly (CircuitTreeSectionDto & {
|
||||||
@@ -287,3 +376,41 @@ function formatGroupComponentIdentifier(
|
|||||||
"Die Stromkreisgruppe enthält eine nicht unterstützte Verteilerkomponente."
|
"Die Stromkreisgruppe enthält eine nicht unterstützte Verteilerkomponente."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCircuitMoveSortOrder(
|
||||||
|
circuits: CircuitTreeSectionDto["circuits"],
|
||||||
|
placement:
|
||||||
|
| { kind: "end" }
|
||||||
|
| {
|
||||||
|
kind: "before" | "after";
|
||||||
|
targetCircuitId: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const ordered = [...circuits].sort(
|
||||||
|
(left, right) =>
|
||||||
|
left.sortOrder - right.sortOrder ||
|
||||||
|
left.id.localeCompare(right.id)
|
||||||
|
);
|
||||||
|
if (placement.kind === "end") {
|
||||||
|
return ordered.length === 0
|
||||||
|
? 10
|
||||||
|
: ordered[ordered.length - 1].sortOrder + 10;
|
||||||
|
}
|
||||||
|
const index = ordered.findIndex(
|
||||||
|
(circuit) => circuit.id === placement.targetCircuitId
|
||||||
|
);
|
||||||
|
if (index < 0) {
|
||||||
|
throw new Error("Der Zielstromkreis wurde nicht gefunden.");
|
||||||
|
}
|
||||||
|
const current = ordered[index].sortOrder;
|
||||||
|
if (placement.kind === "before") {
|
||||||
|
const previous = ordered[index - 1]?.sortOrder;
|
||||||
|
return previous === undefined
|
||||||
|
? current - 10
|
||||||
|
: previous + (current - previous) / 2;
|
||||||
|
}
|
||||||
|
const next = ordered[index + 1]?.sortOrder;
|
||||||
|
return next === undefined
|
||||||
|
? current + 10
|
||||||
|
: current + (next - current) / 2;
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ import "./circuit-group-move-project-command.repository.test.js";
|
|||||||
import "./circuit-group-subtree-snapshot.test.js";
|
import "./circuit-group-subtree-snapshot.test.js";
|
||||||
import "./circuit-group-subtree-project-command.repository.test.js";
|
import "./circuit-group-subtree-project-command.repository.test.js";
|
||||||
import {
|
import {
|
||||||
|
buildCircuitGroupMovePlan,
|
||||||
buildCircuitGroupRenumberPlan,
|
buildCircuitGroupRenumberPlan,
|
||||||
buildCircuitGroupReorderAssignments,
|
buildCircuitGroupReorderAssignments,
|
||||||
buildNewCircuitGroupSnapshot,
|
buildNewCircuitGroupSnapshot,
|
||||||
|
canMoveCircuitToGroup,
|
||||||
canRenumberCircuitGroups,
|
canRenumberCircuitGroups,
|
||||||
canDeleteCircuitGroup,
|
canDeleteCircuitGroup,
|
||||||
renameCircuitGroupSnapshot,
|
renameCircuitGroupSnapshot,
|
||||||
@@ -219,6 +221,79 @@ describe("circuit group numbering", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("plans a same-category circuit drop with the next target BMK", () => {
|
||||||
|
const source = {
|
||||||
|
...groupSection("lighting-1", "lighting", 1, 10),
|
||||||
|
circuits: [
|
||||||
|
{
|
||||||
|
id: "circuit-source",
|
||||||
|
circuitListId: "list-1",
|
||||||
|
sectionId: "lighting-1",
|
||||||
|
equipmentIdentifier: "-1F1.5",
|
||||||
|
sortOrder: 10,
|
||||||
|
isReserve: false,
|
||||||
|
circuitTotalPower: 0,
|
||||||
|
deviceRows: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const target = {
|
||||||
|
...groupSection("lighting-2", "lighting", 2, 20),
|
||||||
|
circuits: [
|
||||||
|
{
|
||||||
|
id: "circuit-target",
|
||||||
|
circuitListId: "list-1",
|
||||||
|
sectionId: "lighting-2",
|
||||||
|
equipmentIdentifier: "-1F2.3",
|
||||||
|
sortOrder: 20,
|
||||||
|
isReserve: false,
|
||||||
|
circuitTotalPower: 0,
|
||||||
|
deviceRows: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
canMoveCircuitToGroup(
|
||||||
|
[source, target],
|
||||||
|
"circuit-source",
|
||||||
|
"lighting-2"
|
||||||
|
),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
buildCircuitGroupMovePlan({
|
||||||
|
sections: [source, target],
|
||||||
|
circuitId: "circuit-source",
|
||||||
|
targetSectionId: "lighting-2",
|
||||||
|
placement: {
|
||||||
|
kind: "before",
|
||||||
|
targetCircuitId: "circuit-target",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
circuitId: "circuit-source",
|
||||||
|
circuitListId: "list-1",
|
||||||
|
expectedSectionId: "lighting-1",
|
||||||
|
targetSectionId: "lighting-2",
|
||||||
|
expectedEquipmentIdentifier: "-1F1.5",
|
||||||
|
targetEquipmentIdentifier: "-1F2.4",
|
||||||
|
expectedSortOrder: 10,
|
||||||
|
targetSortOrder: 10,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
canMoveCircuitToGroup(
|
||||||
|
[
|
||||||
|
source,
|
||||||
|
groupSection("single-1", "single_phase", 1, 20),
|
||||||
|
],
|
||||||
|
"circuit-source",
|
||||||
|
"single-1"
|
||||||
|
),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("formats the agreed identifiers including the leading hyphen", () => {
|
it("formats the agreed identifiers including the leading hyphen", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
formatGroupUpstreamProtectionIdentifier("lighting", 1),
|
formatGroupUpstreamProtectionIdentifier("lighting", 1),
|
||||||
|
|||||||
Reference in New Issue
Block a user