From ad498f2bb56c348b578bcb4dd7e9075c71cd4206 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 3 May 2026 23:00:46 +0200 Subject: [PATCH] New usable editor --- src/app/globals.css | 118 +++ .../[circuitListId]/tree-edit/page.tsx | 33 + .../[circuitListId]/tree/page.tsx | 7 +- .../components/circuit-tree-editor.tsx | 744 ++++++++++++++++++ src/frontend/types.ts | 44 ++ src/frontend/utils/api.ts | 56 ++ 6 files changed, 1001 insertions(+), 1 deletion(-) create mode 100644 src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree-edit/page.tsx create mode 100644 src/frontend/components/circuit-tree-editor.tsx diff --git a/src/app/globals.css b/src/app/globals.css index dbc80ca..3a008a5 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -39,3 +39,121 @@ body { .circuit-tree-table .indented-cell { padding-left: 1.5rem; } + +.tree-editor-shell { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.tree-grid-wrap { + overflow: auto; + border: 1px solid #d9dee8; + background: #fff; +} + +.tree-grid { + width: max-content; + min-width: 100%; + border-collapse: collapse; + font-size: 0.9rem; +} + +.tree-grid th, +.tree-grid td { + border: 1px solid #e4e9f2; + padding: 0.35rem 0.5rem; + white-space: nowrap; + vertical-align: middle; +} + +.tree-grid th { + background: #f4f7fb; + position: sticky; + top: 0; + z-index: 2; +} + +.tree-grid .num { + text-align: right; +} + +.tree-grid .section-row td { + background: #e8eef8; + font-weight: 600; +} + +.tree-grid .summary-row td { + background: #f3f7fd; +} + +.tree-grid .device-row td:first-child { + color: #6b7280; +} + +.tree-grid .reserve-row td { + background: #fff8e7; +} + +.tree-grid .placeholder-row td { + background: #f7f7f7; + color: #6b7280; + font-style: italic; +} + +.tree-grid .cell-editable { + cursor: text; +} + +.tree-grid .cell-selected { + outline: 2px solid #4c7dd9; + outline-offset: -2px; +} + +.tree-grid input { + width: 100%; + min-width: 5rem; + border: 1px solid #9fb6e0; + border-radius: 2px; + padding: 0.2rem 0.3rem; +} + +.tree-grid .action-cell { + display: flex; + gap: 0.3rem; +} + +.tree-grid .action-cell button { + border: 1px solid #c4cddc; + background: #fff; + padding: 0.2rem 0.45rem; + border-radius: 3px; + font-size: 0.78rem; +} + +.notice { + padding: 0.5rem 0.75rem; + border-radius: 4px; + border: 1px solid transparent; +} + +.notice.info { + background: #ebf3ff; + border-color: #bad1f7; +} + +.notice.error { + background: #fdecec; + border-color: #f5b5b5; +} + +.notice.muted { + background: #f6f6f6; + border-color: #e4e4e4; +} + +.todo-hint { + color: #6b7280; + font-size: 0.8rem; + margin: 0; +} diff --git a/src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree-edit/page.tsx b/src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree-edit/page.tsx new file mode 100644 index 0000000..6b2ef6a --- /dev/null +++ b/src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree-edit/page.tsx @@ -0,0 +1,33 @@ +"use client"; + +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { CircuitTreeEditor } from "../../../../../../frontend/components/circuit-tree-editor"; + +export default function CircuitTreeEditPage() { + const params = useParams<{ projectId: string; circuitListId: string }>(); + + return ( +
+
+
+

Circuit Tree Editor

+

Basic editing for the circuit-first model.

+
+
+ + Read-only tree + + + Legacy editor + +
+
+ +
+ ); +} + diff --git a/src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree/page.tsx b/src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree/page.tsx index 5426907..45225f6 100644 --- a/src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree/page.tsx +++ b/src/app/projects/[projectId]/circuit-lists/[circuitListId]/tree/page.tsx @@ -18,6 +18,12 @@ export default function CircuitTreePreviewPage() { Read-only preview of section blocks, circuits and device rows.

+ + Open editor + ); } - diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx new file mode 100644 index 0000000..9ca8e9a --- /dev/null +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -0,0 +1,744 @@ +"use client"; + +import { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react"; +import { + createCircuit, + createCircuitDeviceRow, + deleteCircuitById, + deleteCircuitDeviceRowById, + getCircuitTree, + getNextCircuitIdentifier, + renumberCircuitSection, + updateCircuitById, + updateCircuitDeviceRowById, +} from "../utils/api"; +import type { CircuitTreeCircuitDto, CircuitTreeDeviceRowDto, CircuitTreeResponseDto } from "../types"; + +type CellKey = + | "equipmentIdentifier" + | "name" + | "displayName" + | "phaseType" + | "connectionKind" + | "costGroup" + | "category" + | "level" + | "roomNumberSnapshot" + | "roomNameSnapshot" + | "quantity" + | "powerPerUnit" + | "simultaneityFactor" + | "cosPhi" + | "rowTotalPower" + | "circuitTotalPower" + | "protectionType" + | "protectionRatedCurrent" + | "protectionCharacteristic" + | "cableType" + | "cableCrossSection" + | "cableLength" + | "rcdAssignment" + | "terminalDesignation" + | "voltage" + | "status" + | "isReserve" + | "remark"; + +interface GridRow { + rowKey: string; + kind: "section" | "summary" | "compact" | "device" | "reserve" | "placeholder"; + sectionId: string; + circuit?: CircuitTreeCircuitDto; + device?: CircuitTreeDeviceRowDto; +} + +interface SelectedCell { + rowKey: string; + cellKey: CellKey; +} + +interface EditingCell extends SelectedCell { + draft: string; +} + +const columns: Array<{ key: CellKey; label: string; numeric?: boolean }> = [ + { key: "equipmentIdentifier", label: "Equipment identifier" }, + { key: "name", label: "Name" }, + { key: "displayName", label: "Display name" }, + { key: "phaseType", label: "Phase type" }, + { key: "connectionKind", label: "Connection kind" }, + { key: "costGroup", label: "Cost group" }, + { key: "category", label: "Category" }, + { key: "level", label: "Level" }, + { key: "roomNumberSnapshot", label: "Room number" }, + { key: "roomNameSnapshot", label: "Room name" }, + { key: "quantity", label: "Quantity", numeric: true }, + { key: "powerPerUnit", label: "Power / unit", numeric: true }, + { key: "simultaneityFactor", label: "Simultaneity", numeric: true }, + { key: "cosPhi", label: "cosPhi", numeric: true }, + { key: "rowTotalPower", label: "Row total", numeric: true }, + { key: "circuitTotalPower", label: "Circuit total", numeric: true }, + { key: "protectionType", label: "Protection type" }, + { key: "protectionRatedCurrent", label: "Protection current", numeric: true }, + { key: "protectionCharacteristic", label: "Protection characteristic" }, + { key: "cableType", label: "Cable type" }, + { key: "cableCrossSection", label: "Cable cross-section" }, + { key: "cableLength", label: "Cable length", numeric: true }, + { key: "rcdAssignment", label: "RCD assignment" }, + { key: "terminalDesignation", label: "Terminal designation" }, + { key: "voltage", label: "Voltage", numeric: true }, + { key: "status", label: "Status" }, + { key: "isReserve", label: "Reserve" }, + { key: "remark", label: "Remark" }, +]; + +function formatValue(value: string | number | boolean | undefined) { + if (value === undefined || value === null || value === "") { + return "-"; + } + if (typeof value === "boolean") { + return value ? "Yes" : "No"; + } + return String(value); +} + +function getDeviceFieldValue(device: CircuitTreeDeviceRowDto, cellKey: CellKey): string | number | boolean | undefined { + switch (cellKey) { + case "name": + return device.name; + case "displayName": + return device.displayName || device.name; + case "phaseType": + return device.phaseType; + case "connectionKind": + return device.connectionKind; + case "costGroup": + return device.costGroup; + case "category": + return device.category; + case "level": + return device.level; + case "roomNumberSnapshot": + return device.roomNumberSnapshot; + case "roomNameSnapshot": + return device.roomNameSnapshot; + case "quantity": + return device.quantity; + case "powerPerUnit": + return device.powerPerUnit; + case "simultaneityFactor": + return device.simultaneityFactor; + case "cosPhi": + return device.cosPhi; + case "rowTotalPower": + return device.rowTotalPower; + case "remark": + return device.remark; + default: + return undefined; + } +} + +function getCircuitFieldValue(circuit: CircuitTreeCircuitDto, cellKey: CellKey): string | number | boolean | undefined { + switch (cellKey) { + case "equipmentIdentifier": + return circuit.equipmentIdentifier; + case "displayName": + return circuit.displayName; + case "circuitTotalPower": + return circuit.circuitTotalPower; + case "protectionType": + return circuit.protectionType; + case "protectionRatedCurrent": + return circuit.protectionRatedCurrent; + case "protectionCharacteristic": + return circuit.protectionCharacteristic; + case "cableType": + return circuit.cableType; + case "cableCrossSection": + return circuit.cableCrossSection; + case "cableLength": + return circuit.cableLength; + case "rcdAssignment": + return circuit.rcdAssignment; + case "terminalDesignation": + return circuit.terminalDesignation; + case "voltage": + return circuit.voltage; + case "status": + return circuit.status; + case "isReserve": + return circuit.isReserve; + case "remark": + return circuit.remark; + default: + return undefined; + } +} + +function isCircuitField(cellKey: CellKey) { + return [ + "equipmentIdentifier", + "displayName", + "protectionType", + "protectionRatedCurrent", + "protectionCharacteristic", + "cableType", + "cableCrossSection", + "cableLength", + "rcdAssignment", + "terminalDesignation", + "voltage", + "status", + "isReserve", + "remark", + "circuitTotalPower", + ].includes(cellKey); +} + +function isDeviceField(cellKey: CellKey) { + return [ + "name", + "displayName", + "phaseType", + "connectionKind", + "costGroup", + "category", + "level", + "roomNumberSnapshot", + "roomNameSnapshot", + "quantity", + "powerPerUnit", + "simultaneityFactor", + "cosPhi", + "remark", + "rowTotalPower", + ].includes(cellKey); +} + +function parseNumeric(cellKey: CellKey, draft: string): number | undefined { + const trimmed = draft.trim(); + if (trimmed === "") { + return undefined; + } + const parsed = Number(trimmed); + if (Number.isNaN(parsed)) { + throw new Error(`Invalid number in ${cellKey}`); + } + return parsed; +} + +export function CircuitTreeEditor(props: { projectId: string; circuitListId: string }) { + const { projectId, circuitListId } = props; + const [data, setData] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [selectedCell, setSelectedCell] = useState(null); + const [editingCell, setEditingCell] = useState(null); + const [activeSectionId, setActiveSectionId] = useState(null); + const [isSaving, setIsSaving] = useState(false); + const [pendingFocus, setPendingFocus] = useState(null); + const containerRef = useRef(null); + + async function loadTree() { + setIsLoading(true); + setError(null); + try { + const tree = await getCircuitTree(projectId, circuitListId); + setData(tree); + } catch (err) { + setError(err instanceof Error ? err.message : "Could not load circuit tree."); + } finally { + setIsLoading(false); + } + } + + useEffect(() => { + void loadTree(); + }, [projectId, circuitListId]); + + const gridRows = useMemo(() => { + if (!data) { + return [] as GridRow[]; + } + const rows: GridRow[] = []; + for (const section of data.sections) { + rows.push({ rowKey: `section:${section.id}`, kind: "section", sectionId: section.id }); + for (const circuit of section.circuits) { + if (circuit.deviceRows.length === 0) { + rows.push({ + rowKey: `reserve:${circuit.id}`, + kind: "reserve", + sectionId: section.id, + circuit, + }); + continue; + } + if (circuit.deviceRows.length === 1) { + rows.push({ + rowKey: `compact:${circuit.id}`, + kind: "compact", + sectionId: section.id, + circuit, + device: circuit.deviceRows[0], + }); + continue; + } + rows.push({ + rowKey: `summary:${circuit.id}`, + kind: "summary", + sectionId: section.id, + circuit, + }); + for (const device of circuit.deviceRows) { + rows.push({ + rowKey: `device:${device.id}`, + kind: "device", + sectionId: section.id, + circuit, + device, + }); + } + } + rows.push({ rowKey: `placeholder:${section.id}`, kind: "placeholder", sectionId: section.id }); + } + return rows; + }, [data]); + + useEffect(() => { + if (!pendingFocus) { + return; + } + setSelectedCell(pendingFocus); + setEditingCell({ ...pendingFocus, draft: "" }); + setPendingFocus(null); + }, [pendingFocus]); + + const editableCells = useMemo(() => { + const cells: SelectedCell[] = []; + for (const row of gridRows) { + for (const column of columns) { + if (canEditCell(row, column.key)) { + cells.push({ rowKey: row.rowKey, cellKey: column.key }); + } + } + } + return cells; + }, [gridRows]); + + function findRow(rowKey: string) { + return gridRows.find((row) => row.rowKey === rowKey); + } + + function canEditCell(row: GridRow, cellKey: CellKey) { + if (row.kind === "section" || row.kind === "placeholder") { + return false; + } + if (cellKey === "rowTotalPower" || cellKey === "circuitTotalPower") { + return false; + } + if (row.kind === "summary" || row.kind === "reserve") { + return isCircuitField(cellKey); + } + if (row.kind === "device") { + return isDeviceField(cellKey); + } + if (row.kind === "compact") { + return isCircuitField(cellKey) || isDeviceField(cellKey); + } + return false; + } + + function getCellValue(row: GridRow, cellKey: CellKey): string | number | boolean | undefined { + const circuit = row.circuit; + const device = row.device; + if (!circuit) { + return undefined; + } + if (row.kind === "compact" && device) { + if (cellKey === "displayName") { + return device.displayName || device.name; + } + if (isDeviceField(cellKey)) { + return getDeviceFieldValue(device, cellKey); + } + } + if (row.kind === "device" && device) { + return getDeviceFieldValue(device, cellKey); + } + return getCircuitFieldValue(circuit, cellKey); + } + + function startEdit(cell: SelectedCell, initialDraft?: string) { + const row = findRow(cell.rowKey); + if (!row || !canEditCell(row, cell.cellKey)) { + return; + } + const value = initialDraft ?? String(getCellValue(row, cell.cellKey) ?? ""); + setEditingCell({ ...cell, draft: value === "-" ? "" : value }); + } + + function moveSelection(offset: number) { + if (!selectedCell) { + if (editableCells.length > 0) { + setSelectedCell(editableCells[0]); + } + return; + } + const index = editableCells.findIndex( + (cell) => cell.rowKey === selectedCell.rowKey && cell.cellKey === selectedCell.cellKey + ); + if (index < 0) { + return; + } + const nextIndex = Math.min(editableCells.length - 1, Math.max(0, index + offset)); + setSelectedCell(editableCells[nextIndex]); + } + + async function saveEditingCell(nextCell?: SelectedCell | null) { + if (!editingCell) { + return; + } + const row = findRow(editingCell.rowKey); + if (!row || !row.circuit) { + setEditingCell(null); + return; + } + try { + setIsSaving(true); + setError(null); + const key = editingCell.cellKey; + const draft = editingCell.draft; + + if ((row.kind === "summary" || row.kind === "reserve") && isCircuitField(key)) { + await patchCircuit(row.circuit.id, key, draft); + } else if (row.kind === "device" && row.device && isDeviceField(key)) { + await patchDeviceRow(row.device.id, key, draft); + } else if (row.kind === "compact") { + if (row.device && isDeviceField(key)) { + await patchDeviceRow(row.device.id, key, draft); + } else if (isCircuitField(key)) { + await patchCircuit(row.circuit.id, key, draft); + } + } + + setEditingCell(null); + await loadTree(); + if (nextCell) { + setSelectedCell(nextCell); + } + } catch (err) { + setError(err instanceof Error ? err.message : "Save failed."); + } finally { + setIsSaving(false); + } + } + + async function patchCircuit(circuitId: string, key: CellKey, draft: string) { + const payload: Record = {}; + if (key === "isReserve") { + payload.isReserve = draft.toLowerCase() === "true" || draft === "1" || draft.toLowerCase() === "yes"; + } else if (["protectionRatedCurrent", "cableLength", "voltage"].includes(key)) { + payload[key] = parseNumeric(key, draft); + } else if (key === "circuitTotalPower" || key === "rowTotalPower") { + return; + } else { + payload[key] = draft.trim() === "" ? undefined : draft; + } + await updateCircuitById(circuitId, payload); + } + + async function patchDeviceRow(rowId: string, key: CellKey, draft: string) { + const payload: Record = {}; + if (["quantity", "powerPerUnit", "simultaneityFactor", "cosPhi"].includes(key)) { + payload[key] = parseNumeric(key, draft); + } else { + payload[key] = draft.trim() === "" ? undefined : draft; + } + await updateCircuitDeviceRowById(rowId, payload); + } + + async function handleAddReserveCircuit(sectionId: string) { + try { + setError(null); + setIsSaving(true); + const section = data?.sections.find((entry) => entry.id === sectionId); + if (!section) { + return; + } + const next = await getNextCircuitIdentifier(sectionId); + const sortOrder = + section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10; + await createCircuit(projectId, circuitListId, { + sectionId, + equipmentIdentifier: next.nextIdentifier, + displayName: "Reserve", + sortOrder, + isReserve: true, + }); + await loadTree(); + setActiveSectionId(sectionId); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to add reserve circuit."); + } finally { + setIsSaving(false); + } + } + + async function handleAddManualDevice(circuit: CircuitTreeCircuitDto, sectionId: string) { + try { + setError(null); + setIsSaving(true); + const created = (await createCircuitDeviceRow(circuit.id, { + name: "Manual device", + displayName: "Manual device", + phaseType: "single_phase", + quantity: 1, + powerPerUnit: 0, + simultaneityFactor: 1, + cosPhi: 1, + })) as { id: string }; + await loadTree(); + setActiveSectionId(sectionId); + setPendingFocus({ + rowKey: circuit.deviceRows.length === 0 ? `compact:${circuit.id}` : `device:${created.id}`, + cellKey: "displayName", + }); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to add device row."); + } finally { + setIsSaving(false); + } + } + + async function handleDeleteDevice(rowId: string) { + if (!confirm("Delete this device row?")) { + return; + } + try { + setError(null); + setIsSaving(true); + await deleteCircuitDeviceRowById(rowId); + await loadTree(); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to delete device row."); + } finally { + setIsSaving(false); + } + } + + async function handleDeleteCircuit(circuitId: string) { + if (!confirm("Delete this circuit and all assigned device rows?")) { + return; + } + try { + setError(null); + setIsSaving(true); + await deleteCircuitById(circuitId); + await loadTree(); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to delete circuit."); + } finally { + setIsSaving(false); + } + } + + async function handleRenumberSection(sectionId: string) { + if (!confirm("Renumber this section? Only circuits in this section will change.")) { + return; + } + try { + setError(null); + setIsSaving(true); + await renumberCircuitSection(sectionId); + await loadTree(); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to renumber section."); + } finally { + setIsSaving(false); + } + } + + function handleKeyDown(event: KeyboardEvent) { + if (editingCell) { + return; + } + const isCtrlPlus = + event.ctrlKey && + (event.key === "+" || (event.shiftKey && event.key === "=") || event.code === "NumpadAdd"); + if (isCtrlPlus) { + event.preventDefault(); + const sectionId = activeSectionId ?? data?.sections[0]?.id; + if (sectionId) { + void handleAddReserveCircuit(sectionId); + } + return; + } + if (event.key === "ArrowRight") { + event.preventDefault(); + moveSelection(1); + } else if (event.key === "ArrowLeft") { + event.preventDefault(); + moveSelection(-1); + } else if (event.key === "ArrowDown") { + event.preventDefault(); + moveSelection(1); + } else if (event.key === "ArrowUp") { + event.preventDefault(); + moveSelection(-1); + } else if (event.key === "Enter" || event.key === "F2") { + event.preventDefault(); + if (selectedCell) { + startEdit(selectedCell); + } + } else if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) { + if (selectedCell) { + event.preventDefault(); + startEdit(selectedCell, event.key); + } + } + } + + if (isLoading) { + return
Loading circuit tree editor...
; + } + if (error) { + return
{error}
; + } + if (!data || data.sections.length === 0) { + return
No sections/circuits available.
; + } + + return ( +
+ {isSaving ?
Saving...
: null} +
+ + + + {columns.map((column) => ( + + ))} + + + + + {gridRows.map((row) => { + if (row.kind === "section") { + const section = data.sections.find((entry) => entry.id === row.sectionId)!; + return ( + + + + + ); + } + + if (row.kind === "placeholder") { + return ( + + + + + ); + } + + return ( + setActiveSectionId(row.sectionId)} + > + {columns.map((column) => { + const selected = + selectedCell?.rowKey === row.rowKey && selectedCell.cellKey === column.key; + const editing = + editingCell?.rowKey === row.rowKey && editingCell.cellKey === column.key; + const editable = canEditCell(row, column.key); + const value = getCellValue(row, column.key); + + return ( + + ); + })} + + + ); + })} + +
+ {column.label} + Actions
+ {section.displayName} + + + +
-frei-read-only placeholder
setSelectedCell({ rowKey: row.rowKey, cellKey: column.key })} + onDoubleClick={() => startEdit({ rowKey: row.rowKey, cellKey: column.key })} + > + {editing ? ( + + setEditingCell((current) => + current ? { ...current, draft: event.target.value } : current + ) + } + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + void saveEditingCell(selectedCell); + } else if (event.key === "Escape") { + event.preventDefault(); + setEditingCell(null); + } else if (event.key === "Tab") { + event.preventDefault(); + const idx = editableCells.findIndex( + (cell) => + cell.rowKey === editingCell.rowKey && cell.cellKey === editingCell.cellKey + ); + const next = + idx >= 0 + ? editableCells[ + event.shiftKey + ? Math.max(0, idx - 1) + : Math.min(editableCells.length - 1, idx + 1) + ] + : null; + void saveEditingCell(next); + } + }} + /> + ) : ( + formatValue(value) + )} + + {row.circuit && row.kind !== "device" ? ( + <> + + + + ) : null} + {row.device ? ( + + ) : null} +
+
+

