New usable editor

This commit is contained in:
2026-05-03 23:00:46 +02:00
parent d1ce485572
commit ad498f2bb5
6 changed files with 1001 additions and 1 deletions
+118
View File
@@ -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;
}
@@ -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 (
<main className="container py-4">
<div className="d-flex justify-content-between align-items-center mb-3">
<div>
<h1 className="h4 mb-1">Circuit Tree Editor</h1>
<p className="text-secondary mb-0">Basic editing for the circuit-first model.</p>
</div>
<div className="d-flex gap-2">
<Link
href={`/projects/${params.projectId}/circuit-lists/${params.circuitListId}/tree`}
className="btn btn-outline-secondary btn-sm"
>
Read-only tree
</Link>
<Link href={`/projects/${params.projectId}/circuit-lists`} className="btn btn-outline-secondary btn-sm">
Legacy editor
</Link>
</div>
</div>
<CircuitTreeEditor projectId={params.projectId} circuitListId={params.circuitListId} />
</main>
);
}
@@ -18,6 +18,12 @@ export default function CircuitTreePreviewPage() {
Read-only preview of section blocks, circuits and device rows.
</p>
</div>
<Link
href={`/projects/${projectId}/circuit-lists/${circuitListId}/tree-edit`}
className="btn btn-outline-primary btn-sm"
>
Open editor
</Link>
<Link
href={`/projects/${projectId}/circuit-lists`}
className="btn btn-outline-secondary btn-sm"
@@ -30,4 +36,3 @@ export default function CircuitTreePreviewPage() {
</main>
);
}
@@ -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<CircuitTreeResponseDto | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedCell, setSelectedCell] = useState<SelectedCell | null>(null);
const [editingCell, setEditingCell] = useState<EditingCell | null>(null);
const [activeSectionId, setActiveSectionId] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false);
const [pendingFocus, setPendingFocus] = useState<SelectedCell | null>(null);
const containerRef = useRef<HTMLDivElement | null>(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<string, unknown> = {};
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<string, unknown> = {};
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<HTMLDivElement>) {
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 <div className="notice info">Loading circuit tree editor...</div>;
}
if (error) {
return <div className="notice error">{error}</div>;
}
if (!data || data.sections.length === 0) {
return <div className="notice muted">No sections/circuits available.</div>;
}
return (
<div className="tree-editor-shell">
{isSaving ? <div className="notice info">Saving...</div> : null}
<div className="tree-grid-wrap" ref={containerRef} tabIndex={0} onKeyDown={handleKeyDown}>
<table className="tree-grid">
<thead>
<tr>
{columns.map((column) => (
<th key={column.key} className={column.numeric ? "num" : ""}>
{column.label}
</th>
))}
<th>Actions</th>
</tr>
</thead>
<tbody>
{gridRows.map((row) => {
if (row.kind === "section") {
const section = data.sections.find((entry) => entry.id === row.sectionId)!;
return (
<tr key={row.rowKey} className="section-row">
<td colSpan={columns.length}>
<strong>{section.displayName}</strong>
</td>
<td className="action-cell">
<button type="button" onClick={() => void handleAddReserveCircuit(section.id)}>
Add reserve circuit
</button>
<button type="button" onClick={() => void handleRenumberSection(section.id)}>
Renumber section
</button>
</td>
</tr>
);
}
if (row.kind === "placeholder") {
return (
<tr key={row.rowKey} className="placeholder-row">
<td>-frei-</td>
<td colSpan={columns.length}>read-only placeholder</td>
</tr>
);
}
return (
<tr
key={row.rowKey}
className={row.kind === "summary" ? "summary-row" : row.kind === "device" ? "device-row" : row.kind === "reserve" ? "reserve-row" : ""}
onClick={() => 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 (
<td
key={column.key}
className={`${column.numeric ? "num" : ""} ${selected ? "cell-selected" : ""} ${editable ? "cell-editable" : ""}`}
onClick={() => setSelectedCell({ rowKey: row.rowKey, cellKey: column.key })}
onDoubleClick={() => startEdit({ rowKey: row.rowKey, cellKey: column.key })}
>
{editing ? (
<input
autoFocus
value={editingCell.draft}
onChange={(event) =>
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)
)}
</td>
);
})}
<td className="action-cell">
{row.circuit && row.kind !== "device" ? (
<>
<button type="button" onClick={() => void handleAddManualDevice(row.circuit!, row.sectionId)}>
Add manual device
</button>
<button type="button" onClick={() => void handleDeleteCircuit(row.circuit!.id)}>
Delete circuit
</button>
</>
) : null}
{row.device ? (
<button type="button" onClick={() => void handleDeleteDevice(row.device!.id)}>
Delete device
</button>
) : null}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<p className="todo-hint">TODO Phase: Ctrl+Plus currently adds reserve circuit at end of active section.</p>
</div>
);
}
+44
View File
@@ -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<CreateCircuitInputDto>;
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<CreateCircuitDeviceRowInputDto>;
+56
View File
@@ -14,6 +14,10 @@ import type {
RoomDto,
UpdateConsumerInput,
CircuitTreeResponseDto,
CreateCircuitInputDto,
UpdateCircuitInputDto,
CreateCircuitDeviceRowInputDto,
UpdateCircuitDeviceRowInputDto,
} from "../types";
async function request<T>(url: string, init?: RequestInit): Promise<T> {
@@ -82,6 +86,58 @@ 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 updateCircuitById(circuitId: string, input: UpdateCircuitInputDto) {
return request(`/api/circuits/${circuitId}`, {
method: "PATCH",
body: JSON.stringify(input),
});
}
export function deleteCircuitById(circuitId: string) {
return request<void>(`/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<void>(`/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<FloorDto[]>(`/api/projects/${projectId}/floors`);
}