From efbb81c13d95676f21ee6c2036525218bfb80f0a Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Mon, 4 May 2026 19:00:21 +0200 Subject: [PATCH] Project devices sidebar working --- src/app/globals.css | 81 ++++++ .../components/circuit-tree-editor.tsx | 240 +++++++++++++++++- 2 files changed, 311 insertions(+), 10 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 6798083..589c389 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -52,6 +52,81 @@ body { background: #fff; } +.tree-editor-layout { + display: grid; + grid-template-columns: 320px 1fr; + gap: 0.75rem; + min-height: 560px; +} + +.project-device-sidebar { + border: 1px solid #d9dee8; + background: #fff; + padding: 0.6rem; + display: flex; + flex-direction: column; + gap: 0.55rem; +} + +.project-device-sidebar h3 { + margin: 0; + font-size: 1rem; +} + +.project-device-sidebar input, +.project-device-sidebar select { + width: 100%; + border: 1px solid #cfd7e5; + border-radius: 4px; + padding: 0.25rem 0.35rem; +} + +.project-device-list { + max-height: 360px; + overflow: auto; + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.project-device-item { + border: 1px solid #d5ddec; + background: #f8faff; + text-align: left; + padding: 0.4rem; + border-radius: 4px; + display: flex; + flex-direction: column; + gap: 0.1rem; + font-size: 0.8rem; +} + +.project-device-item.selected { + border-color: #4c7dd9; + background: #edf3ff; +} + +.sidebar-actions { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.sidebar-actions label { + display: flex; + flex-direction: column; + gap: 0.2rem; + font-size: 0.82rem; +} + +.sidebar-actions button { + border: 1px solid #c4cddc; + background: #fff; + border-radius: 4px; + padding: 0.3rem 0.45rem; + font-size: 0.82rem; +} + .tree-grid { width: max-content; min-width: 100%; @@ -157,3 +232,9 @@ body { font-size: 0.8rem; margin: 0; } + +@media (max-width: 1200px) { + .tree-editor-layout { + grid-template-columns: 1fr; + } +} diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index 801983d..429933f 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -8,11 +8,17 @@ import { deleteCircuitDeviceRowById, getCircuitTree, getNextCircuitIdentifier, + listProjectDevices, renumberCircuitSection, updateCircuitById, updateCircuitDeviceRowById, } from "../utils/api"; -import type { CircuitTreeCircuitDto, CircuitTreeDeviceRowDto, CircuitTreeResponseDto } from "../types"; +import type { + CircuitTreeCircuitDto, + CircuitTreeDeviceRowDto, + CircuitTreeResponseDto, + ProjectDeviceDto, +} from "../types"; type CellKey = | "equipmentIdentifier" @@ -250,6 +256,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const [editingCell, setEditingCell] = useState(null); const [activeSectionId, setActiveSectionId] = useState(null); const [isSaving, setIsSaving] = useState(false); + const [projectDevices, setProjectDevices] = useState([]); + const [projectDeviceSearch, setProjectDeviceSearch] = useState(""); + const [selectedProjectDeviceId, setSelectedProjectDeviceId] = useState(null); + const [targetSectionId, setTargetSectionId] = useState(null); + const [targetCircuitId, setTargetCircuitId] = useState(null); const [pendingFocus, setPendingFocus] = useState(null); const pendingSelectionAfterReload = useRef(null); const containerRef = useRef(null); @@ -278,6 +289,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str void loadTree({ showLoading: true }); }, [projectId, circuitListId]); + useEffect(() => { + async function loadProjectDeviceList() { + try { + const list = await listProjectDevices(projectId); + setProjectDevices(list); + } catch (err) { + setError(normalizeUiError(err)); + } + } + void loadProjectDeviceList(); + }, [projectId]); + const visibleRows = useMemo(() => { if (!data) { return [] as VisibleGridRow[]; @@ -309,6 +332,30 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str return rows; }, [data]); + const searchableProjectDevices = useMemo(() => { + const term = projectDeviceSearch.trim().toLowerCase(); + if (!term) { + return projectDevices; + } + return projectDevices.filter((device) => { + const haystack = `${device.name} ${device.displayName}`.toLowerCase(); + return haystack.includes(term); + }); + }, [projectDeviceSearch, projectDevices]); + + const circuitOptions = useMemo(() => { + if (!data) { + return [] as Array<{ id: string; label: string; sectionId: string }>; + } + return data.sections.flatMap((section) => + section.circuits.map((circuit) => ({ + id: circuit.id, + sectionId: section.id, + label: `${circuit.equipmentIdentifier} - ${circuit.displayName || "Circuit"}`, + })) + ); + }, [data]); + function makeRow( rowType: RowType, sectionId: string, @@ -754,6 +801,112 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str } } + function resolvePhaseType(device: ProjectDeviceDto): string { + if (device.phaseCount === 3) { + return "three_phase"; + } + return "single_phase"; + } + + function resolveSelectedProjectDevice() { + if (!selectedProjectDeviceId) { + return null; + } + return projectDevices.find((device) => device.id === selectedProjectDeviceId) ?? null; + } + + async function handleAddProjectDeviceAsNewCircuit() { + const device = resolveSelectedProjectDevice(); + if (!device) { + setError("Please select a project device."); + return; + } + if (!targetSectionId) { + setError("Please select a target section."); + return; + } + try { + setError(null); + setIsSaving(true); + const section = data?.sections.find((entry) => entry.id === targetSectionId); + if (!section) { + throw new Error("Invalid target section."); + } + const next = await getNextCircuitIdentifier(targetSectionId); + const sortOrder = + section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10; + const createdCircuit = (await createCircuit(projectId, circuitListId, { + sectionId: targetSectionId, + equipmentIdentifier: next.nextIdentifier, + displayName: device.displayName || device.name, + sortOrder, + isReserve: false, + })) as CircuitTreeCircuitDto; + + const createdRow = (await createCircuitDeviceRow(createdCircuit.id, { + linkedProjectDeviceId: device.id, + name: device.name, + displayName: device.displayName, + phaseType: resolvePhaseType(device), + quantity: device.quantity, + powerPerUnit: device.installedPowerPerUnitKw, + simultaneityFactor: device.demandFactor, + cosPhi: device.powerFactor ?? undefined, + category: device.category ?? undefined, + })) as { id: string }; + + await loadTree({ showLoading: false }); + setActiveSectionId(targetSectionId); + setTargetCircuitId(createdCircuit.id); + setPendingFocus({ rowKey: `device:${createdRow.id}`, cellKey: "displayName" }); + } catch (err) { + setError(normalizeUiError(err)); + } finally { + setIsSaving(false); + } + } + + async function handleAddProjectDeviceToCircuit() { + const device = resolveSelectedProjectDevice(); + if (!device) { + setError("Please select a project device."); + return; + } + let circuitId = targetCircuitId; + if (!circuitId && selectedCell) { + const row = findRow(selectedCell.rowKey); + if (row?.circuit?.id) { + circuitId = row.circuit.id; + } + } + if (!circuitId) { + setError("Please select a target circuit."); + return; + } + + try { + setError(null); + setIsSaving(true); + const createdRow = (await createCircuitDeviceRow(circuitId, { + linkedProjectDeviceId: device.id, + name: device.name, + displayName: device.displayName, + phaseType: resolvePhaseType(device), + quantity: device.quantity, + powerPerUnit: device.installedPowerPerUnitKw, + simultaneityFactor: device.demandFactor, + cosPhi: device.powerFactor ?? undefined, + category: device.category ?? undefined, + })) as { id: string }; + await loadTree({ showLoading: false }); + setPendingFocus({ rowKey: `device:${createdRow.id}`, cellKey: "displayName" }); + } catch (err) { + setError(normalizeUiError(err)); + } finally { + setIsSaving(false); + } + } + async function handleDeleteDevice(rowId: string) { if (!confirm("Delete this device row?")) { return; @@ -890,14 +1043,80 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str return (
{isSaving ?
Saving...
: null} -
- +
+ +
+
{columns.map((column) => ( @@ -1033,7 +1252,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str ); })} -
+ +

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