Add distribution board copy and delete controls
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -114,6 +114,11 @@ export interface DistributionBoardCommandResultDto
|
||||
distributionBoard: DistributionBoardDto;
|
||||
}
|
||||
|
||||
export interface DistributionBoardCopyCommandResultDto
|
||||
extends DistributionBoardCommandResultDto {
|
||||
circuitList: CircuitListDto;
|
||||
}
|
||||
|
||||
export interface DistributionBoardDeleteCommandResultDto
|
||||
extends ProjectCommandResultDto {
|
||||
distributionBoardId: string;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user