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.
|
route requires `expectedRevision` and returns the updated history state.
|
||||||
Stored schema-version 1 and 2 setup commands remain executable with their four
|
Stored schema-version 1 and 2 setup commands remain executable with their four
|
||||||
legacy sections and without invented components.
|
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
|
Mutable group-protection and auxiliary distribution-board components use
|
||||||
`distribution-board-component.insert` and
|
`distribution-board-component.insert` and
|
||||||
`distribution-board-component.delete`; edits and footer reordering use
|
`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 }`
|
`{ "floorId": null, "supplyType": "AV", "expectedRevision": 13 }`
|
||||||
- executes `distribution-board.update`; floor and supply type are restored
|
- executes `distribution-board.update`; floor and supply type are restored
|
||||||
together by persistent Undo/Redo
|
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
|
### 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.
|
Freigabe der Netzart in den Projekteinstellungen.
|
||||||
Gespeicherte Anlage-Commands der Schemas 1 und 2 bleiben mit ihren vier
|
Gespeicherte Anlage-Commands der Schemas 1 und 2 bleiben mit ihren vier
|
||||||
Legacy-Abschnitten und ohne nachträglich erfundene Komponenten ausführbar.
|
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.insert` und
|
||||||
`distribution-board-component.delete` sowie
|
`distribution-board-component.delete` sowie
|
||||||
`distribution-board-component.update` versionieren Anlage, Entfernung,
|
`distribution-board-component.update` versionieren Anlage, Entfernung,
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import { useParams } from "next/navigation";
|
|||||||
import { FormEvent, useEffect, useMemo, useState } from "react";
|
import { FormEvent, useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
copyGlobalDeviceToProject,
|
copyGlobalDeviceToProject,
|
||||||
|
copyDistributionBoard,
|
||||||
copyProjectDeviceToGlobal,
|
copyProjectDeviceToGlobal,
|
||||||
createDistributionBoard,
|
createDistributionBoard,
|
||||||
createFloor,
|
createFloor,
|
||||||
createProjectDevice,
|
createProjectDevice,
|
||||||
createRoom,
|
createRoom,
|
||||||
deleteProjectDevice,
|
deleteProjectDevice,
|
||||||
|
deleteDistributionBoard,
|
||||||
disconnectProjectDeviceRows,
|
disconnectProjectDeviceRows,
|
||||||
exportProjectTransfer,
|
exportProjectTransfer,
|
||||||
getProjectDeviceSyncPreview,
|
getProjectDeviceSyncPreview,
|
||||||
@@ -91,6 +93,7 @@ export default function ProjectDetailPage() {
|
|||||||
editingBoardSimultaneityFactor,
|
editingBoardSimultaneityFactor,
|
||||||
setEditingBoardSimultaneityFactor,
|
setEditingBoardSimultaneityFactor,
|
||||||
] = useState("1");
|
] = useState("1");
|
||||||
|
const [editingBoardCopyName, setEditingBoardCopyName] = useState("");
|
||||||
const [floorName, setFloorName] = useState("");
|
const [floorName, setFloorName] = useState("");
|
||||||
const [roomNumber, setRoomNumber] = useState("");
|
const [roomNumber, setRoomNumber] = useState("");
|
||||||
const [roomName, setRoomName] = useState("");
|
const [roomName, setRoomName] = useState("");
|
||||||
@@ -405,6 +408,7 @@ export default function ProjectDetailPage() {
|
|||||||
setEditingBoardSimultaneityFactor(
|
setEditingBoardSimultaneityFactor(
|
||||||
String(board.simultaneityFactor)
|
String(board.simultaneityFactor)
|
||||||
);
|
);
|
||||||
|
setEditingBoardCopyName(`${board.name} Kopie`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openBoardCreator() {
|
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() {
|
async function handleExportProject() {
|
||||||
if (!project) {
|
if (!project) {
|
||||||
return;
|
return;
|
||||||
@@ -1111,7 +1197,7 @@ export default function ProjectDetailPage() {
|
|||||||
|
|
||||||
{structureModal === "board" ? (
|
{structureModal === "board" ? (
|
||||||
<FormModal
|
<FormModal
|
||||||
description="Eine Stromkreisliste mit den vier Standardbereichen wird automatisch angelegt."
|
description="Eine Stromkreisliste mit den drei Standardgruppen wird automatisch angelegt."
|
||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
onClose={() => setStructureModal(null)}
|
onClose={() => setStructureModal(null)}
|
||||||
onSubmit={handleCreateBoard}
|
onSubmit={handleCreateBoard}
|
||||||
@@ -1271,6 +1357,75 @@ export default function ProjectDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</FormModal>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,11 @@ export interface DistributionBoardCommandResultDto
|
|||||||
distributionBoard: DistributionBoardDto;
|
distributionBoard: DistributionBoardDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DistributionBoardCopyCommandResultDto
|
||||||
|
extends DistributionBoardCommandResultDto {
|
||||||
|
circuitList: CircuitListDto;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DistributionBoardDeleteCommandResultDto
|
export interface DistributionBoardDeleteCommandResultDto
|
||||||
extends ProjectCommandResultDto {
|
extends ProjectCommandResultDto {
|
||||||
distributionBoardId: string;
|
distributionBoardId: string;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
CreateGlobalDeviceInput,
|
CreateGlobalDeviceInput,
|
||||||
DistributionBoardDto,
|
DistributionBoardDto,
|
||||||
DistributionBoardCommandResultDto,
|
DistributionBoardCommandResultDto,
|
||||||
|
DistributionBoardCopyCommandResultDto,
|
||||||
DistributionBoardDeleteCommandResultDto,
|
DistributionBoardDeleteCommandResultDto,
|
||||||
FloorDto,
|
FloorDto,
|
||||||
GlobalDeviceDto,
|
GlobalDeviceDto,
|
||||||
@@ -338,7 +339,7 @@ export function copyDistributionBoard(
|
|||||||
name: string,
|
name: string,
|
||||||
expectedRevision: number
|
expectedRevision: number
|
||||||
) {
|
) {
|
||||||
return request<DistributionBoardCommandResultDto>(
|
return request<DistributionBoardCopyCommandResultDto>(
|
||||||
`/api/projects/${projectId}/distribution-boards/${distributionBoardId}/copy`,
|
`/api/projects/${projectId}/distribution-boards/${distributionBoardId}/copy`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ export async function copyDistributionBoard(
|
|||||||
return res.status(201).json({
|
return res.status(201).json({
|
||||||
...result,
|
...result,
|
||||||
distributionBoard: copy.distributionBoard,
|
distributionBoard: copy.distributionBoard,
|
||||||
|
circuitList: copy.circuitList,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return respondWithProjectCommandError(error, res);
|
return respondWithProjectCommandError(error, res);
|
||||||
|
|||||||
Reference in New Issue
Block a user