Persist circuit structure edits
This commit is contained in:
@@ -1,14 +1,9 @@
|
||||
import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js";
|
||||
import type { CircuitDeviceRowTransactionStore } from "../ports/circuit-device-row-transaction.store.js";
|
||||
import type { CircuitSectionTransactionStore } from "../ports/circuit-section-transaction.store.js";
|
||||
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
||||
import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
|
||||
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
||||
import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js";
|
||||
import type {
|
||||
CreateCircuitDeviceRowInput,
|
||||
CreateCircuitInput,
|
||||
CreateCircuitWithDeviceRowsInput,
|
||||
MoveCircuitDeviceRowInput,
|
||||
MoveCircuitDeviceRowsBulkInput,
|
||||
ReorderSectionCircuitsInput,
|
||||
@@ -19,30 +14,24 @@ import { CircuitNumberingService } from "./circuit-numbering.service.js";
|
||||
export class CircuitWriteService {
|
||||
private readonly circuitRepository: CircuitRepository;
|
||||
private readonly circuitSectionRepository: CircuitSectionRepository;
|
||||
private readonly circuitListRepository: CircuitListRepository;
|
||||
private readonly deviceRowRepository: CircuitDeviceRowRepository;
|
||||
private readonly deviceRowTransactionStore?: CircuitDeviceRowTransactionStore;
|
||||
private readonly circuitSectionTransactionStore?: CircuitSectionTransactionStore;
|
||||
private readonly projectDeviceRepository: ProjectDeviceRepository;
|
||||
private readonly numberingService: CircuitNumberingService;
|
||||
|
||||
constructor(deps?: {
|
||||
circuitRepository?: CircuitRepository;
|
||||
circuitSectionRepository?: CircuitSectionRepository;
|
||||
circuitListRepository?: CircuitListRepository;
|
||||
deviceRowRepository?: CircuitDeviceRowRepository;
|
||||
deviceRowTransactionStore?: CircuitDeviceRowTransactionStore;
|
||||
circuitSectionTransactionStore?: CircuitSectionTransactionStore;
|
||||
projectDeviceRepository?: ProjectDeviceRepository;
|
||||
numberingService?: CircuitNumberingService;
|
||||
}) {
|
||||
this.circuitRepository = deps?.circuitRepository ?? new CircuitRepository();
|
||||
this.circuitSectionRepository = deps?.circuitSectionRepository ?? new CircuitSectionRepository();
|
||||
this.circuitListRepository = deps?.circuitListRepository ?? new CircuitListRepository();
|
||||
this.deviceRowRepository = deps?.deviceRowRepository ?? new CircuitDeviceRowRepository();
|
||||
this.deviceRowTransactionStore = deps?.deviceRowTransactionStore;
|
||||
this.circuitSectionTransactionStore = deps?.circuitSectionTransactionStore;
|
||||
this.projectDeviceRepository = deps?.projectDeviceRepository ?? new ProjectDeviceRepository();
|
||||
this.numberingService = deps?.numberingService ?? new CircuitNumberingService();
|
||||
}
|
||||
|
||||
@@ -58,36 +47,6 @@ export class CircuitWriteService {
|
||||
return section;
|
||||
}
|
||||
|
||||
// Enforces BMK uniqueness inside one circuit list.
|
||||
private async assertUniqueEquipmentIdentifier(
|
||||
circuitListId: string,
|
||||
equipmentIdentifier: string,
|
||||
excludeCircuitId?: string
|
||||
) {
|
||||
const exists = await this.circuitRepository.existsByEquipmentIdentifier(
|
||||
circuitListId,
|
||||
equipmentIdentifier,
|
||||
excludeCircuitId
|
||||
);
|
||||
if (exists) {
|
||||
throw new Error("Duplicate equipmentIdentifier in circuit list.");
|
||||
}
|
||||
}
|
||||
|
||||
// Validates linked project-device id against owning project of the circuit list.
|
||||
private async assertValidLinkedProjectDeviceForProject(
|
||||
projectId: string,
|
||||
linkedProjectDeviceId?: string
|
||||
) {
|
||||
if (!linkedProjectDeviceId) {
|
||||
return;
|
||||
}
|
||||
const device = await this.projectDeviceRepository.findById(projectId, linkedProjectDeviceId);
|
||||
if (!device) {
|
||||
throw new Error("Invalid linked project device id.");
|
||||
}
|
||||
}
|
||||
|
||||
private getDeviceRowTransactionStore() {
|
||||
if (!this.deviceRowTransactionStore) {
|
||||
throw new Error("Circuit device-row transactions are not configured.");
|
||||
@@ -102,92 +61,6 @@ export class CircuitWriteService {
|
||||
return this.circuitSectionTransactionStore;
|
||||
}
|
||||
|
||||
private async assertValidLinkedProjectDevice(circuitId: string, linkedProjectDeviceId?: string) {
|
||||
if (!linkedProjectDeviceId) {
|
||||
return;
|
||||
}
|
||||
const circuit = await this.circuitRepository.findById(circuitId);
|
||||
if (!circuit) {
|
||||
throw new Error("Invalid circuit id.");
|
||||
}
|
||||
const list = await this.circuitListRepository.findByIdByListIdOnly(circuit.circuitListId);
|
||||
if (!list) {
|
||||
throw new Error("Circuit list not found.");
|
||||
}
|
||||
await this.assertValidLinkedProjectDeviceForProject(list.projectId, linkedProjectDeviceId);
|
||||
}
|
||||
|
||||
async createCircuit(projectId: string, circuitListId: string, input: CreateCircuitInput) {
|
||||
const list = await this.circuitListRepository.findById(projectId, circuitListId);
|
||||
if (!list) {
|
||||
throw new Error("Circuit list not found in project.");
|
||||
}
|
||||
await this.assertSectionInList(input.sectionId, circuitListId);
|
||||
await this.assertUniqueEquipmentIdentifier(circuitListId, input.equipmentIdentifier);
|
||||
const id = await this.circuitRepository.create({
|
||||
circuitListId,
|
||||
sectionId: input.sectionId,
|
||||
equipmentIdentifier: input.equipmentIdentifier,
|
||||
displayName: input.displayName,
|
||||
sortOrder: input.sortOrder,
|
||||
protectionType: input.protectionType,
|
||||
protectionRatedCurrent: input.protectionRatedCurrent,
|
||||
protectionCharacteristic: input.protectionCharacteristic,
|
||||
cableType: input.cableType,
|
||||
cableCrossSection: input.cableCrossSection,
|
||||
cableLength: input.cableLength,
|
||||
rcdAssignment: input.rcdAssignment,
|
||||
terminalDesignation: input.terminalDesignation,
|
||||
voltage: input.voltage,
|
||||
controlRequirement: input.controlRequirement,
|
||||
status: input.status,
|
||||
isReserve: input.isReserve ?? true,
|
||||
remark: input.remark,
|
||||
});
|
||||
return this.circuitRepository.findById(id);
|
||||
}
|
||||
|
||||
async createCircuitWithDeviceRows(
|
||||
projectId: string,
|
||||
circuitListId: string,
|
||||
input: CreateCircuitWithDeviceRowsInput
|
||||
) {
|
||||
const list = await this.circuitListRepository.findById(projectId, circuitListId);
|
||||
if (!list) {
|
||||
throw new Error("Circuit list not found in project.");
|
||||
}
|
||||
await this.assertSectionInList(input.circuit.sectionId, circuitListId);
|
||||
await this.assertUniqueEquipmentIdentifier(
|
||||
circuitListId,
|
||||
input.circuit.equipmentIdentifier
|
||||
);
|
||||
for (const row of input.deviceRows) {
|
||||
await this.assertValidLinkedProjectDeviceForProject(
|
||||
list.projectId,
|
||||
row.linkedProjectDeviceId
|
||||
);
|
||||
}
|
||||
|
||||
const created = this.getDeviceRowTransactionStore().createCircuitWithDeviceRows({
|
||||
circuit: {
|
||||
circuitListId,
|
||||
...input.circuit,
|
||||
},
|
||||
deviceRows: input.deviceRows,
|
||||
});
|
||||
const circuit = await this.circuitRepository.findById(created.circuitId);
|
||||
const deviceRows = await Promise.all(
|
||||
created.rowIds.map((rowId) => this.deviceRowRepository.findById(rowId))
|
||||
);
|
||||
if (!circuit || deviceRows.some((row) => !row)) {
|
||||
throw new Error("Der angelegte Stromkreis konnte nicht geladen werden.");
|
||||
}
|
||||
return {
|
||||
circuit,
|
||||
deviceRows,
|
||||
};
|
||||
}
|
||||
|
||||
async deleteCircuit(circuitId: string) {
|
||||
const current = await this.circuitRepository.findById(circuitId);
|
||||
if (!current) {
|
||||
@@ -196,50 +69,6 @@ export class CircuitWriteService {
|
||||
await this.circuitRepository.delete(circuitId);
|
||||
}
|
||||
|
||||
async createDeviceRow(circuitId: string, input: CreateCircuitDeviceRowInput) {
|
||||
const circuit = await this.circuitRepository.findById(circuitId);
|
||||
if (!circuit) {
|
||||
throw new Error("Invalid circuit id.");
|
||||
}
|
||||
await this.assertValidLinkedProjectDevice(circuitId, input.linkedProjectDeviceId);
|
||||
|
||||
const rowId = this.getDeviceRowTransactionStore().createInCircuit({
|
||||
circuitId,
|
||||
linkedProjectDeviceId: input.linkedProjectDeviceId,
|
||||
sortOrder: input.sortOrder,
|
||||
name: input.name,
|
||||
displayName: input.displayName,
|
||||
phaseType: input.phaseType,
|
||||
connectionKind: input.connectionKind,
|
||||
costGroup: input.costGroup,
|
||||
category: input.category,
|
||||
level: input.level,
|
||||
roomId: input.roomId,
|
||||
roomNumberSnapshot: input.roomNumberSnapshot,
|
||||
roomNameSnapshot: input.roomNameSnapshot,
|
||||
quantity: input.quantity,
|
||||
powerPerUnit: input.powerPerUnit,
|
||||
simultaneityFactor: input.simultaneityFactor,
|
||||
cosPhi: input.cosPhi,
|
||||
remark: input.remark,
|
||||
overriddenFields: input.overriddenFields,
|
||||
});
|
||||
|
||||
return this.deviceRowRepository.findById(rowId);
|
||||
}
|
||||
|
||||
async deleteDeviceRow(rowId: string) {
|
||||
const current = await this.deviceRowRepository.findById(rowId);
|
||||
if (!current) {
|
||||
throw new Error("Invalid device row id.");
|
||||
}
|
||||
const circuit = await this.circuitRepository.findById(current.circuitId);
|
||||
if (!circuit) {
|
||||
throw new Error("Invalid circuit id.");
|
||||
}
|
||||
this.getDeviceRowTransactionStore().deleteFromCircuit(rowId, circuit.id);
|
||||
}
|
||||
|
||||
async moveDeviceRow(rowId: string, input: MoveCircuitDeviceRowInput) {
|
||||
const row = await this.deviceRowRepository.findById(rowId);
|
||||
if (!row) {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -1,44 +1,10 @@
|
||||
import type { Request, Response } from "express";
|
||||
import { circuitWriteService } from "../composition/circuit-write-service.js";
|
||||
import {
|
||||
createCircuitDeviceRowSchema,
|
||||
moveCircuitDeviceRowsBulkSchema,
|
||||
moveCircuitDeviceRowSchema,
|
||||
} from "../../shared/validation/circuit.schemas.js";
|
||||
|
||||
export async function createCircuitDeviceRow(req: Request, res: Response) {
|
||||
const { circuitId } = req.params;
|
||||
if (typeof circuitId !== "string") {
|
||||
return res.status(400).json({ error: "Invalid circuitId" });
|
||||
}
|
||||
|
||||
const parsed = createCircuitDeviceRowSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: parsed.error.flatten() });
|
||||
}
|
||||
|
||||
try {
|
||||
const created = await circuitWriteService.createDeviceRow(circuitId, parsed.data);
|
||||
return res.status(201).json(created);
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to create device row." });
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCircuitDeviceRow(req: Request, res: Response) {
|
||||
const { rowId } = req.params;
|
||||
if (typeof rowId !== "string") {
|
||||
return res.status(400).json({ error: "Invalid rowId" });
|
||||
}
|
||||
|
||||
try {
|
||||
await circuitWriteService.deleteDeviceRow(rowId);
|
||||
return res.status(204).send();
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to delete device row." });
|
||||
}
|
||||
}
|
||||
|
||||
export async function moveCircuitDeviceRow(req: Request, res: Response) {
|
||||
const { rowId } = req.params;
|
||||
if (typeof rowId !== "string") {
|
||||
|
||||
@@ -1,56 +1,5 @@
|
||||
import type { Request, Response } from "express";
|
||||
import { circuitWriteService } from "../composition/circuit-write-service.js";
|
||||
import {
|
||||
createCircuitSchema,
|
||||
createCircuitWithDeviceRowsSchema,
|
||||
} from "../../shared/validation/circuit.schemas.js";
|
||||
|
||||
export async function createCircuit(req: Request, res: Response) {
|
||||
const { projectId, circuitListId } = req.params;
|
||||
if (typeof projectId !== "string" || typeof circuitListId !== "string") {
|
||||
return res.status(400).json({ error: "Invalid parameters" });
|
||||
}
|
||||
|
||||
const parsed = createCircuitSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: parsed.error.flatten() });
|
||||
}
|
||||
|
||||
try {
|
||||
const created = await circuitWriteService.createCircuit(projectId, circuitListId, parsed.data);
|
||||
return res.status(201).json(created);
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to create circuit." });
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCircuitWithDeviceRows(req: Request, res: Response) {
|
||||
const { projectId, circuitListId } = req.params;
|
||||
if (typeof projectId !== "string" || typeof circuitListId !== "string") {
|
||||
return res.status(400).json({ error: "Ungültige Parameter." });
|
||||
}
|
||||
|
||||
const parsed = createCircuitWithDeviceRowsSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: parsed.error.flatten() });
|
||||
}
|
||||
|
||||
try {
|
||||
const created = await circuitWriteService.createCircuitWithDeviceRows(
|
||||
projectId,
|
||||
circuitListId,
|
||||
parsed.data
|
||||
);
|
||||
return res.status(201).json(created);
|
||||
} catch (error) {
|
||||
return res.status(400).json({
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Stromkreis und Gerätezeilen konnten nicht erstellt werden.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCircuit(req: Request, res: Response) {
|
||||
const { circuitId } = req.params;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Router } from "express";
|
||||
import {
|
||||
deleteCircuitDeviceRow,
|
||||
moveCircuitDeviceRowsBulk,
|
||||
moveCircuitDeviceRow,
|
||||
} from "../controllers/circuit-device-row.controller.js";
|
||||
@@ -9,4 +8,3 @@ export const circuitDeviceRowRouter = Router();
|
||||
|
||||
circuitDeviceRowRouter.patch("/circuit-device-rows/move-bulk", moveCircuitDeviceRowsBulk);
|
||||
circuitDeviceRowRouter.patch("/circuit-device-rows/:rowId/move", moveCircuitDeviceRow);
|
||||
circuitDeviceRowRouter.delete("/circuit-device-rows/:rowId", deleteCircuitDeviceRow);
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
import { Router } from "express";
|
||||
import {
|
||||
createCircuit,
|
||||
createCircuitWithDeviceRows,
|
||||
deleteCircuit,
|
||||
getNextCircuitIdentifier,
|
||||
} from "../controllers/circuit.controller.js";
|
||||
import { createCircuitDeviceRow } from "../controllers/circuit-device-row.controller.js";
|
||||
|
||||
export const circuitRouter = Router();
|
||||
|
||||
circuitRouter.post("/projects/:projectId/circuit-lists/:circuitListId/circuits", createCircuit);
|
||||
circuitRouter.post(
|
||||
"/projects/:projectId/circuit-lists/:circuitListId/circuits-with-device-rows",
|
||||
createCircuitWithDeviceRows
|
||||
);
|
||||
circuitRouter.delete("/circuits/:circuitId", deleteCircuit);
|
||||
circuitRouter.get("/circuit-sections/:sectionId/next-identifier", getNextCircuitIdentifier);
|
||||
circuitRouter.post("/circuits/:circuitId/device-rows", createCircuitDeviceRow);
|
||||
|
||||
|
||||
@@ -1,51 +1,5 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const createCircuitSchema = z.object({
|
||||
sectionId: z.string().min(1),
|
||||
equipmentIdentifier: z.string().min(1),
|
||||
displayName: z.string().optional(),
|
||||
sortOrder: z.number(),
|
||||
protectionType: z.string().optional(),
|
||||
protectionRatedCurrent: z.number().min(0).optional(),
|
||||
protectionCharacteristic: z.string().optional(),
|
||||
cableType: z.string().optional(),
|
||||
cableCrossSection: z.string().optional(),
|
||||
cableLength: z.number().min(0).optional(),
|
||||
rcdAssignment: z.string().optional(),
|
||||
terminalDesignation: z.string().optional(),
|
||||
voltage: z.number().positive().optional(),
|
||||
controlRequirement: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
isReserve: z.boolean().optional(),
|
||||
remark: z.string().optional(),
|
||||
});
|
||||
|
||||
export const createCircuitDeviceRowSchema = z.object({
|
||||
linkedProjectDeviceId: z.string().min(1).optional(),
|
||||
name: z.string().min(1),
|
||||
displayName: z.string().min(1),
|
||||
phaseType: z.string().optional(),
|
||||
connectionKind: z.string().optional(),
|
||||
costGroup: z.string().optional(),
|
||||
category: z.string().optional(),
|
||||
level: z.string().optional(),
|
||||
roomId: z.string().min(1).optional(),
|
||||
roomNumberSnapshot: z.string().optional(),
|
||||
roomNameSnapshot: z.string().optional(),
|
||||
quantity: z.number().min(0),
|
||||
powerPerUnit: z.number().min(0),
|
||||
simultaneityFactor: z.number().min(0),
|
||||
cosPhi: z.number().positive().optional(),
|
||||
remark: z.string().optional(),
|
||||
overriddenFields: z.string().optional(),
|
||||
sortOrder: z.number().optional(),
|
||||
});
|
||||
|
||||
export const createCircuitWithDeviceRowsSchema = z.object({
|
||||
circuit: createCircuitSchema,
|
||||
deviceRows: z.array(createCircuitDeviceRowSchema).min(1),
|
||||
});
|
||||
|
||||
export const moveCircuitDeviceRowSchema = z
|
||||
.object({
|
||||
targetCircuitId: z.string().min(1).optional(),
|
||||
@@ -88,9 +42,6 @@ export const updateSectionEquipmentIdentifiersSchema = z.object({
|
||||
.min(1),
|
||||
});
|
||||
|
||||
export type CreateCircuitInput = z.infer<typeof createCircuitSchema>;
|
||||
export type CreateCircuitDeviceRowInput = z.infer<typeof createCircuitDeviceRowSchema>;
|
||||
export type CreateCircuitWithDeviceRowsInput = z.infer<typeof createCircuitWithDeviceRowsSchema>;
|
||||
export type MoveCircuitDeviceRowInput = z.infer<typeof moveCircuitDeviceRowSchema>;
|
||||
export type MoveCircuitDeviceRowsBulkInput = z.infer<typeof moveCircuitDeviceRowsBulkSchema>;
|
||||
export type ReorderSectionCircuitsInput = z.infer<typeof reorderSectionCircuitsSchema>;
|
||||
|
||||
Reference in New Issue
Block a user