Persist circuit structure edits

This commit is contained in:
2026-07-25 20:49:34 +02:00
parent 76d8d59391
commit 2eba4ea75e
19 changed files with 809 additions and 964 deletions
+392 -278
View File
@@ -23,6 +23,10 @@ import {
deviceFieldKeys,
formatValue,
} from "../utils/circuit-grid-model";
import {
buildCircuitDeviceRowInsertSnapshot,
buildCircuitInsertSnapshot,
} from "../utils/circuit-structure-command";
import type {
CellKey,
CellKind,
@@ -36,13 +40,13 @@ import {
} from "../utils/circuit-grid-projection";
import type { VisibleGridRow } from "../utils/circuit-grid-projection";
import {
createCircuit,
createCircuitWithDeviceRows,
createCircuitDeviceRow,
deleteCircuitById,
deleteCircuitDeviceRowById,
deleteCircuitCommand,
deleteCircuitDeviceRowCommand,
getCircuitTree,
getNextCircuitIdentifier,
insertCircuitCommand,
insertCircuitDeviceRowCommand,
listProjectDevices,
moveCircuitDeviceRowsBulk,
moveCircuitDeviceRowById,
@@ -56,11 +60,18 @@ import {
} from "../utils/api";
import type {
CircuitTreeCircuitDto,
CircuitTreeDeviceRowDto,
CircuitTreeResponseDto,
CreateCircuitDeviceRowInputDto,
CreateCircuitInputDto,
ProjectCommandResultDto,
ProjectDeviceDto,
} from "../types";
import type {
CircuitDeviceRowSnapshot,
} from "../../domain/models/circuit-device-row-structure-project-command.model";
import type {
CircuitSnapshot,
} from "../../domain/models/circuit-structure-project-command.model";
type SaveDirection = "stay" | "next" | "prev";
type StartEditMode = "selectExisting" | "replaceWithTypedChar";
@@ -96,28 +107,6 @@ interface HistoryCommand {
};
}
interface CircuitSnapshot {
id: string;
sectionId: string;
equipmentIdentifier: string;
displayName?: string;
sortOrder: number;
protectionType?: string;
protectionRatedCurrent?: number;
protectionCharacteristic?: string;
cableType?: string;
cableCrossSection?: string;
cableLength?: number;
rcdAssignment?: string;
terminalDesignation?: string;
voltage?: number;
controlRequirement?: string;
status?: string;
isReserve: boolean;
remark?: string;
deviceRows: CircuitTreeDeviceRowDto[];
}
type ProjectDeviceDropIntent =
| { kind: "new-circuit"; sectionId: string; valid: boolean }
| { kind: "add-to-circuit"; circuitId: string; sectionId: string; valid: boolean };
@@ -805,6 +794,90 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
);
}
function createDeviceRowSnapshot(
circuitId: string,
values: CreateCircuitDeviceRowInputDto,
defaultSortOrder: number
) {
return buildCircuitDeviceRowInsertSnapshot({
id: crypto.randomUUID(),
circuitId,
values,
defaultSortOrder,
});
}
function createCircuitSnapshot(
values: CreateCircuitInputDto,
deviceRowValues: CreateCircuitDeviceRowInputDto[] = []
) {
const circuitId = crypto.randomUUID();
const deviceRows = deviceRowValues.map((row, index) =>
createDeviceRowSnapshot(circuitId, row, (index + 1) * 10)
);
return buildCircuitInsertSnapshot({
id: circuitId,
circuitListId,
values,
deviceRows,
});
}
async function persistCircuitInsert(
circuit: CircuitSnapshot,
description: string
) {
const result = await insertCircuitCommand(
projectId,
getExpectedProjectRevision(),
circuit,
description
);
applyProjectCommandResult(result);
}
async function persistCircuitDelete(
circuitId: string,
description: string
) {
const result = await deleteCircuitCommand(
projectId,
getExpectedProjectRevision(),
circuitId,
circuitListId,
description
);
applyProjectCommandResult(result);
}
async function persistDeviceRowInsert(
row: CircuitDeviceRowSnapshot,
description: string
) {
const result = await insertCircuitDeviceRowCommand(
projectId,
getExpectedProjectRevision(),
row,
description
);
applyProjectCommandResult(result);
}
async function persistDeviceRowDelete(
rowId: string,
circuitId: string,
description: string
) {
const result = await deleteCircuitDeviceRowCommand(
projectId,
getExpectedProjectRevision(),
rowId,
circuitId,
description
);
applyProjectCommandResult(result);
}
// Runs a normal command and records it in session-local history.
async function runCommand(command: HistoryCommand) {
try {
@@ -1247,15 +1320,15 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
const editValues = createValuesFromEditPatch(
buildDeviceRowEditPatch(key, draft)
);
const created = await createCircuitWithDeviceRows(projectId, circuitListId, {
circuit: {
const circuit = createCircuitSnapshot(
{
sectionId,
equipmentIdentifier: next.nextIdentifier,
displayName: "Neuer Stromkreis",
sortOrder,
isReserve: false,
},
deviceRows: [
[
{
name: "Manuelles Gerät",
displayName: "Manuelles Gerät",
@@ -1266,24 +1339,25 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
cosPhi: 1,
...editValues,
},
],
});
const createdCircuit = created.circuit;
return { rowKey: `circuitCompact:${createdCircuit.id}`, cellKey: key };
]
);
await persistCircuitInsert(circuit, "Aus freier Zeile erstellen");
return { rowKey: `circuitCompact:${circuit.id}`, cellKey: key };
}
const editValues = createValuesFromEditPatch(
buildCircuitEditPatch(key, draft)
);
const createdCircuit = (await createCircuit(projectId, circuitListId, {
const circuit = createCircuitSnapshot({
sectionId,
equipmentIdentifier: next.nextIdentifier,
displayName: "Neuer Stromkreis",
sortOrder,
isReserve: true,
...editValues,
})) as CircuitTreeCircuitDto;
return { rowKey: `reserveCircuit:${createdCircuit.id}`, cellKey: key };
});
await persistCircuitInsert(circuit, "Aus freier Zeile erstellen");
return { rowKey: `reserveCircuit:${circuit.id}`, cellKey: key };
}
// Commits the active editor input through command history so edits remain undoable.
@@ -1337,11 +1411,32 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
return nextSelectionIntent();
},
undo: async () => {
if (!createdCircuitId) {
return null;
}
await deleteCircuitById(createdCircuitId);
return buildSelectionIntent({ rowKey: `placeholder:${row.sectionId}`, cellKey: editingCell.cellKey });
return {
rowKey: `placeholder:${row.sectionId}`,
cellKey: editingCell.cellKey,
rowType: "placeholder",
sectionId: row.sectionId,
};
},
persistent: {
undoIntent: () => ({
rowKey: `placeholder:${row.sectionId}`,
cellKey: editingCell.cellKey,
rowType: "placeholder",
sectionId: row.sectionId,
}),
redoIntent: () =>
createdCircuitId
? {
rowKey: targetSelection.rowKey,
cellKey: editingCell.cellKey,
rowType: targetSelection.rowKey.startsWith("circuitCompact:")
? "circuitCompact"
: "reserveCircuit",
sectionId: row.sectionId,
circuitId: createdCircuitId,
}
: null,
},
};
setEditingCell(null);
@@ -1412,26 +1507,54 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
const editValues = createValuesFromEditPatch(
buildDeviceRowEditPatch(editingCell.cellKey, next)
);
const created = (await createCircuitDeviceRow(row.circuit!.id, {
name: "Reserveverbraucher",
displayName: "Reserveverbraucher",
phaseType: "single_phase",
quantity: 1,
powerPerUnit: 0,
simultaneityFactor: 1,
cosPhi: 1,
...editValues,
})) as { id: string };
createdRowId = created.id;
const snapshot = createDeviceRowSnapshot(
row.circuit!.id,
{
name: "Reserveverbraucher",
displayName: "Reserveverbraucher",
phaseType: "single_phase",
quantity: 1,
powerPerUnit: 0,
simultaneityFactor: 1,
cosPhi: 1,
...editValues,
},
10
);
await persistDeviceRowInsert(
snapshot,
"Gerätezeile im Reservestromkreis erstellen"
);
createdRowId = snapshot.id;
targetSelection = { rowKey: `circuitCompact:${row.circuit!.id}`, cellKey: editingCell.cellKey };
return nextSelectionIntent();
},
undo: async () => {
if (!createdRowId) {
return null;
}
await deleteCircuitDeviceRowById(createdRowId);
return buildSelectionIntent({ rowKey: `reserveCircuit:${row.circuit!.id}`, cellKey: editingCell.cellKey });
undo: async () => ({
rowKey: `reserveCircuit:${row.circuit!.id}`,
cellKey: editingCell.cellKey,
rowType: "reserveCircuit",
sectionId: row.sectionId,
circuitId: row.circuit!.id,
}),
persistent: {
undoIntent: () => ({
rowKey: `reserveCircuit:${row.circuit!.id}`,
cellKey: editingCell.cellKey,
rowType: "reserveCircuit",
sectionId: row.sectionId,
circuitId: row.circuit!.id,
}),
redoIntent: () =>
createdRowId
? {
rowKey: `circuitCompact:${row.circuit!.id}`,
cellKey: editingCell.cellKey,
rowType: "circuitCompact",
sectionId: row.sectionId,
circuitId: row.circuit!.id,
deviceId: createdRowId,
}
: null,
},
};
setEditingCell(null);
@@ -1460,33 +1583,47 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
redo: async () => {
const next = await getNextCircuitIdentifier(sectionId);
const sortOrder = getInsertionSortOrder(section.circuits, afterCircuitId);
const created = (await createCircuit(projectId, circuitListId, {
const circuit = createCircuitSnapshot({
sectionId,
equipmentIdentifier: next.nextIdentifier,
displayName: "Reserve",
sortOrder,
isReserve: true,
})) as CircuitTreeCircuitDto;
createdCircuitId = created.id;
});
await persistCircuitInsert(circuit, "Stromkreis hinzufügen");
createdCircuitId = circuit.id;
setActiveSectionId(sectionId);
return {
rowKey: `reserveCircuit:${created.id}`,
rowKey: `reserveCircuit:${circuit.id}`,
cellKey: "equipmentIdentifier",
rowType: "reserveCircuit",
sectionId,
circuitId: created.id,
circuitId: circuit.id,
};
},
undo: async () => {
if (createdCircuitId) {
await deleteCircuitById(createdCircuitId);
}
return {
undo: async () => ({
rowKey: `placeholder:${sectionId}`,
cellKey: "equipmentIdentifier",
rowType: "placeholder",
sectionId,
}),
persistent: {
undoIntent: () => ({
rowKey: `placeholder:${sectionId}`,
cellKey: "equipmentIdentifier",
rowType: "placeholder",
sectionId,
};
}),
redoIntent: () =>
createdCircuitId
? {
rowKey: `reserveCircuit:${createdCircuitId}`,
cellKey: "equipmentIdentifier",
rowType: "reserveCircuit",
sectionId,
circuitId: createdCircuitId,
}
: null,
},
});
}
@@ -1499,10 +1636,35 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
const originalDeviceCount = circuit.deviceRows.length;
const section = data?.sections.find((entry) => entry.id === sectionId);
let createdRowId: string | null = null;
const getUndoIntent = (): SelectionIntent => {
const rowKey =
originalDeviceCount === 0
? `reserveCircuit:${circuit.id}`
: originalDeviceCount === 1
? `circuitCompact:${circuit.id}`
: afterDeviceRowId
? `device:${afterDeviceRowId}`
: `circuitSummary:${circuit.id}`;
return {
rowKey,
cellKey: "displayName",
rowType:
originalDeviceCount === 0
? "reserveCircuit"
: originalDeviceCount === 1
? "circuitCompact"
: afterDeviceRowId
? "deviceRow"
: "circuitSummary",
sectionId,
circuitId: circuit.id,
deviceId: afterDeviceRowId,
};
};
await runCommand({
label: "Manuelles Gerät hinzufügen",
redo: async () => {
const created = (await createCircuitDeviceRow(circuit.id, {
const rowSnapshot = createDeviceRowSnapshot(circuit.id, {
name: "Manuelles Gerät",
displayName: "Manuelles Gerät",
phaseType: section?.key === "three_phase" ? "three_phase" : "single_phase",
@@ -1511,45 +1673,42 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
simultaneityFactor: 1,
cosPhi: 1,
sortOrder: getInsertionSortOrder(circuit.deviceRows, afterDeviceRowId),
})) as { id: string };
createdRowId = created.id;
}, 10);
await persistDeviceRowInsert(
rowSnapshot,
"Manuelles Gerät hinzufügen"
);
createdRowId = rowSnapshot.id;
setActiveSectionId(sectionId);
return {
rowKey: originalDeviceCount === 0 ? `circuitCompact:${circuit.id}` : `device:${created.id}`,
rowKey: originalDeviceCount === 0 ? `circuitCompact:${circuit.id}` : `device:${rowSnapshot.id}`,
cellKey: "displayName",
rowType: originalDeviceCount === 0 ? "circuitCompact" : "deviceRow",
sectionId,
circuitId: circuit.id,
deviceId: created.id,
deviceId: rowSnapshot.id,
};
},
undo: async () => {
if (createdRowId) {
await deleteCircuitDeviceRowById(createdRowId);
}
const rowKey =
originalDeviceCount === 0
? `reserveCircuit:${circuit.id}`
: originalDeviceCount === 1
? `circuitCompact:${circuit.id}`
: afterDeviceRowId
? `device:${afterDeviceRowId}`
: `circuitSummary:${circuit.id}`;
return {
rowKey,
cellKey: "displayName",
rowType:
originalDeviceCount === 0
? "reserveCircuit"
: originalDeviceCount === 1
? "circuitCompact"
: afterDeviceRowId
? "deviceRow"
: "circuitSummary",
sectionId,
circuitId: circuit.id,
deviceId: afterDeviceRowId,
};
undo: async () => getUndoIntent(),
persistent: {
undoIntent: getUndoIntent,
redoIntent: () =>
createdRowId
? {
rowKey:
originalDeviceCount === 0
? `circuitCompact:${circuit.id}`
: `device:${createdRowId}`,
cellKey: "displayName",
rowType:
originalDeviceCount === 0
? "circuitCompact"
: "deviceRow",
sectionId,
circuitId: circuit.id,
deviceId: createdRowId,
}
: null,
},
});
}
@@ -1646,16 +1805,30 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
deviceId: created.rowId,
};
},
undo: async () => {
if (createdCircuitId) {
await deleteCircuitById(createdCircuitId);
}
return {
undo: async () => ({
rowKey: `placeholder:${targetSectionId}`,
cellKey: "displayName",
rowType: "placeholder",
sectionId: targetSectionId,
}),
persistent: {
undoIntent: () => ({
rowKey: `placeholder:${targetSectionId}`,
cellKey: "displayName",
rowType: "placeholder",
sectionId: targetSectionId,
};
}),
redoIntent: () =>
createdCircuitId && createdRowId
? {
rowKey: `device:${createdRowId}`,
cellKey: "displayName",
rowType: "deviceRow",
sectionId: targetSectionId,
circuitId: createdCircuitId,
deviceId: createdRowId,
}
: null,
},
});
}
@@ -1698,17 +1871,32 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
deviceId: created.rowId,
};
},
undo: async () => {
if (createdRowId) {
await deleteCircuitDeviceRowById(createdRowId);
}
return {
undo: async () => ({
rowKey: `circuitSummary:${circuitId}`,
cellKey: "displayName",
rowType: "circuitSummary",
sectionId,
circuitId,
}),
persistent: {
undoIntent: () => ({
rowKey: `circuitSummary:${circuitId}`,
cellKey: "displayName",
rowType: "circuitSummary",
sectionId: findCircuitSectionId(circuitId) ?? "",
sectionId,
circuitId,
};
}),
redoIntent: () =>
createdRowId
? {
rowKey: `device:${createdRowId}`,
cellKey: "displayName",
rowType: "deviceRow",
sectionId,
circuitId,
deviceId: createdRowId,
}
: null,
},
});
}
@@ -1721,15 +1909,15 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
const next = await getNextCircuitIdentifier(sectionId);
const sortOrder =
section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
const created = await createCircuitWithDeviceRows(projectId, circuitListId, {
circuit: {
const circuit = createCircuitSnapshot(
{
sectionId,
equipmentIdentifier: next.nextIdentifier,
displayName: device.displayName || device.name,
sortOrder,
isReserve: false,
},
deviceRows: [
[
{
linkedProjectDeviceId: device.id,
name: device.name,
@@ -1744,125 +1932,47 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
category: device.category ?? undefined,
remark: device.remark ?? undefined,
},
],
});
const createdCircuit = created.circuit;
const createdRow = created.deviceRows[0];
]
);
await persistCircuitInsert(
circuit,
"Projektgerät als neuen Stromkreis hinzufügen"
);
const createdRow = circuit.deviceRows[0];
setActiveSectionId(sectionId);
setTargetCircuitId(createdCircuit.id);
return { circuitId: createdCircuit.id, rowId: createdRow.id };
setTargetCircuitId(circuit.id);
return { circuitId: circuit.id, rowId: createdRow.id };
}
async function insertProjectDeviceToCircuit(device: ProjectDeviceDto, circuitId: string) {
const createdRow = (await createCircuitDeviceRow(circuitId, {
linkedProjectDeviceId: device.id,
name: device.name,
displayName: device.displayName,
phaseType: resolvePhaseType(device),
connectionKind: device.connectionKind ?? undefined,
costGroup: device.costGroup ?? undefined,
quantity: device.quantity,
powerPerUnit: device.powerPerUnit,
simultaneityFactor: device.simultaneityFactor,
cosPhi: device.cosPhi ?? undefined,
category: device.category ?? undefined,
remark: device.remark ?? undefined,
})) as { id: string };
return { rowId: createdRow.id };
}
function getCircuitSnapshot(circuitId: string): CircuitSnapshot | null {
for (const section of data?.sections ?? []) {
const found = section.circuits.find((entry) => entry.id === circuitId);
if (!found) {
continue;
}
return {
id: found.id,
sectionId: found.sectionId,
equipmentIdentifier: found.equipmentIdentifier,
displayName: found.displayName ?? undefined,
sortOrder: found.sortOrder,
protectionType: found.protectionType ?? undefined,
protectionRatedCurrent: found.protectionRatedCurrent ?? undefined,
protectionCharacteristic: found.protectionCharacteristic ?? undefined,
cableType: found.cableType ?? undefined,
cableCrossSection: found.cableCrossSection ?? undefined,
cableLength: found.cableLength ?? undefined,
rcdAssignment: found.rcdAssignment ?? undefined,
terminalDesignation: found.terminalDesignation ?? undefined,
voltage: found.voltage ?? undefined,
controlRequirement: found.controlRequirement ?? undefined,
status: found.status ?? undefined,
isReserve: found.isReserve,
remark: found.remark ?? undefined,
deviceRows: found.deviceRows.map((row) => ({ ...row })),
};
const circuit = allCircuits.find((entry) => entry.id === circuitId);
if (!circuit) {
throw new Error("Der Zielstromkreis ist ungültig.");
}
return null;
}
async function recreateCircuit(snapshot: CircuitSnapshot) {
const circuitInput = {
sectionId: snapshot.sectionId,
equipmentIdentifier: snapshot.equipmentIdentifier,
displayName: snapshot.displayName,
sortOrder: snapshot.sortOrder,
protectionType: snapshot.protectionType,
protectionRatedCurrent: snapshot.protectionRatedCurrent,
protectionCharacteristic: snapshot.protectionCharacteristic,
cableType: snapshot.cableType,
cableCrossSection: snapshot.cableCrossSection,
cableLength: snapshot.cableLength,
rcdAssignment: snapshot.rcdAssignment,
terminalDesignation: snapshot.terminalDesignation,
voltage: snapshot.voltage,
controlRequirement: snapshot.controlRequirement,
status: snapshot.status,
isReserve: snapshot.isReserve,
remark: snapshot.remark,
};
if (snapshot.deviceRows.length === 0) {
const createdCircuit = (await createCircuit(
projectId,
circuitListId,
circuitInput
)) as CircuitTreeCircuitDto;
return { circuitId: createdCircuit.id, rowIds: [] };
}
const created = await createCircuitWithDeviceRows(projectId, circuitListId, {
circuit: {
...circuitInput,
isReserve: false,
const row = createDeviceRowSnapshot(
circuitId,
{
linkedProjectDeviceId: device.id,
name: device.name,
displayName: device.displayName,
phaseType: resolvePhaseType(device),
connectionKind: device.connectionKind ?? undefined,
costGroup: device.costGroup ?? undefined,
quantity: device.quantity,
powerPerUnit: device.powerPerUnit,
simultaneityFactor: device.simultaneityFactor,
cosPhi: device.cosPhi ?? undefined,
category: device.category ?? undefined,
remark: device.remark ?? undefined,
},
deviceRows: snapshot.deviceRows.map((row) => ({
linkedProjectDeviceId: row.linkedProjectDeviceId,
name: row.name,
displayName: row.displayName,
phaseType: row.phaseType,
connectionKind: row.connectionKind,
costGroup: row.costGroup,
category: row.category,
level: row.level,
roomId: row.roomId,
roomNumberSnapshot: row.roomNumberSnapshot,
roomNameSnapshot: row.roomNameSnapshot,
quantity: row.quantity,
powerPerUnit: row.powerPerUnit,
simultaneityFactor: row.simultaneityFactor,
cosPhi: row.cosPhi,
remark: row.remark,
overriddenFields: row.overriddenFields,
sortOrder: row.sortOrder,
})),
});
return {
circuitId: created.circuit.id,
rowIds: created.deviceRows.map((row) => row.id),
};
getInsertionSortOrder(circuit.deviceRows)
);
await persistDeviceRowInsert(
row,
"Projektgerät zum Stromkreis hinzufügen"
);
return { rowId: row.id };
}
function parseDraggedProjectDeviceId(event: DragEvent<HTMLElement>) {
@@ -2053,11 +2163,13 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
// it can create new rows/circuits instead of moving existing ones.
if (intent.kind === "new-circuit") {
let createdCircuitId: string | null = null;
let createdRowId: string | null = null;
await runCommand({
label: "Projektgerät in neuen Stromkreis ziehen",
redo: async () => {
const created = await insertProjectDeviceAsNewCircuit(device, intent.sectionId);
createdCircuitId = created.circuitId;
createdRowId = created.rowId;
return {
rowKey: `device:${created.rowId}`,
cellKey: "displayName",
@@ -2067,11 +2179,20 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
deviceId: created.rowId,
};
},
undo: async () => {
if (createdCircuitId) {
await deleteCircuitById(createdCircuitId);
}
return null;
undo: async () => null,
persistent: {
undoIntent: () => null,
redoIntent: () =>
createdCircuitId && createdRowId
? {
rowKey: `device:${createdRowId}`,
cellKey: "displayName",
rowType: "deviceRow",
sectionId: intent.sectionId,
circuitId: createdCircuitId,
deviceId: createdRowId,
}
: null,
},
});
return;
@@ -2091,11 +2212,20 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
deviceId: created.rowId,
};
},
undo: async () => {
if (createdRowId) {
await deleteCircuitDeviceRowById(createdRowId);
}
return null;
undo: async () => null,
persistent: {
undoIntent: () => null,
redoIntent: () =>
createdRowId
? {
rowKey: `device:${createdRowId}`,
cellKey: "displayName",
rowType: "deviceRow",
sectionId: intent.sectionId,
circuitId: intent.circuitId,
deviceId: createdRowId,
}
: null,
},
});
}
@@ -2259,10 +2389,12 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
});
}
// Delete undo recreates the row from captured snapshot because delete is destructive.
// Persistent delete commands preserve the complete row and its stable id.
async function handleDeleteDevice(rowId: string) {
const sourceCircuitId = findDeviceRowCircuitId(rowId);
const sourceCircuit = sourceCircuitId ? getCircuitSnapshot(sourceCircuitId) : null;
const sourceCircuit = sourceCircuitId
? allCircuits.find((circuit) => circuit.id === sourceCircuitId)
: null;
const rowSnapshot = sourceCircuit?.deviceRows.find((row) => row.id === rowId);
if (!sourceCircuitId || !sourceCircuit || !rowSnapshot) {
setError("Die Gerätezeile ist ungültig.");
@@ -2287,68 +2419,50 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
} else if (!confirm(`Gerätezeile „${rowSnapshot.displayName}“ löschen?`)) {
return;
}
let recreatedRowId: string | null = null;
await runCommand({
label: "Gerätezeile löschen",
redo: async () => {
await deleteCircuitDeviceRowById(recreatedRowId ?? rowId);
await persistDeviceRowDelete(
rowId,
sourceCircuitId,
"Gerätezeile löschen"
);
return null;
},
undo: async () => {
// Recreate instead of "undelete": backend ids are immutable once deleted.
const created = (await createCircuitDeviceRow(sourceCircuitId, {
linkedProjectDeviceId: rowSnapshot.linkedProjectDeviceId,
name: rowSnapshot.name,
displayName: rowSnapshot.displayName,
phaseType: rowSnapshot.phaseType,
connectionKind: rowSnapshot.connectionKind,
costGroup: rowSnapshot.costGroup,
category: rowSnapshot.category,
level: rowSnapshot.level,
roomId: rowSnapshot.roomId,
roomNumberSnapshot: rowSnapshot.roomNumberSnapshot,
roomNameSnapshot: rowSnapshot.roomNameSnapshot,
quantity: rowSnapshot.quantity,
powerPerUnit: rowSnapshot.powerPerUnit,
simultaneityFactor: rowSnapshot.simultaneityFactor,
cosPhi: rowSnapshot.cosPhi,
remark: rowSnapshot.remark,
overriddenFields: rowSnapshot.overriddenFields,
sortOrder: rowSnapshot.sortOrder,
})) as { id: string };
recreatedRowId = created.id;
return null;
undo: async () => null,
persistent: {
undoIntent: () => null,
redoIntent: () => null,
},
});
}
// Deletes a circuit and all child rows; undo recreates from captured snapshot.
// Persistent circuit delete preserves the complete circuit block and all stable ids.
async function handleDeleteCircuit(circuitId: string, askForConfirmation = true) {
const snapshot = getCircuitSnapshot(circuitId);
if (!snapshot) {
const circuit = allCircuits.find((entry) => entry.id === circuitId);
if (!circuit) {
setError("Der Stromkreis ist ungültig.");
return;
}
if (
askForConfirmation &&
!confirm(
`Stromkreis ${snapshot.equipmentIdentifier} und ${snapshot.deviceRows.length} zugeordnete Gerätezeile(n) löschen?\n\n` +
`Stromkreis ${circuit.equipmentIdentifier} und ${circuit.deviceRows.length} zugeordnete Gerätezeile(n) löschen?\n\n` +
"Bestehende Betriebsmittelkennzeichen werden nicht neu nummeriert."
)
) {
return;
}
let recreatedCircuitId: string | null = null;
await runCommand({
label: "Stromkreis löschen",
redo: async () => {
await deleteCircuitById(recreatedCircuitId ?? circuitId);
await persistCircuitDelete(circuitId, "Stromkreis löschen");
return null;
},
undo: async () => {
const recreated = await recreateCircuit(snapshot);
recreatedCircuitId = recreated.circuitId;
return null;
undo: async () => null,
persistent: {
undoIntent: () => null,
redoIntent: () => null,
},
});
}
+78 -42
View File
@@ -17,10 +17,6 @@ import type {
ProjectDto,
RoomDto,
CircuitTreeResponseDto,
CircuitTreeCircuitDto,
CircuitTreeDeviceRowDto,
CreateCircuitInputDto,
CreateCircuitDeviceRowInputDto,
} from "../types";
import type { ProjectDeviceSyncField } from "../../shared/constants/project-device-sync-fields";
import {
@@ -31,6 +27,12 @@ import {
createCircuitDeviceRowUpdateProjectCommand,
type CircuitDeviceRowUpdatePatch,
} from "../../domain/models/circuit-device-row-project-command.model";
import type {
CircuitDeviceRowSnapshot,
} from "../../domain/models/circuit-device-row-structure-project-command.model";
import type {
CircuitSnapshot,
} from "../../domain/models/circuit-structure-project-command.model";
async function request<T>(url: string, init?: RequestInit): Promise<T> {
const response = await fetch(url, {
@@ -159,33 +161,6 @@ export function getCircuitTree(projectId: string, circuitListId: string) {
return request<CircuitTreeResponseDto>(`/api/projects/${projectId}/circuit-lists/${circuitListId}/tree`);
}
export function createCircuit(projectId: string, circuitListId: string, input: CreateCircuitInputDto) {
return request(`/api/projects/${projectId}/circuit-lists/${circuitListId}/circuits`, {
method: "POST",
body: JSON.stringify(input),
});
}
export function createCircuitWithDeviceRows(
projectId: string,
circuitListId: string,
input: {
circuit: CreateCircuitInputDto;
deviceRows: CreateCircuitDeviceRowInputDto[];
}
) {
return request<{
circuit: CircuitTreeCircuitDto;
deviceRows: CircuitTreeDeviceRowDto[];
}>(
`/api/projects/${projectId}/circuit-lists/${circuitListId}/circuits-with-device-rows`,
{
method: "POST",
body: JSON.stringify(input),
}
);
}
export function updateCircuitById(
projectId: string,
expectedRevision: number,
@@ -200,6 +175,43 @@ export function updateCircuitById(
);
}
export function insertCircuitCommand(
projectId: string,
expectedRevision: number,
circuit: CircuitSnapshot,
description: string
) {
return executeProjectCommand(
projectId,
expectedRevision,
{
schemaVersion: 1,
type: "circuit.insert",
payload: { circuit },
},
description
);
}
export function deleteCircuitCommand(
projectId: string,
expectedRevision: number,
circuitId: string,
expectedCircuitListId: string,
description: string
) {
return executeProjectCommand(
projectId,
expectedRevision,
{
schemaVersion: 1,
type: "circuit.delete",
payload: { circuitId, expectedCircuitListId },
},
description
);
}
export function deleteCircuitById(circuitId: string) {
return request<void>(`/api/circuits/${circuitId}`, {
method: "DELETE",
@@ -212,13 +224,6 @@ export function getNextCircuitIdentifier(sectionId: string) {
);
}
export function createCircuitDeviceRow(circuitId: string, input: CreateCircuitDeviceRowInputDto) {
return request(`/api/circuits/${circuitId}/device-rows`, {
method: "POST",
body: JSON.stringify(input),
});
}
export function updateCircuitDeviceRowById(
projectId: string,
expectedRevision: number,
@@ -233,10 +238,41 @@ export function updateCircuitDeviceRowById(
);
}
export function deleteCircuitDeviceRowById(rowId: string) {
return request<void>(`/api/circuit-device-rows/${rowId}`, {
method: "DELETE",
});
export function insertCircuitDeviceRowCommand(
projectId: string,
expectedRevision: number,
row: CircuitDeviceRowSnapshot,
description: string
) {
return executeProjectCommand(
projectId,
expectedRevision,
{
schemaVersion: 1,
type: "circuit-device-row.insert",
payload: { row },
},
description
);
}
export function deleteCircuitDeviceRowCommand(
projectId: string,
expectedRevision: number,
rowId: string,
expectedCircuitId: string,
description: string
) {
return executeProjectCommand(
projectId,
expectedRevision,
{
schemaVersion: 1,
type: "circuit-device-row.delete",
payload: { rowId, expectedCircuitId },
},
description
);
}
export function moveCircuitDeviceRowById(
@@ -0,0 +1,73 @@
import type {
CircuitDeviceRowSnapshot,
} from "../../domain/models/circuit-device-row-structure-project-command.model.js";
import type {
CircuitSnapshot,
} from "../../domain/models/circuit-structure-project-command.model.js";
import type {
CreateCircuitDeviceRowInputDto,
CreateCircuitInputDto,
} from "../types.js";
export function buildCircuitDeviceRowInsertSnapshot(input: {
id: string;
circuitId: string;
values: CreateCircuitDeviceRowInputDto;
defaultSortOrder: number;
}): CircuitDeviceRowSnapshot {
const { id, circuitId, values, defaultSortOrder } = input;
return {
id,
circuitId,
linkedProjectDeviceId: values.linkedProjectDeviceId ?? null,
legacyConsumerId: null,
sortOrder: values.sortOrder ?? defaultSortOrder,
name: values.name,
displayName: values.displayName,
phaseType: values.phaseType ?? null,
connectionKind: values.connectionKind ?? null,
costGroup: values.costGroup ?? null,
category: values.category ?? null,
level: values.level ?? null,
roomId: values.roomId ?? null,
roomNumberSnapshot: values.roomNumberSnapshot ?? null,
roomNameSnapshot: values.roomNameSnapshot ?? null,
quantity: values.quantity,
powerPerUnit: values.powerPerUnit,
simultaneityFactor: values.simultaneityFactor,
cosPhi: values.cosPhi ?? null,
remark: values.remark ?? null,
overriddenFields: values.overriddenFields ?? null,
};
}
export function buildCircuitInsertSnapshot(input: {
id: string;
circuitListId: string;
values: CreateCircuitInputDto;
deviceRows: CircuitDeviceRowSnapshot[];
}): CircuitSnapshot {
const { id, circuitListId, values, deviceRows } = input;
return {
id,
circuitListId,
sectionId: values.sectionId,
equipmentIdentifier: values.equipmentIdentifier,
displayName: values.displayName ?? null,
sortOrder: values.sortOrder,
protectionType: values.protectionType ?? null,
protectionRatedCurrent: values.protectionRatedCurrent ?? null,
protectionCharacteristic: values.protectionCharacteristic ?? null,
cableType: values.cableType ?? null,
cableCrossSection: values.cableCrossSection ?? null,
cableLength: values.cableLength ?? null,
rcdAssignment: values.rcdAssignment ?? null,
terminalDesignation: values.terminalDesignation ?? null,
voltage: values.voltage ?? null,
controlRequirement: values.controlRequirement ?? null,
status: values.status ?? null,
isReserve: deviceRows.length === 0,
remark: values.remark ?? null,
deviceRows,
};
}