Persist circuit cell edits
This commit is contained in:
@@ -7,15 +7,12 @@ import { circuits } from "../schema/circuits.js";
|
||||
import { distributionBoards } from "../schema/distribution-boards.js";
|
||||
import {
|
||||
toCircuitDeviceRowCreateValues,
|
||||
toCircuitDeviceRowPatchValues,
|
||||
toCircuitDeviceRowUpdateValues,
|
||||
type CircuitDeviceRowCreateInput,
|
||||
type CircuitDeviceRowPatchInput,
|
||||
type CircuitDeviceRowUpdateInput,
|
||||
} from "./circuit-device-row.persistence.js";
|
||||
export type {
|
||||
CircuitDeviceRowCreateInput,
|
||||
CircuitDeviceRowPatchInput,
|
||||
CircuitDeviceRowUpdateInput,
|
||||
} from "./circuit-device-row.persistence.js";
|
||||
export { toCircuitDeviceRowCreateValues } from "./circuit-device-row.persistence.js";
|
||||
@@ -102,14 +99,6 @@ export class CircuitDeviceRowRepository {
|
||||
.where(eq(circuitDeviceRows.id, rowId));
|
||||
}
|
||||
|
||||
async updateFields(rowId: string, input: CircuitDeviceRowPatchInput) {
|
||||
const values = toCircuitDeviceRowPatchValues(input);
|
||||
if (Object.keys(values).length === 0) {
|
||||
return;
|
||||
}
|
||||
await db.update(circuitDeviceRows).set(values).where(eq(circuitDeviceRows.id, rowId));
|
||||
}
|
||||
|
||||
async delete(rowId: string) {
|
||||
await db.delete(circuitDeviceRows).where(eq(circuitDeviceRows.id, rowId));
|
||||
}
|
||||
|
||||
@@ -4,18 +4,14 @@ import { db } from "../client.js";
|
||||
import { circuits } from "../schema/circuits.js";
|
||||
import {
|
||||
toCircuitCreateValues,
|
||||
toCircuitPatchValues,
|
||||
type CircuitCreatePersistenceInput,
|
||||
type CircuitPatchPersistenceInput,
|
||||
} from "./circuit.persistence.js";
|
||||
|
||||
export type {
|
||||
CircuitCreatePersistenceInput,
|
||||
CircuitPatchPersistenceInput,
|
||||
} from "./circuit.persistence.js";
|
||||
export {
|
||||
toCircuitCreateValues,
|
||||
toCircuitPatchValues,
|
||||
} from "./circuit.persistence.js";
|
||||
|
||||
export class CircuitRepository {
|
||||
@@ -84,14 +80,6 @@ export class CircuitRepository {
|
||||
.where(eq(circuits.id, circuitId));
|
||||
}
|
||||
|
||||
async updateFields(circuitId: string, input: CircuitPatchPersistenceInput) {
|
||||
const values = toCircuitPatchValues(input);
|
||||
if (Object.keys(values).length === 0) {
|
||||
return;
|
||||
}
|
||||
await db.update(circuits).set(values).where(eq(circuits.id, circuitId));
|
||||
}
|
||||
|
||||
async delete(circuitId: string) {
|
||||
await db.delete(circuits).where(eq(circuits.id, circuitId));
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ export interface CircuitTreeSectionBlock {
|
||||
|
||||
export interface CircuitTreeResponse {
|
||||
circuitListId: string;
|
||||
currentRevision: number;
|
||||
sections: CircuitTreeSectionBlock[];
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,8 @@ import type {
|
||||
MoveCircuitDeviceRowsBulkInput,
|
||||
ReorderSectionCircuitsInput,
|
||||
UpdateSectionEquipmentIdentifiersInput,
|
||||
UpdateCircuitDeviceRowInput,
|
||||
UpdateCircuitInput,
|
||||
} from "../../shared/validation/circuit.schemas.js";
|
||||
import { CircuitNumberingService } from "./circuit-numbering.service.js";
|
||||
import { deriveOverriddenFieldsForLocalEdit } from "./project-device-overrides.js";
|
||||
|
||||
export class CircuitWriteService {
|
||||
private readonly circuitRepository: CircuitRepository;
|
||||
@@ -191,21 +188,6 @@ export class CircuitWriteService {
|
||||
};
|
||||
}
|
||||
|
||||
async updateCircuit(circuitId: string, input: UpdateCircuitInput) {
|
||||
const current = await this.circuitRepository.findById(circuitId);
|
||||
if (!current) {
|
||||
throw new Error("Invalid circuit id.");
|
||||
}
|
||||
|
||||
const sectionId = input.sectionId ?? current.sectionId;
|
||||
const equipmentIdentifier = input.equipmentIdentifier ?? current.equipmentIdentifier;
|
||||
await this.assertSectionInList(sectionId, current.circuitListId);
|
||||
await this.assertUniqueEquipmentIdentifier(current.circuitListId, equipmentIdentifier, circuitId);
|
||||
|
||||
await this.circuitRepository.updateFields(circuitId, input);
|
||||
return this.circuitRepository.findById(circuitId);
|
||||
}
|
||||
|
||||
async deleteCircuit(circuitId: string) {
|
||||
const current = await this.circuitRepository.findById(circuitId);
|
||||
if (!current) {
|
||||
@@ -246,23 +228,6 @@ export class CircuitWriteService {
|
||||
return this.deviceRowRepository.findById(rowId);
|
||||
}
|
||||
|
||||
async updateDeviceRow(rowId: string, input: UpdateCircuitDeviceRowInput) {
|
||||
const current = await this.deviceRowRepository.findById(rowId);
|
||||
if (!current) {
|
||||
throw new Error("Invalid device row id.");
|
||||
}
|
||||
await this.assertValidLinkedProjectDevice(current.circuitId, input.linkedProjectDeviceId);
|
||||
const overriddenFields = deriveOverriddenFieldsForLocalEdit(
|
||||
current,
|
||||
input
|
||||
);
|
||||
await this.deviceRowRepository.updateFields(rowId, {
|
||||
...input,
|
||||
...(overriddenFields !== undefined ? { overriddenFields } : {}),
|
||||
});
|
||||
return this.deviceRowRepository.findById(rowId);
|
||||
}
|
||||
|
||||
async deleteDeviceRow(rowId: string) {
|
||||
const current = await this.deviceRowRepository.findById(rowId);
|
||||
if (!current) {
|
||||
|
||||
@@ -17,12 +17,11 @@ import {
|
||||
} from "../utils/circuit-grid-safety";
|
||||
import {
|
||||
allColumns,
|
||||
buildCircuitEditPatch,
|
||||
buildDeviceRowEditPatch,
|
||||
defaultVisibleColumnKeys,
|
||||
deviceFieldKeys,
|
||||
formatValue,
|
||||
getCircuitValue,
|
||||
getDeviceValue,
|
||||
parseNumeric,
|
||||
} from "../utils/circuit-grid-model";
|
||||
import type {
|
||||
CellKey,
|
||||
@@ -49,14 +48,17 @@ import {
|
||||
moveCircuitDeviceRowById,
|
||||
reorderSectionCircuits,
|
||||
renumberCircuitSection,
|
||||
redoProjectCommand,
|
||||
updateSectionEquipmentIdentifiers,
|
||||
updateCircuitById,
|
||||
updateCircuitDeviceRowById,
|
||||
undoProjectCommand,
|
||||
} from "../utils/api";
|
||||
import type {
|
||||
CircuitTreeCircuitDto,
|
||||
CircuitTreeDeviceRowDto,
|
||||
CircuitTreeResponseDto,
|
||||
ProjectCommandResultDto,
|
||||
ProjectDeviceDto,
|
||||
} from "../types";
|
||||
|
||||
@@ -88,6 +90,10 @@ interface HistoryCommand {
|
||||
label: string;
|
||||
redo: () => Promise<SelectionIntent | null | void>;
|
||||
undo: () => Promise<SelectionIntent | null | void>;
|
||||
persistent?: {
|
||||
undoIntent: () => SelectionIntent | null;
|
||||
redoIntent: () => SelectionIntent | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface CircuitSnapshot {
|
||||
@@ -163,6 +169,16 @@ function isPrintableKey(event: KeyboardEvent<HTMLElement>) {
|
||||
return event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey;
|
||||
}
|
||||
|
||||
function createValuesFromEditPatch(
|
||||
patch: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(patch).filter(
|
||||
([, value]) => value !== null && value !== ""
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function CircuitTreeEditor(props: { projectId: string; circuitListId: string }) {
|
||||
/*
|
||||
Circuit-tree editor invariant overview:
|
||||
@@ -201,7 +217,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
const [draggingCircuitId, setDraggingCircuitId] = useState<string | null>(null);
|
||||
const [draggingCircuitIds, setDraggingCircuitIds] = useState<string[]>([]);
|
||||
const [circuitReorderIntent, setCircuitReorderIntent] = useState<CircuitReorderDropIntent | null>(null);
|
||||
// Undo/redo history is session-local UI state (not persisted server-side).
|
||||
// The visible stack is session-local during the cutover. Circuit/device field
|
||||
// commands already use the persistent project-wide server history.
|
||||
const [undoStack, setUndoStack] = useState<HistoryCommand[]>([]);
|
||||
const [redoStack, setRedoStack] = useState<HistoryCommand[]>([]);
|
||||
const [historyBusy, setHistoryBusy] = useState(false);
|
||||
@@ -223,6 +240,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const focusTokenRef = useRef(1);
|
||||
const projectRevisionRef = useRef<number | null>(null);
|
||||
|
||||
const orderedColumns = useMemo(
|
||||
() => columnOrder.map((key) => allColumns.find((column) => column.key === key)).filter(Boolean) as ColumnDef[],
|
||||
@@ -241,6 +259,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
setError(null);
|
||||
try {
|
||||
const tree = await getCircuitTree(projectId, circuitListId);
|
||||
projectRevisionRef.current = tree.currentRevision;
|
||||
setData(tree);
|
||||
} catch (err) {
|
||||
setError(normalizeUiError(err));
|
||||
@@ -767,6 +786,25 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
}
|
||||
|
||||
function getExpectedProjectRevision() {
|
||||
if (projectRevisionRef.current === null) {
|
||||
throw new Error("Der Projektstand ist noch nicht geladen.");
|
||||
}
|
||||
return projectRevisionRef.current;
|
||||
}
|
||||
|
||||
function applyProjectCommandResult(result: ProjectCommandResultDto) {
|
||||
projectRevisionRef.current = result.history.currentRevision;
|
||||
setData((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
currentRevision: result.history.currentRevision,
|
||||
}
|
||||
: current
|
||||
);
|
||||
}
|
||||
|
||||
// Runs a normal command and records it in session-local history.
|
||||
async function runCommand(command: HistoryCommand) {
|
||||
try {
|
||||
@@ -790,7 +828,29 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
setError(null);
|
||||
setHistoryBusy(true);
|
||||
setIsSaving(true);
|
||||
const intent = mode === "undo" ? await command.undo() : await command.redo();
|
||||
let intent: SelectionIntent | null | void;
|
||||
if (command.persistent) {
|
||||
const result =
|
||||
mode === "undo"
|
||||
? await undoProjectCommand(
|
||||
projectId,
|
||||
getExpectedProjectRevision()
|
||||
)
|
||||
: await redoProjectCommand(
|
||||
projectId,
|
||||
getExpectedProjectRevision()
|
||||
);
|
||||
applyProjectCommandResult(result);
|
||||
intent =
|
||||
mode === "undo"
|
||||
? command.persistent.undoIntent()
|
||||
: command.persistent.redoIntent();
|
||||
} else {
|
||||
intent =
|
||||
mode === "undo"
|
||||
? await command.undo()
|
||||
: await command.redo();
|
||||
}
|
||||
await reloadWithIntent(intent ?? null);
|
||||
if (mode === "undo") {
|
||||
setUndoStack((current) => current.slice(0, -1));
|
||||
@@ -1145,46 +1205,26 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
setSelectedCell({ rowKey: targetRowKey, cellKey: best });
|
||||
}
|
||||
|
||||
// Maps generic grid cell edits to circuit PATCH payload fields.
|
||||
// Maps grid edits to typed persistent project commands.
|
||||
async function patchCircuit(circuitId: string, key: CellKey, draft: string) {
|
||||
const payload: Record<string, unknown> = {};
|
||||
if (key === "protectionSummary" || key === "cableSummary") {
|
||||
payload.remark = draft.trim() === "" ? undefined : draft;
|
||||
} else if (["protectionRatedCurrent", "cableLength", "voltage"].includes(key)) {
|
||||
payload[key] = parseNumeric(key, draft);
|
||||
} else if (key === "isReserve") {
|
||||
const normalized = draft.trim().toLowerCase();
|
||||
payload.isReserve = ["1", "true", "yes", "ja"].includes(normalized);
|
||||
} else {
|
||||
payload[key] = draft.trim() === "" ? undefined : draft;
|
||||
}
|
||||
await updateCircuitById(circuitId, payload);
|
||||
const result = await updateCircuitById(
|
||||
projectId,
|
||||
getExpectedProjectRevision(),
|
||||
circuitId,
|
||||
buildCircuitEditPatch(key, draft)
|
||||
);
|
||||
applyProjectCommandResult(result);
|
||||
}
|
||||
|
||||
// Maps generic grid cell edits to device-row PATCH payload fields.
|
||||
// Device-row commands derive local ProjectDevice override metadata server-side.
|
||||
async function patchDeviceRow(rowId: string, key: CellKey, draft: string) {
|
||||
const payload: Record<string, unknown> = {};
|
||||
if (["quantity", "powerPerUnit", "simultaneityFactor", "cosPhi"].includes(key)) {
|
||||
payload[key] = parseNumeric(key, draft);
|
||||
} else if (key === "roomSummary") {
|
||||
const trimmed = draft.trim();
|
||||
if (!trimmed) {
|
||||
payload.roomNumberSnapshot = undefined;
|
||||
payload.roomNameSnapshot = undefined;
|
||||
} else {
|
||||
const firstSpace = trimmed.indexOf(" ");
|
||||
if (firstSpace > 0) {
|
||||
payload.roomNumberSnapshot = trimmed.slice(0, firstSpace).trim();
|
||||
payload.roomNameSnapshot = trimmed.slice(firstSpace + 1).trim();
|
||||
} else {
|
||||
payload.roomNumberSnapshot = trimmed;
|
||||
payload.roomNameSnapshot = undefined;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
payload[key] = draft.trim() === "" ? undefined : draft;
|
||||
}
|
||||
await updateCircuitDeviceRowById(rowId, payload);
|
||||
const result = await updateCircuitDeviceRowById(
|
||||
projectId,
|
||||
getExpectedProjectRevision(),
|
||||
rowId,
|
||||
buildDeviceRowEditPatch(key, draft)
|
||||
);
|
||||
applyProjectCommandResult(result);
|
||||
}
|
||||
|
||||
// Creates a real circuit from virtual placeholder row.
|
||||
@@ -1204,6 +1244,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
const isDeviceField = deviceFieldKeys.has(key);
|
||||
|
||||
if (isDeviceField) {
|
||||
const editValues = createValuesFromEditPatch(
|
||||
buildDeviceRowEditPatch(key, draft)
|
||||
);
|
||||
const created = await createCircuitWithDeviceRows(projectId, circuitListId, {
|
||||
circuit: {
|
||||
sectionId,
|
||||
@@ -1221,37 +1264,28 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
powerPerUnit: 0,
|
||||
simultaneityFactor: 1,
|
||||
cosPhi: 1,
|
||||
...editValues,
|
||||
},
|
||||
],
|
||||
});
|
||||
const createdCircuit = created.circuit;
|
||||
const createdRow = created.deviceRows[0];
|
||||
await patchDeviceRow(createdRow.id, key, draft);
|
||||
return { rowKey: `circuitCompact:${createdCircuit.id}`, cellKey: key };
|
||||
}
|
||||
|
||||
const editValues = createValuesFromEditPatch(
|
||||
buildCircuitEditPatch(key, draft)
|
||||
);
|
||||
const createdCircuit = (await createCircuit(projectId, circuitListId, {
|
||||
sectionId,
|
||||
equipmentIdentifier: next.nextIdentifier,
|
||||
displayName: "Neuer Stromkreis",
|
||||
sortOrder,
|
||||
isReserve: true,
|
||||
...editValues,
|
||||
})) as CircuitTreeCircuitDto;
|
||||
await patchCircuit(createdCircuit.id, key, draft);
|
||||
return { rowKey: `reserveCircuit:${createdCircuit.id}`, cellKey: key };
|
||||
}
|
||||
|
||||
// Reads current persisted value into editable draft text for reversible edit commands.
|
||||
function getCurrentDraftForCell(row: VisibleGridRow, cellKey: CellKey, kind: CellKind) {
|
||||
if (kind === "deviceField" && row.device) {
|
||||
return String(getDeviceValue(row.device, cellKey) ?? "");
|
||||
}
|
||||
if (kind === "circuitField" && row.circuit) {
|
||||
return String(getCircuitValue(row.circuit, cellKey) ?? "");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Commits the active editor input through command history so edits remain undoable.
|
||||
async function commitEdit(direction: SaveDirection = "stay") {
|
||||
if (!editingCell) {
|
||||
@@ -1316,7 +1350,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
|
||||
if (cell.kind === "circuitField" && row.circuit) {
|
||||
const previous = getCurrentDraftForCell(row, editingCell.cellKey, cell.kind);
|
||||
const next = editingCell.draft;
|
||||
const command: HistoryCommand = {
|
||||
label: "Stromkreisfeld bearbeiten",
|
||||
@@ -1324,9 +1357,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
await patchCircuit(row.circuit!.id, editingCell.cellKey, next);
|
||||
return nextSelectionIntent();
|
||||
},
|
||||
undo: async () => {
|
||||
await patchCircuit(row.circuit!.id, editingCell.cellKey, previous);
|
||||
return buildSelectionIntent({ rowKey: row.rowKey, cellKey: editingCell.cellKey });
|
||||
undo: async () =>
|
||||
buildSelectionIntent({
|
||||
rowKey: row.rowKey,
|
||||
cellKey: editingCell.cellKey,
|
||||
}),
|
||||
persistent: {
|
||||
undoIntent: () =>
|
||||
buildSelectionIntent({
|
||||
rowKey: row.rowKey,
|
||||
cellKey: editingCell.cellKey,
|
||||
}),
|
||||
redoIntent: nextSelectionIntent,
|
||||
},
|
||||
};
|
||||
setEditingCell(null);
|
||||
@@ -1335,7 +1377,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
|
||||
if (cell.kind === "deviceField" && row.device) {
|
||||
const previous = getCurrentDraftForCell(row, editingCell.cellKey, cell.kind);
|
||||
const next = editingCell.draft;
|
||||
const command: HistoryCommand = {
|
||||
label: "Gerätefeld bearbeiten",
|
||||
@@ -1343,9 +1384,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
await patchDeviceRow(row.device!.id, editingCell.cellKey, next);
|
||||
return nextSelectionIntent();
|
||||
},
|
||||
undo: async () => {
|
||||
await patchDeviceRow(row.device!.id, editingCell.cellKey, previous);
|
||||
return buildSelectionIntent({ rowKey: `device:${row.device!.id}`, cellKey: editingCell.cellKey });
|
||||
undo: async () =>
|
||||
buildSelectionIntent({
|
||||
rowKey: `device:${row.device!.id}`,
|
||||
cellKey: editingCell.cellKey,
|
||||
}),
|
||||
persistent: {
|
||||
undoIntent: () =>
|
||||
buildSelectionIntent({
|
||||
rowKey: `device:${row.device!.id}`,
|
||||
cellKey: editingCell.cellKey,
|
||||
}),
|
||||
redoIntent: nextSelectionIntent,
|
||||
},
|
||||
};
|
||||
setEditingCell(null);
|
||||
@@ -1359,6 +1409,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
const command: HistoryCommand = {
|
||||
label: "Gerätezeile im Reservestromkreis erstellen",
|
||||
redo: async () => {
|
||||
const editValues = createValuesFromEditPatch(
|
||||
buildDeviceRowEditPatch(editingCell.cellKey, next)
|
||||
);
|
||||
const created = (await createCircuitDeviceRow(row.circuit!.id, {
|
||||
name: "Reserveverbraucher",
|
||||
displayName: "Reserveverbraucher",
|
||||
@@ -1367,9 +1420,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
powerPerUnit: 0,
|
||||
simultaneityFactor: 1,
|
||||
cosPhi: 1,
|
||||
...editValues,
|
||||
})) as { id: string };
|
||||
createdRowId = created.id;
|
||||
await patchDeviceRow(created.id, editingCell.cellKey, next);
|
||||
targetSelection = { rowKey: `circuitCompact:${row.circuit!.id}`, cellKey: editingCell.cellKey };
|
||||
return nextSelectionIntent();
|
||||
},
|
||||
|
||||
@@ -240,6 +240,7 @@ export interface CircuitTreeMigrationReportDto {
|
||||
|
||||
export interface CircuitTreeResponseDto {
|
||||
circuitListId: string;
|
||||
currentRevision: number;
|
||||
sections: CircuitTreeSectionDto[];
|
||||
migrationReport?: CircuitTreeMigrationReportDto;
|
||||
}
|
||||
@@ -264,8 +265,6 @@ export interface CreateCircuitInputDto {
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export type UpdateCircuitInputDto = Partial<CreateCircuitInputDto>;
|
||||
|
||||
export interface CreateCircuitDeviceRowInputDto {
|
||||
linkedProjectDeviceId?: string;
|
||||
name: string;
|
||||
@@ -287,5 +286,4 @@ export interface CreateCircuitDeviceRowInputDto {
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export type UpdateCircuitDeviceRowInputDto = Partial<CreateCircuitDeviceRowInputDto>;
|
||||
import type { ProjectDeviceSyncField } from "../shared/constants/project-device-sync-fields";
|
||||
|
||||
+32
-12
@@ -20,11 +20,17 @@ import type {
|
||||
CircuitTreeCircuitDto,
|
||||
CircuitTreeDeviceRowDto,
|
||||
CreateCircuitInputDto,
|
||||
UpdateCircuitInputDto,
|
||||
CreateCircuitDeviceRowInputDto,
|
||||
UpdateCircuitDeviceRowInputDto,
|
||||
} from "../types";
|
||||
import type { ProjectDeviceSyncField } from "../../shared/constants/project-device-sync-fields";
|
||||
import {
|
||||
createCircuitUpdateProjectCommand,
|
||||
type CircuitUpdatePatch,
|
||||
} from "../../domain/models/circuit-project-command.model";
|
||||
import {
|
||||
createCircuitDeviceRowUpdateProjectCommand,
|
||||
type CircuitDeviceRowUpdatePatch,
|
||||
} from "../../domain/models/circuit-device-row-project-command.model";
|
||||
|
||||
async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(url, {
|
||||
@@ -180,11 +186,18 @@ export function createCircuitWithDeviceRows(
|
||||
);
|
||||
}
|
||||
|
||||
export function updateCircuitById(circuitId: string, input: UpdateCircuitInputDto) {
|
||||
return request(`/api/circuits/${circuitId}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
export function updateCircuitById(
|
||||
projectId: string,
|
||||
expectedRevision: number,
|
||||
circuitId: string,
|
||||
input: CircuitUpdatePatch
|
||||
) {
|
||||
return executeProjectCommand(
|
||||
projectId,
|
||||
expectedRevision,
|
||||
createCircuitUpdateProjectCommand(circuitId, input),
|
||||
"Stromkreisfeld bearbeiten"
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteCircuitById(circuitId: string) {
|
||||
@@ -206,11 +219,18 @@ export function createCircuitDeviceRow(circuitId: string, input: CreateCircuitDe
|
||||
});
|
||||
}
|
||||
|
||||
export function updateCircuitDeviceRowById(rowId: string, input: UpdateCircuitDeviceRowInputDto) {
|
||||
return request(`/api/circuit-device-rows/${rowId}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
export function updateCircuitDeviceRowById(
|
||||
projectId: string,
|
||||
expectedRevision: number,
|
||||
rowId: string,
|
||||
input: CircuitDeviceRowUpdatePatch
|
||||
) {
|
||||
return executeProjectCommand(
|
||||
projectId,
|
||||
expectedRevision,
|
||||
createCircuitDeviceRowUpdateProjectCommand(rowId, input),
|
||||
"Gerätezeilenfeld bearbeiten"
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteCircuitDeviceRowById(rowId: string) {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { CircuitTreeCircuitDto, CircuitTreeDeviceRowDto } from "../types.js";
|
||||
import type { CircuitUpdatePatch } from "../../domain/models/circuit-project-command.model.js";
|
||||
import type { CircuitDeviceRowUpdatePatch } from "../../domain/models/circuit-device-row-project-command.model.js";
|
||||
|
||||
export type CellKey =
|
||||
| "equipmentIdentifier"
|
||||
@@ -189,6 +191,78 @@ export function parseNumeric(cellKey: CellKey, draft: string): number | undefine
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function buildCircuitEditPatch(
|
||||
key: CellKey,
|
||||
draft: string
|
||||
): CircuitUpdatePatch {
|
||||
if (key === "protectionSummary" || key === "cableSummary") {
|
||||
return { remark: draft.trim() || null };
|
||||
}
|
||||
if (
|
||||
key === "protectionRatedCurrent" ||
|
||||
key === "cableLength" ||
|
||||
key === "voltage"
|
||||
) {
|
||||
return { [key]: parseNumeric(key, draft) ?? null };
|
||||
}
|
||||
if (key === "isReserve") {
|
||||
const normalized = draft.trim().toLowerCase();
|
||||
return {
|
||||
isReserve: ["1", "true", "yes", "ja"].includes(normalized),
|
||||
};
|
||||
}
|
||||
if (key === "equipmentIdentifier") {
|
||||
return { equipmentIdentifier: draft.trim() };
|
||||
}
|
||||
return { [key]: draft.trim() || null } as CircuitUpdatePatch;
|
||||
}
|
||||
|
||||
export function buildDeviceRowEditPatch(
|
||||
key: CellKey,
|
||||
draft: string
|
||||
): CircuitDeviceRowUpdatePatch {
|
||||
if (
|
||||
key === "quantity" ||
|
||||
key === "powerPerUnit" ||
|
||||
key === "simultaneityFactor"
|
||||
) {
|
||||
const value = parseNumeric(key, draft);
|
||||
if (value === undefined) {
|
||||
throw new Error("Dieses Zahlenfeld darf nicht leer sein.");
|
||||
}
|
||||
return { [key]: value };
|
||||
}
|
||||
if (key === "cosPhi") {
|
||||
return { cosPhi: parseNumeric(key, draft) ?? null };
|
||||
}
|
||||
if (key === "roomSummary") {
|
||||
const trimmed = draft.trim();
|
||||
if (!trimmed) {
|
||||
return {
|
||||
roomNumberSnapshot: null,
|
||||
roomNameSnapshot: null,
|
||||
};
|
||||
}
|
||||
const firstSpace = trimmed.indexOf(" ");
|
||||
return firstSpace > 0
|
||||
? {
|
||||
roomNumberSnapshot: trimmed.slice(0, firstSpace).trim(),
|
||||
roomNameSnapshot: trimmed.slice(firstSpace + 1).trim() || null,
|
||||
}
|
||||
: {
|
||||
roomNumberSnapshot: trimmed,
|
||||
roomNameSnapshot: null,
|
||||
};
|
||||
}
|
||||
if (key === "technicalName") {
|
||||
return { name: draft.trim() };
|
||||
}
|
||||
if (key === "displayName") {
|
||||
return { displayName: draft.trim() };
|
||||
}
|
||||
return { [key]: draft.trim() || null } as CircuitDeviceRowUpdatePatch;
|
||||
}
|
||||
|
||||
export function getDeviceValue(device: CircuitTreeDeviceRowDto, key: CellKey): GridValue {
|
||||
switch (key) {
|
||||
case "technicalName": return device.name;
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
createCircuitDeviceRowSchema,
|
||||
moveCircuitDeviceRowsBulkSchema,
|
||||
moveCircuitDeviceRowSchema,
|
||||
updateCircuitDeviceRowSchema,
|
||||
} from "../../shared/validation/circuit.schemas.js";
|
||||
|
||||
export async function createCircuitDeviceRow(req: Request, res: Response) {
|
||||
@@ -26,25 +25,6 @@ export async function createCircuitDeviceRow(req: Request, res: Response) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateCircuitDeviceRow(req: Request, res: Response) {
|
||||
const { rowId } = req.params;
|
||||
if (typeof rowId !== "string") {
|
||||
return res.status(400).json({ error: "Invalid rowId" });
|
||||
}
|
||||
|
||||
const parsed = updateCircuitDeviceRowSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: parsed.error.flatten() });
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await circuitWriteService.updateDeviceRow(rowId, parsed.data);
|
||||
return res.json(updated);
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to update device row." });
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCircuitDeviceRow(req: Request, res: Response) {
|
||||
const { rowId } = req.params;
|
||||
if (typeof rowId !== "string") {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CircuitRepository } from "../../db/repositories/circuit.repository.js";
|
||||
import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js";
|
||||
import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js";
|
||||
import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js";
|
||||
import { ProjectRepository } from "../../db/repositories/project.repository.js";
|
||||
import {
|
||||
calculateCircuitTotalPower,
|
||||
calculateRowTotalPower,
|
||||
@@ -13,6 +14,7 @@ const circuitListRepository = new CircuitListRepository();
|
||||
const circuitSectionRepository = new CircuitSectionRepository();
|
||||
const circuitRepository = new CircuitRepository();
|
||||
const circuitDeviceRowRepository = new CircuitDeviceRowRepository();
|
||||
const projectRepository = new ProjectRepository();
|
||||
|
||||
export function isMissingCircuitTreeSchemaError(error: unknown): boolean {
|
||||
if (!(error instanceof Error)) {
|
||||
@@ -35,6 +37,10 @@ export async function getCircuitTree(req: Request, res: Response) {
|
||||
if (!list) {
|
||||
return res.status(404).json({ error: "Circuit list not found" });
|
||||
}
|
||||
const project = await projectRepository.findById(projectId);
|
||||
if (!project) {
|
||||
return res.status(404).json({ error: "Project not found" });
|
||||
}
|
||||
|
||||
try {
|
||||
const sections = await circuitSectionRepository.listByCircuitList(circuitListId);
|
||||
@@ -52,6 +58,7 @@ export async function getCircuitTree(req: Request, res: Response) {
|
||||
|
||||
const tree: CircuitTreeResponse = {
|
||||
circuitListId,
|
||||
currentRevision: project.currentRevision,
|
||||
sections: sections.map((section) => ({
|
||||
id: section.id,
|
||||
key: section.key,
|
||||
@@ -123,6 +130,7 @@ export async function getCircuitTree(req: Request, res: Response) {
|
||||
if (isMissingCircuitTreeSchemaError(error)) {
|
||||
return res.json({
|
||||
circuitListId,
|
||||
currentRevision: project.currentRevision,
|
||||
sections: [],
|
||||
warning:
|
||||
"Circuit-first tables are not available yet. Run database migrations (including 0008_circuit_first_model).",
|
||||
|
||||
@@ -3,7 +3,6 @@ import { circuitWriteService } from "../composition/circuit-write-service.js";
|
||||
import {
|
||||
createCircuitSchema,
|
||||
createCircuitWithDeviceRowsSchema,
|
||||
updateCircuitSchema,
|
||||
} from "../../shared/validation/circuit.schemas.js";
|
||||
|
||||
export async function createCircuit(req: Request, res: Response) {
|
||||
@@ -53,25 +52,6 @@ export async function createCircuitWithDeviceRows(req: Request, res: Response) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateCircuit(req: Request, res: Response) {
|
||||
const { circuitId } = req.params;
|
||||
if (typeof circuitId !== "string") {
|
||||
return res.status(400).json({ error: "Invalid circuitId" });
|
||||
}
|
||||
|
||||
const parsed = updateCircuitSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: parsed.error.flatten() });
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await circuitWriteService.updateCircuit(circuitId, parsed.data);
|
||||
return res.json(updated);
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to update circuit." });
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCircuit(req: Request, res: Response) {
|
||||
const { circuitId } = req.params;
|
||||
if (typeof circuitId !== "string") {
|
||||
|
||||
@@ -3,12 +3,10 @@ import {
|
||||
deleteCircuitDeviceRow,
|
||||
moveCircuitDeviceRowsBulk,
|
||||
moveCircuitDeviceRow,
|
||||
updateCircuitDeviceRow,
|
||||
} from "../controllers/circuit-device-row.controller.js";
|
||||
|
||||
export const circuitDeviceRowRouter = Router();
|
||||
|
||||
circuitDeviceRowRouter.patch("/circuit-device-rows/move-bulk", moveCircuitDeviceRowsBulk);
|
||||
circuitDeviceRowRouter.patch("/circuit-device-rows/:rowId", updateCircuitDeviceRow);
|
||||
circuitDeviceRowRouter.patch("/circuit-device-rows/:rowId/move", moveCircuitDeviceRow);
|
||||
circuitDeviceRowRouter.delete("/circuit-device-rows/:rowId", deleteCircuitDeviceRow);
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
createCircuitWithDeviceRows,
|
||||
deleteCircuit,
|
||||
getNextCircuitIdentifier,
|
||||
updateCircuit,
|
||||
} from "../controllers/circuit.controller.js";
|
||||
import { createCircuitDeviceRow } from "../controllers/circuit-device-row.controller.js";
|
||||
|
||||
@@ -15,7 +14,6 @@ circuitRouter.post(
|
||||
"/projects/:projectId/circuit-lists/:circuitListId/circuits-with-device-rows",
|
||||
createCircuitWithDeviceRows
|
||||
);
|
||||
circuitRouter.patch("/circuits/:circuitId", updateCircuit);
|
||||
circuitRouter.delete("/circuits/:circuitId", deleteCircuit);
|
||||
circuitRouter.get("/circuit-sections/:sectionId/next-identifier", getNextCircuitIdentifier);
|
||||
circuitRouter.post("/circuits/:circuitId/device-rows", createCircuitDeviceRow);
|
||||
|
||||
@@ -20,13 +20,6 @@ export const createCircuitSchema = z.object({
|
||||
remark: z.string().optional(),
|
||||
});
|
||||
|
||||
export const updateCircuitSchema = createCircuitSchema.partial().extend({
|
||||
sectionId: z.string().min(1).optional(),
|
||||
equipmentIdentifier: z.string().min(1).optional(),
|
||||
sortOrder: z.number().optional(),
|
||||
isReserve: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const createCircuitDeviceRowSchema = z.object({
|
||||
linkedProjectDeviceId: z.string().min(1).optional(),
|
||||
name: z.string().min(1),
|
||||
@@ -53,8 +46,6 @@ export const createCircuitWithDeviceRowsSchema = z.object({
|
||||
deviceRows: z.array(createCircuitDeviceRowSchema).min(1),
|
||||
});
|
||||
|
||||
export const updateCircuitDeviceRowSchema = createCircuitDeviceRowSchema.partial();
|
||||
|
||||
export const moveCircuitDeviceRowSchema = z
|
||||
.object({
|
||||
targetCircuitId: z.string().min(1).optional(),
|
||||
@@ -98,10 +89,8 @@ export const updateSectionEquipmentIdentifiersSchema = z.object({
|
||||
});
|
||||
|
||||
export type CreateCircuitInput = z.infer<typeof createCircuitSchema>;
|
||||
export type UpdateCircuitInput = z.infer<typeof updateCircuitSchema>;
|
||||
export type CreateCircuitDeviceRowInput = z.infer<typeof createCircuitDeviceRowSchema>;
|
||||
export type CreateCircuitWithDeviceRowsInput = z.infer<typeof createCircuitWithDeviceRowsSchema>;
|
||||
export type UpdateCircuitDeviceRowInput = z.infer<typeof updateCircuitDeviceRowSchema>;
|
||||
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