"use client"; import Link from "next/link"; import { useParams } from "next/navigation"; import { FormEvent, useEffect, useMemo, useState } from "react"; import { copyGlobalDeviceToProject, copyProjectDeviceToGlobal, createDistributionBoard, createFloor, createProjectDevice, createRoom, deleteProjectDevice, disconnectProjectDeviceRows, getProjectDeviceSyncPreview, listCircuitLists, listDistributionBoards, listFloors, listGlobalDevices, listProjectDevices, listProjects, listRooms, reconnectProjectDeviceRows, restoreProjectDeviceRows, synchronizeProjectDeviceRows, updateProjectDevice, updateProjectSettings, } from "../../../frontend/utils/api"; import type { CircuitListDto, CreateProjectDeviceInput, DistributionBoardDto, FloorDto, GlobalDeviceDto, ProjectDeviceDto, ProjectDeviceSyncPreviewDto, ProjectDeviceSyncRestoreRowDto, ProjectDto, RoomDto, } from "../../../frontend/types"; import { projectDeviceSyncFields, type ProjectDeviceSyncField, } from "../../../shared/constants/project-device-sync-fields"; const projectDeviceSyncFieldLabels: Record = { name: "Technischer Name", displayName: "Anzeigename", phaseType: "Phasenart", connectionKind: "Anschlussart", costGroup: "Kostengruppe", category: "Kategorie", quantity: "Anzahl", powerPerUnit: "Leistung je Stück", simultaneityFactor: "Gleichzeitigkeitsfaktor", cosPhi: "cos Phi", remark: "Bemerkung", }; type ProjectDeviceUndoState = | { kind: "synchronize"; projectDeviceId: string; rows: ProjectDeviceSyncRestoreRowDto[] } | { kind: "disconnect"; projectDeviceId: string; rowIds: string[] }; const emptyProjectDevice: CreateProjectDeviceInput = { name: "", displayName: "", phaseType: "single_phase", connectionKind: "", costGroup: "", category: "", quantity: 1, powerPerUnit: 0.1, simultaneityFactor: 1, cosPhi: 1, remark: "", }; function toOptionalNumber(value: string) { const trimmed = value.trim(); if (!trimmed) { return undefined; } return Number(trimmed); } export default function ProjectDetailPage() { const params = useParams<{ projectId: string }>(); const [projectId, setProjectId] = useState(""); const [project, setProject] = useState(null); const [boards, setBoards] = useState([]); const [circuitLists, setCircuitLists] = useState([]); const [floors, setFloors] = useState([]); const [rooms, setRooms] = useState([]); const [projectDevices, setProjectDevices] = useState([]); const [globalDevices, setGlobalDevices] = useState([]); const [boardName, setBoardName] = useState(""); const [floorName, setFloorName] = useState(""); const [roomNumber, setRoomNumber] = useState(""); const [roomName, setRoomName] = useState(""); const [roomFloorId, setRoomFloorId] = useState(""); const [singlePhaseVoltageV, setSinglePhaseVoltageV] = useState("230"); const [threePhaseVoltageV, setThreePhaseVoltageV] = useState("400"); const [projectDeviceForm, setProjectDeviceForm] = useState>({ name: "", displayName: "", phaseType: "single_phase", connectionKind: "", costGroup: "", category: "", quantity: "1", powerPerUnit: "0.1", simultaneityFactor: "1", cosPhi: "1", remark: "", }); const [selectedGlobalDeviceId, setSelectedGlobalDeviceId] = useState(""); const [syncPreview, setSyncPreview] = useState(null); const [selectedSyncRowIds, setSelectedSyncRowIds] = useState([]); const [selectedSyncFields, setSelectedSyncFields] = useState([]); const [projectDeviceUndo, setProjectDeviceUndo] = useState(null); const [error, setError] = useState(null); const [isSaving, setIsSaving] = useState(false); useEffect(() => { setProjectId(params.projectId); }, [params.projectId]); useEffect(() => { if (!projectId) { return; } Promise.all([ listProjects(), listDistributionBoards(projectId), listCircuitLists(projectId), listFloors(projectId), listRooms(projectId), listProjectDevices(projectId), listGlobalDevices(), ]) .then(([ projects, distributionBoards, loadedCircuitLists, loadedFloors, loadedRooms, loadedProjectDevices, loadedGlobalDevices, ]) => { const currentProject = projects.find((item) => item.id === projectId) ?? null; setProject(currentProject); if (currentProject) { setSinglePhaseVoltageV(String(currentProject.singlePhaseVoltageV)); setThreePhaseVoltageV(String(currentProject.threePhaseVoltageV)); } setBoards(distributionBoards); setCircuitLists(loadedCircuitLists); setFloors(loadedFloors); setRooms(loadedRooms); setProjectDevices(loadedProjectDevices); setGlobalDevices(loadedGlobalDevices); setError(null); }) .catch((err: unknown) => setError(err instanceof Error ? err.message : "Projektdaten konnten nicht geladen werden.") ); }, [projectId]); const boardCount = useMemo(() => boards.length, [boards.length]); const floorCount = useMemo(() => floors.length, [floors.length]); const roomCount = useMemo(() => rooms.length, [rooms.length]); const projectDeviceCount = useMemo(() => projectDevices.length, [projectDevices.length]); const floorById = useMemo(() => new Map(floors.map((item) => [item.id, item])), [floors]); const circuitListByBoardId = useMemo( () => new Map(circuitLists.map((circuitList) => [circuitList.distributionBoardId, circuitList])), [circuitLists] ); async function handleCreateBoard(event: FormEvent) { event.preventDefault(); if (!projectId || !boardName.trim()) { return; } setIsSaving(true); setError(null); try { const created = await createDistributionBoard(projectId, boardName.trim()); setBoards((current) => [...current, created]); setCircuitLists(await listCircuitLists(projectId)); setBoardName(""); } catch (err) { setError(err instanceof Error ? err.message : "Verteilung konnte nicht erstellt werden."); } finally { setIsSaving(false); } } async function handleCreateFloor(event: FormEvent) { event.preventDefault(); if (!projectId || !floorName.trim()) { return; } setIsSaving(true); setError(null); try { const created = await createFloor(projectId, { name: floorName.trim() }); setFloors((current) => [...current, created]); setFloorName(""); } catch (err) { setError(err instanceof Error ? err.message : "Etage konnte nicht erstellt werden."); } finally { setIsSaving(false); } } async function handleCreateRoom(event: FormEvent) { event.preventDefault(); if (!projectId || !roomNumber.trim() || !roomName.trim()) { return; } setIsSaving(true); setError(null); try { const created = await createRoom(projectId, { floorId: roomFloorId || undefined, roomNumber: roomNumber.trim(), roomName: roomName.trim(), }); setRooms((current) => [...current, created]); setRoomNumber(""); setRoomName(""); setRoomFloorId(""); } catch (err) { setError(err instanceof Error ? err.message : "Raum konnte nicht erstellt werden."); } finally { setIsSaving(false); } } async function handleSaveProjectSettings() { if (!projectId) { return; } setIsSaving(true); setError(null); try { const updated = await updateProjectSettings(projectId, { singlePhaseVoltageV: Number(singlePhaseVoltageV), threePhaseVoltageV: Number(threePhaseVoltageV), }); setProject(updated); } catch (err) { setError(err instanceof Error ? err.message : "Projekteigenschaften konnten nicht gespeichert werden."); } finally { setIsSaving(false); } } async function handleCreateProjectDevice(event: FormEvent) { event.preventDefault(); if (!projectId || !projectDeviceForm.name.trim()) { return; } const payload: CreateProjectDeviceInput = { name: projectDeviceForm.name.trim(), displayName: projectDeviceForm.displayName.trim() || projectDeviceForm.name.trim(), phaseType: projectDeviceForm.phaseType === "three_phase" ? "three_phase" : "single_phase", connectionKind: projectDeviceForm.connectionKind.trim() || undefined, costGroup: projectDeviceForm.costGroup.trim() || undefined, category: projectDeviceForm.category.trim() || undefined, quantity: Number(projectDeviceForm.quantity), powerPerUnit: Number(projectDeviceForm.powerPerUnit), simultaneityFactor: Number(projectDeviceForm.simultaneityFactor), cosPhi: toOptionalNumber(projectDeviceForm.cosPhi), remark: projectDeviceForm.remark.trim() || undefined, }; setIsSaving(true); setError(null); try { const created = await createProjectDevice(projectId, payload); setProjectDevices((current) => [...current, created]); setProjectDeviceForm({ name: emptyProjectDevice.name, displayName: emptyProjectDevice.displayName, phaseType: emptyProjectDevice.phaseType, connectionKind: emptyProjectDevice.connectionKind ?? "", costGroup: emptyProjectDevice.costGroup ?? "", category: emptyProjectDevice.category ?? "", quantity: String(emptyProjectDevice.quantity), powerPerUnit: String(emptyProjectDevice.powerPerUnit), simultaneityFactor: String(emptyProjectDevice.simultaneityFactor), cosPhi: String(emptyProjectDevice.cosPhi ?? ""), remark: emptyProjectDevice.remark ?? "", }); } catch (err) { setError(err instanceof Error ? err.message : "Projektgerät konnte nicht erstellt werden."); } finally { setIsSaving(false); } } async function handleDeleteProjectDevice(projectDeviceId: string) { if (!projectId) { return; } setIsSaving(true); setError(null); try { await deleteProjectDevice(projectId, projectDeviceId); setProjectDevices((current) => current.filter((item) => item.id !== projectDeviceId)); } catch (err) { setError(err instanceof Error ? err.message : "Projektgerät konnte nicht gelöscht werden."); } finally { setIsSaving(false); } } async function handleQuickUpdateProjectDevice( device: ProjectDeviceDto, key: "name" | "displayName" | "category", value: string ) { if (!projectId) { return; } const payload: CreateProjectDeviceInput = { name: key === "name" ? value : device.name, displayName: key === "displayName" ? value : device.displayName, phaseType: device.phaseType, connectionKind: device.connectionKind ?? undefined, costGroup: device.costGroup ?? undefined, category: key === "category" ? value : device.category ?? undefined, quantity: device.quantity, powerPerUnit: device.powerPerUnit, simultaneityFactor: device.simultaneityFactor, cosPhi: device.cosPhi ?? undefined, remark: device.remark ?? undefined, voltageV: device.voltageV ?? undefined, }; try { const updated = await updateProjectDevice(projectId, device.id, payload); setProjectDevices((current) => current.map((item) => (item.id === device.id ? updated : item))); await openProjectDeviceSyncPreview(updated.id); } catch (err) { setError(err instanceof Error ? err.message : "Projektgerät konnte nicht aktualisiert werden."); } } async function openProjectDeviceSyncPreview(projectDeviceId: string) { if (!projectId) { return; } try { setProjectDeviceUndo(null); const preview = await getProjectDeviceSyncPreview(projectId, projectDeviceId); setSyncPreview(preview); setSelectedSyncRowIds(preview.rows.filter((row) => row.differences.length > 0).map((row) => row.rowId)); const differentFields = new Set( preview.rows.flatMap((row) => row.differences.map((difference) => difference.field)) ); setSelectedSyncFields( projectDeviceSyncFields.filter((field) => field !== "displayName" && differentFields.has(field)) ); } catch (err) { setError(err instanceof Error ? err.message : "Verknüpfungen konnten nicht geladen werden."); } } function toggleSyncRow(rowId: string) { setSelectedSyncRowIds((current) => current.includes(rowId) ? current.filter((id) => id !== rowId) : [...current, rowId] ); } function toggleSyncField(field: ProjectDeviceSyncField) { setSelectedSyncFields((current) => current.includes(field) ? current.filter((entry) => entry !== field) : [...current, field] ); } async function handleSynchronizeProjectDeviceRows() { if (!projectId || !syncPreview || selectedSyncRowIds.length === 0 || selectedSyncFields.length === 0) { return; } setIsSaving(true); setError(null); try { const result = await synchronizeProjectDeviceRows( projectId, syncPreview.projectDevice.id, selectedSyncRowIds, selectedSyncFields ); setSyncPreview(result.preview); setSelectedSyncRowIds( result.preview.rows.filter((row) => row.differences.length > 0).map((row) => row.rowId) ); setProjectDeviceUndo({ kind: "synchronize", projectDeviceId: syncPreview.projectDevice.id, rows: result.undo.rows, }); } catch (err) { setError(err instanceof Error ? err.message : "Gerätezeilen konnten nicht synchronisiert werden."); } finally { setIsSaving(false); } } async function handleDisconnectProjectDeviceRows() { if (!projectId || !syncPreview || selectedSyncRowIds.length === 0) { return; } setIsSaving(true); setError(null); try { const result = await disconnectProjectDeviceRows( projectId, syncPreview.projectDevice.id, selectedSyncRowIds ); await openProjectDeviceSyncPreview(syncPreview.projectDevice.id); setProjectDeviceUndo({ kind: "disconnect", projectDeviceId: syncPreview.projectDevice.id, rowIds: result.undo.rowIds, }); } catch (err) { setError(err instanceof Error ? err.message : "Verknüpfungen konnten nicht getrennt werden."); } finally { setIsSaving(false); } } async function handleUndoProjectDeviceOperation() { if (!projectId || !projectDeviceUndo) { return; } setIsSaving(true); setError(null); try { const preview = projectDeviceUndo.kind === "synchronize" ? await restoreProjectDeviceRows( projectId, projectDeviceUndo.projectDeviceId, projectDeviceUndo.rows ) : await reconnectProjectDeviceRows( projectId, projectDeviceUndo.projectDeviceId, projectDeviceUndo.rowIds ); setSyncPreview(preview); setSelectedSyncRowIds(preview.rows.filter((row) => row.differences.length > 0).map((row) => row.rowId)); setProjectDeviceUndo(null); } catch (err) { setError(err instanceof Error ? err.message : "Die letzte Aktion konnte nicht rückgängig gemacht werden."); } finally { setIsSaving(false); } } async function handleCopyGlobalToProject() { if (!projectId || !selectedGlobalDeviceId) { return; } setIsSaving(true); setError(null); try { const created = await copyGlobalDeviceToProject(projectId, selectedGlobalDeviceId); setProjectDevices((current) => [...current, created]); } catch (err) { setError(err instanceof Error ? err.message : "Globales Gerät konnte nicht ins Projekt kopiert werden."); } finally { setIsSaving(false); } } async function handleCopyProjectToGlobal(projectDeviceId: string) { if (!projectId) { return; } setIsSaving(true); setError(null); try { const created = await copyProjectDeviceToGlobal(projectId, projectDeviceId); setGlobalDevices((current) => [...current, created]); } catch (err) { setError(err instanceof Error ? err.message : "Projektgerät konnte nicht global kopiert werden."); } finally { setIsSaving(false); } } return (

{project?.name ?? "Projekt"}

Verteilerübersicht und Einstieg in die Stromkreislisten

Zur Projektseite
{error ?
{error}
: null}
Projekteigenschaften
setSinglePhaseVoltageV(event.target.value)} />
setThreePhaseVoltageV(event.target.value)} />
Neue Verteilung
setBoardName(event.target.value)} />
Alle Verteilungen {boardCount}
{boards.map((board) => { const circuitList = circuitListByBoardId.get(board.id); return ( ); })} {!boards.length ? ( ) : null}
Verteilung Aktionen
{board.name}
{circuitList ? ( Stromkreisliste öffnen ) : ( Keine Stromkreisliste vorhanden )} Legacy
Noch keine Verteilungen vorhanden.
Etagen {floorCount}
setFloorName(event.target.value)} />
    {floors.map((floor) => (
  • {floor.name}
  • ))} {!floors.length ? (
  • Noch keine Etagen vorhanden.
  • ) : null}
