Add distribution board copy and delete controls

This commit is contained in:
2026-07-31 12:03:52 +02:00
parent 86f1d42e60
commit 5bee3cb103
7 changed files with 196 additions and 2 deletions
+7
View File
@@ -301,6 +301,13 @@ inverse removes only the same unchanged and still-empty structure; the POST
route requires `expectedRevision` and returns the updated history state.
Stored schema-version 1 and 2 setup commands remain executable with their four
legacy sections and without invented components.
Complete populated distribution-board copying and deletion use
`distribution-board.insert-subtree` and `distribution-board.delete-subtree`.
Their exact snapshots include the board, circuit list, groups, circuits,
device rows, components and all one-to-one protection data. Copy remaps all
owning ids but preserves project-device and room links. Both operations are
atomic persistent commands with restart-safe Undo/Redo; deletion requires an
explicit UI warning.
Mutable group-protection and auxiliary distribution-board components use
`distribution-board-component.insert` and
`distribution-board-component.delete`; edits and footer reordering use
+15
View File
@@ -233,6 +233,21 @@ returns HTTP `409` with `PROJECT_HISTORY_OPERATION_UNAVAILABLE`.
`{ "floorId": null, "supplyType": "AV", "expectedRevision": 13 }`
- executes `distribution-board.update`; floor and supply type are restored
together by persistent Undo/Redo
- `POST /projects/:projectId/distribution-boards/:distributionBoardId/copy`
- body: `{ "name": "UV-02 Kopie", "expectedRevision": 14 }`
- duplicates the complete distribution-board subtree with new owning UUIDs
while preserving project-device and room links
- response includes the new `distributionBoard`, its `circuitList`, the
revision and current history state
- `DELETE /projects/:projectId/distribution-boards/:distributionBoardId`
- body: `{ "expectedRevision": 15 }`
- removes the complete, exactly captured distribution-board subtree in one
transaction
- response includes `distributionBoardId`, the revision and current history
state
- Copy and deletion use `distribution-board.insert-subtree` and
`distribution-board.delete-subtree`. Persistent Undo/Redo restores or
removes the same complete subtree, including protection devices.
### Project Floors and Rooms
+10
View File
@@ -285,6 +285,16 @@ nachträgliche Bearbeitung prüfen die Projektzugehörigkeit der Etage und die
Freigabe der Netzart in den Projekteinstellungen.
Gespeicherte Anlage-Commands der Schemas 1 und 2 bleiben mit ihren vier
Legacy-Abschnitten und ohne nachträglich erfundene Komponenten ausführbar.
Bereits befüllte Verteilungen werden über
`distribution-board.insert-subtree` und `distribution-board.delete-subtree`
als vollständiger Unterbaum kopiert beziehungsweise gelöscht. Der Snapshot
umfasst Verteilung, Stromkreisliste, Gruppen, Stromkreise, Gerätezeilen,
Verteilerkomponenten und alle zugehörigen Schutzgeräte. Beim Kopieren werden
sämtliche besitzenden UUIDs neu vergeben, während fachliche Verknüpfungen zu
Projektgeräten und Räumen erhalten bleiben. Beide Aktionen sind atomare
Projektrevisionen und bleiben nach einem Neustart über Undo/Redo umkehrbar. Die
Projektseite bietet sie im Einstellungsmodal der jeweiligen Verteilung an;
Löschen verlangt dort eine ausdrückliche Bestätigung.
`distribution-board-component.insert` und
`distribution-board-component.delete` sowie
`distribution-board-component.update` versionieren Anlage, Entfernung,
+156 -1
View File
@@ -5,12 +5,14 @@ import { useParams } from "next/navigation";
import { FormEvent, useEffect, useMemo, useState } from "react";
import {
copyGlobalDeviceToProject,
copyDistributionBoard,
copyProjectDeviceToGlobal,
createDistributionBoard,
createFloor,
createProjectDevice,
createRoom,
deleteProjectDevice,
deleteDistributionBoard,
disconnectProjectDeviceRows,
exportProjectTransfer,
getProjectDeviceSyncPreview,
@@ -91,6 +93,7 @@ export default function ProjectDetailPage() {
editingBoardSimultaneityFactor,
setEditingBoardSimultaneityFactor,
] = useState("1");
const [editingBoardCopyName, setEditingBoardCopyName] = useState("");
const [floorName, setFloorName] = useState("");
const [roomNumber, setRoomNumber] = useState("");
const [roomName, setRoomName] = useState("");
@@ -405,6 +408,7 @@ export default function ProjectDetailPage() {
setEditingBoardSimultaneityFactor(
String(board.simultaneityFactor)
);
setEditingBoardCopyName(`${board.name} Kopie`);
}
function openBoardCreator() {
@@ -463,6 +467,88 @@ export default function ProjectDetailPage() {
}
}
async function handleCopyBoard() {
if (
!projectId ||
!project ||
!editingBoard ||
!editingBoardCopyName.trim()
) {
return;
}
setIsSaving(true);
setError(null);
try {
const result = await copyDistributionBoard(
projectId,
editingBoard.id,
editingBoardCopyName.trim(),
project.currentRevision
);
setBoards((current) => [
...current,
result.distributionBoard,
]);
setCircuitLists((current) => [
...current,
result.circuitList,
]);
applyProjectRevision(result.history.currentRevision);
setEditingBoard(null);
} catch (err) {
setError(
err instanceof Error
? err.message
: "Verteilung konnte nicht kopiert werden."
);
} finally {
setIsSaving(false);
}
}
async function handleDeleteBoard() {
if (!projectId || !project || !editingBoard) {
return;
}
const confirmed = window.confirm(
`Verteilung „${editingBoard.name}“ wirklich löschen?\n\n` +
"Dabei werden die Stromkreisliste, alle Gruppen, Stromkreise, Gerätezeilen, Schutzgeräte und weiteren Verteilergeräte entfernt. " +
"Der Vorgang kann anschließend über die projektweite Historie rückgängig gemacht werden."
);
if (!confirmed) {
return;
}
setIsSaving(true);
setError(null);
try {
const result = await deleteDistributionBoard(
projectId,
editingBoard.id,
project.currentRevision
);
setBoards((current) =>
current.filter((board) => board.id !== result.distributionBoardId)
);
setCircuitLists((current) =>
current.filter(
(circuitList) =>
circuitList.distributionBoardId !==
result.distributionBoardId
)
);
applyProjectRevision(result.history.currentRevision);
setEditingBoard(null);
} catch (err) {
setError(
err instanceof Error
? err.message
: "Verteilung konnte nicht gelöscht werden."
);
} finally {
setIsSaving(false);
}
}
async function handleExportProject() {
if (!project) {
return;
@@ -1111,7 +1197,7 @@ export default function ProjectDetailPage() {
{structureModal === "board" ? (
<FormModal
description="Eine Stromkreisliste mit den vier Standardbereichen wird automatisch angelegt."
description="Eine Stromkreisliste mit den drei Standardgruppen wird automatisch angelegt."
isSaving={isSaving}
onClose={() => setStructureModal(null)}
onSubmit={handleCreateBoard}
@@ -1271,6 +1357,75 @@ export default function ProjectDetailPage() {
</div>
</div>
</div>
<hr className="my-4" />
<section aria-labelledby="copy-distribution-board-heading">
<h3
className="h6"
id="copy-distribution-board-heading"
>
Verteilung kopieren
</h3>
<p className="text-secondary small">
Erstellt eine vollständige Kopie des zuletzt gespeicherten
Stands einschließlich Gruppen, Stromkreisen, Gerätezeilen und
Schutzgeräten. Noch nicht gespeicherte Änderungen oben werden
nicht übernommen.
</p>
<div className="row g-2 align-items-end">
<div className="col-12 col-md">
<label
className="form-label"
htmlFor="copy-distribution-board-name"
>
Bezeichnung der Kopie
</label>
<input
className="form-control"
id="copy-distribution-board-name"
maxLength={200}
onChange={(event) =>
setEditingBoardCopyName(event.target.value)
}
value={editingBoardCopyName}
/>
</div>
<div className="col-12 col-md-auto">
<button
className="btn btn-outline-primary"
disabled={isSaving || !editingBoardCopyName.trim()}
onClick={() => void handleCopyBoard()}
type="button"
>
Verteilung kopieren
</button>
</div>
</div>
</section>
<hr className="my-4" />
<section
aria-labelledby="delete-distribution-board-heading"
className="border border-danger-subtle rounded p-3"
>
<h3
className="h6 text-danger"
id="delete-distribution-board-heading"
>
Gefahrenbereich
</h3>
<p className="text-secondary small mb-3">
Entfernt die vollständige Verteilung. Die Aktion wird erst
nach einer ausdrücklichen Warnung ausgeführt und kann über die
projektweite Historie rückgängig gemacht werden.
</p>
<button
className="btn btn-outline-danger"
disabled={isSaving}
onClick={() => void handleDeleteBoard()}
type="button"
>
Verteilung löschen
</button>
</section>
</FormModal>
) : null}
+5
View File
@@ -114,6 +114,11 @@ export interface DistributionBoardCommandResultDto
distributionBoard: DistributionBoardDto;
}
export interface DistributionBoardCopyCommandResultDto
extends DistributionBoardCommandResultDto {
circuitList: CircuitListDto;
}
export interface DistributionBoardDeleteCommandResultDto
extends ProjectCommandResultDto {
distributionBoardId: string;
+2 -1
View File
@@ -6,6 +6,7 @@ import type {
CreateGlobalDeviceInput,
DistributionBoardDto,
DistributionBoardCommandResultDto,
DistributionBoardCopyCommandResultDto,
DistributionBoardDeleteCommandResultDto,
FloorDto,
GlobalDeviceDto,
@@ -338,7 +339,7 @@ export function copyDistributionBoard(
name: string,
expectedRevision: number
) {
return request<DistributionBoardCommandResultDto>(
return request<DistributionBoardCopyCommandResultDto>(
`/api/projects/${projectId}/distribution-boards/${distributionBoardId}/copy`,
{
method: "POST",
@@ -140,6 +140,7 @@ export async function copyDistributionBoard(
return res.status(201).json({
...result,
distributionBoard: copy.distributionBoard,
circuitList: copy.circuitList,
});
} catch (error) {
return respondWithProjectCommandError(error, res);