TODO Phase: Ctrl+Plus currently adds reserve circuit at end of active section.

+
+ ); +} diff --git a/src/frontend/types.ts b/src/frontend/types.ts index e159499..dbcb12e 100644 --- a/src/frontend/types.ts +++ b/src/frontend/types.ts @@ -241,3 +241,47 @@ export interface CircuitTreeResponseDto { sections: CircuitTreeSectionDto[]; migrationReport?: CircuitTreeMigrationReportDto; } + +export interface CreateCircuitInputDto { + 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; + status?: string; + isReserve?: boolean; + remark?: string; +} + +export type UpdateCircuitInputDto = Partial; + +export interface CreateCircuitDeviceRowInputDto { + linkedProjectDeviceId?: string; + name: string; + displayName: string; + phaseType?: string; + connectionKind?: string; + costGroup?: string; + category?: string; + level?: string; + roomId?: string; + roomNumberSnapshot?: string; + roomNameSnapshot?: string; + quantity: number; + powerPerUnit: number; + simultaneityFactor: number; + cosPhi?: number; + remark?: string; + overriddenFields?: string; + sortOrder?: number; +} + +export type UpdateCircuitDeviceRowInputDto = Partial; diff --git a/src/frontend/utils/api.ts b/src/frontend/utils/api.ts index ce971c6..14ee6ae 100644 --- a/src/frontend/utils/api.ts +++ b/src/frontend/utils/api.ts @@ -14,6 +14,10 @@ import type { RoomDto, UpdateConsumerInput, CircuitTreeResponseDto, + CreateCircuitInputDto, + UpdateCircuitInputDto, + CreateCircuitDeviceRowInputDto, + UpdateCircuitDeviceRowInputDto, } from "../types"; async function request(url: string, init?: RequestInit): Promise { @@ -82,6 +86,58 @@ export function getCircuitTree(projectId: string, circuitListId: string) { return request(`/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 updateCircuitById(circuitId: string, input: UpdateCircuitInputDto) { + return request(`/api/circuits/${circuitId}`, { + method: "PATCH", + body: JSON.stringify(input), + }); +} + +export function deleteCircuitById(circuitId: string) { + return request(`/api/circuits/${circuitId}`, { + method: "DELETE", + }); +} + +export function getNextCircuitIdentifier(sectionId: string) { + return request<{ sectionId: string; nextIdentifier: string }>( + `/api/circuit-sections/${sectionId}/next-identifier` + ); +} + +export function createCircuitDeviceRow(circuitId: string, input: CreateCircuitDeviceRowInputDto) { + return request(`/api/circuits/${circuitId}/device-rows`, { + method: "POST", + body: JSON.stringify(input), + }); +} + +export function updateCircuitDeviceRowById(rowId: string, input: UpdateCircuitDeviceRowInputDto) { + return request(`/api/circuit-device-rows/${rowId}`, { + method: "PATCH", + body: JSON.stringify(input), + }); +} + +export function deleteCircuitDeviceRowById(rowId: string) { + return request(`/api/circuit-device-rows/${rowId}`, { + method: "DELETE", + }); +} + +export function renumberCircuitSection(sectionId: string) { + return request(`/api/circuit-sections/${sectionId}/renumber`, { + method: "POST", + }); +} + export function listFloors(projectId: string) { return request(`/api/projects/${projectId}/floors`); }