Räume {roomCount}
setRoomNumber(event.target.value)} />
setRoomName(event.target.value)} />
{rooms.map((room) => ( ))} {!rooms.length ? ( ) : null}
Raumnummer Raumname Etage
{room.roomNumber} {room.roomName} {room.floorId ? floorById.get(room.floorId)?.name ?? "-" : "-"}
Noch keine Räume vorhanden.
Projektgeräte / Verbraucher {projectDeviceCount}
setProjectDeviceForm((current) => ({ ...current, name: event.target.value })) } />
setProjectDeviceForm((current) => ({ ...current, displayName: event.target.value })) } />
setProjectDeviceForm((current) => ({ ...current, category: event.target.value })) } />
setProjectDeviceForm((current) => ({ ...current, connectionKind: event.target.value })) } />
setProjectDeviceForm((current) => ({ ...current, costGroup: event.target.value })) } />
setProjectDeviceForm((current) => ({ ...current, quantity: event.target.value })) } />
setProjectDeviceForm((current) => ({ ...current, powerPerUnit: event.target.value, })) } />
setProjectDeviceForm((current) => ({ ...current, simultaneityFactor: event.target.value })) } />
setProjectDeviceForm((current) => ({ ...current, cosPhi: event.target.value })) } />
setProjectDeviceForm((current) => ({ ...current, remark: event.target.value })) } />
{projectDevices.map((device) => ( ))} {!projectDevices.length ? ( ) : null}
Interner Name Anzeigename Phasenart Kategorie Kostengruppe Anzahl Leistung je Stück [kW] GZF Gesamtleistung [kW] Aktionen
event.target.value !== device.name ? handleQuickUpdateProjectDevice(device, "name", event.target.value) : undefined } /> event.target.value !== device.displayName ? handleQuickUpdateProjectDevice(device, "displayName", event.target.value) : undefined } /> {device.phaseType === "three_phase" ? "3-ph" : "1-ph"} event.target.value !== (device.category ?? "") ? handleQuickUpdateProjectDevice(device, "category", event.target.value) : undefined } /> {device.costGroup ?? "-"} {device.quantity} {device.powerPerUnit} {device.simultaneityFactor} {device.totalPower}
Noch keine Projektgeräte vorhanden.
{syncPreview ? (

Verknüpfte Zeilen: {syncPreview.projectDevice.displayName}

Nur ausgewählte Felder und Zeilen werden übernommen. Der Anzeigename bleibt standardmäßig lokal.

{projectDeviceSyncFields.map((field) => ( ))}
{syncPreview.rows.map((row) => ( ))} {syncPreview.rows.length === 0 ? ( ) : null}
Auswahl Verteilung / Stromkreis Gerätezeile Abweichungen
toggleSyncRow(row.rowId)} /> {row.distributionBoardName}
{row.equipmentIdentifier} {row.circuitDisplayName ? ` – ${row.circuitDisplayName}` : ""}
{row.rowDisplayName}
{row.differences.map((difference) => ( {projectDeviceSyncFieldLabels[difference.field]}: {String(difference.currentValue ?? "–")} →{" "} {String(difference.sourceValue ?? "–")} ))} {row.differences.length === 0 ? ( Aktuell ) : null}
Keine verknüpften Circuit-First-Gerätezeilen vorhanden.
{projectDeviceUndo ? ( ) : null}
) : null}
3 parallele Stromkreislisten öffnen
); }