"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, exportProjectTransfer, getProjectDeviceSyncPreview, listCircuitLists, listDistributionBoards, listFloors, listGlobalDevices, listProjectDevices, listProjects, listRooms, importProjectTransfer, synchronizeProjectDeviceRows, updateDistributionBoard, updateProjectDevice, updateProjectSettings, } from "../../../frontend/utils/api"; import type { CircuitListDto, CreateProjectDeviceInput, DistributionBoardDto, FloorDto, GlobalDeviceDto, ProjectDeviceDto, ProjectDeviceSyncPreviewDto, ProjectDto, RoomDto, } from "../../../frontend/types"; import { projectDeviceSyncFields, type ProjectDeviceSyncField, } from "../../../shared/constants/project-device-sync-fields"; import { distributionBoardSupplyTypeLabels, type DistributionBoardSupplyType, } from "../../../shared/constants/distribution-board"; import { ProjectVersionHistory } from "../../../frontend/components/project-version-history"; import { ProjectSettingsModal, type ProjectSettingsInput, } from "../../../frontend/components/project-settings-modal"; import { FormModal } from "../../../frontend/components/form-modal"; import { ProjectDeviceModal } from "../../../frontend/components/project-device-modal"; 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", }; 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 [boardFloorId, setBoardFloorId] = useState(""); const [boardSupplyType, setBoardSupplyType] = useState("AV"); const [editingBoard, setEditingBoard] = useState(null); const [editingBoardFloorId, setEditingBoardFloorId] = useState(""); const [editingBoardSupplyType, setEditingBoardSupplyType] = useState("AV"); const [ editingBoardSimultaneityFactor, setEditingBoardSimultaneityFactor, ] = useState("1"); const [floorName, setFloorName] = useState(""); const [roomNumber, setRoomNumber] = useState(""); const [roomName, setRoomName] = useState(""); const [roomFloorId, setRoomFloorId] = useState(""); const [isProjectSettingsOpen, setIsProjectSettingsOpen] = useState(false); const [structureModal, setStructureModal] = useState< "board" | "floor" | "room" | null >(null); const [isProjectDeviceModalOpen, setIsProjectDeviceModalOpen] = useState(false); const [editingProjectDevice, setEditingProjectDevice] = useState(null); const [projectDeviceQuery, setProjectDeviceQuery] = useState(""); const [syncPreview, setSyncPreview] = useState(null); const [selectedSyncRowIds, setSelectedSyncRowIds] = useState([]); const [selectedSyncFields, setSelectedSyncFields] = useState([]); 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); 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 visibleProjectDevices = useMemo(() => { const query = projectDeviceQuery.trim().toLocaleLowerCase("de"); if (!query) { return projectDevices; } return projectDevices.filter((device) => [ device.name, device.displayName, device.category, device.connectionKind, device.costGroup, ].some((value) => value?.toLocaleLowerCase("de").includes(query)) ); }, [projectDeviceQuery, projectDevices]); const floorById = useMemo(() => new Map(floors.map((item) => [item.id, item])), [floors]); const circuitListByBoardId = useMemo( () => new Map(circuitLists.map((circuitList) => [circuitList.distributionBoardId, circuitList])), [circuitLists] ); const enabledBoardSupplyTypes = project?.enabledDistributionBoardSupplyTypes ?? []; const usedBoardSupplyTypes = useMemo( () => [ ...new Set( boards .map((board) => board.supplyType) .filter( ( supplyType ): supplyType is DistributionBoardSupplyType => supplyType !== null ) ), ], [boards] ); function applyProjectRevision(currentRevision: number) { setProject((current) => current ? { ...current, currentRevision } : current ); } async function handleCreateBoard(event: FormEvent) { event.preventDefault(); if (!projectId || !project || !boardName.trim()) { return; } setIsSaving(true); setError(null); try { const result = await createDistributionBoard( projectId, { name: boardName.trim(), floorId: boardFloorId || null, supplyType: boardSupplyType, }, project.currentRevision ); setBoards((current) => [ ...current, result.distributionBoard, ]); setCircuitLists(await listCircuitLists(projectId)); applyProjectRevision(result.history.currentRevision); setBoardName(""); setBoardFloorId(""); setBoardSupplyType( project.enabledDistributionBoardSupplyTypes[0] ); setStructureModal(null); } 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 || !project || !floorName.trim()) { return; } setIsSaving(true); setError(null); try { const result = await createFloor( projectId, { name: floorName.trim() }, project.currentRevision ); setFloors((current) => [...current, result.floor]); applyProjectRevision(result.history.currentRevision); setFloorName(""); setStructureModal(null); } 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 || !project || !roomNumber.trim() || !roomName.trim() ) { return; } setIsSaving(true); setError(null); try { const result = await createRoom( projectId, { floorId: roomFloorId || undefined, roomNumber: roomNumber.trim(), roomName: roomName.trim(), }, project.currentRevision ); setRooms((current) => [...current, result.room]); applyProjectRevision(result.history.currentRevision); setRoomNumber(""); setRoomName(""); setRoomFloorId(""); setStructureModal(null); } catch (err) { setError(err instanceof Error ? err.message : "Raum konnte nicht erstellt werden."); } finally { setIsSaving(false); } } async function handleSaveProjectSettings(input: ProjectSettingsInput) { if (!projectId || !project) { return; } setIsSaving(true); setError(null); try { const result = await updateProjectSettings( projectId, project.currentRevision, input ); setProject(result.project); setIsProjectSettingsOpen(false); } catch (err) { setError(err instanceof Error ? err.message : "Projekteigenschaften konnten nicht gespeichert werden."); } finally { setIsSaving(false); } } async function handleCreateProjectDevice( payload: CreateProjectDeviceInput ) { if (!projectId || !project || !payload.name.trim()) { return; } setIsSaving(true); setError(null); try { const result = await createProjectDevice( projectId, payload, project.currentRevision ); setProjectDevices((current) => [...current, result.projectDevice]); applyProjectRevision(result.history.currentRevision); setIsProjectDeviceModalOpen(false); } catch (err) { setError(err instanceof Error ? err.message : "Projektgerät konnte nicht erstellt werden."); } finally { setIsSaving(false); } } async function handleDeleteProjectDevice(projectDeviceId: string) { if (!projectId || !project) { return; } setIsSaving(true); setError(null); try { const result = await deleteProjectDevice( projectId, projectDeviceId, project.currentRevision ); setProjectDevices((current) => current.filter((item) => item.id !== projectDeviceId)); applyProjectRevision(result.history.currentRevision); } catch (err) { setError(err instanceof Error ? err.message : "Projektgerät konnte nicht gelöscht werden."); } finally { setIsSaving(false); } } async function handleUpdateProjectDevice( device: ProjectDeviceDto, payload: CreateProjectDeviceInput ) { if (!projectId || !project) { return; } setIsSaving(true); setError(null); try { const result = await updateProjectDevice( projectId, device.id, payload, project.currentRevision ); setProjectDevices((current) => current.map((item) => item.id === device.id ? result.projectDevice : item ) ); applyProjectRevision(result.history.currentRevision); setEditingProjectDevice(null); await openProjectDeviceSyncPreview(result.projectDevice.id); } catch (err) { setError(err instanceof Error ? err.message : "Projektgerät konnte nicht aktualisiert werden."); } finally { setIsSaving(false); } } function openBoardEditor(board: DistributionBoardDto) { setEditingBoard(board); setEditingBoardFloorId(board.floorId ?? ""); setEditingBoardSupplyType( board.supplyType ?? project?.enabledDistributionBoardSupplyTypes[0] ?? "AV" ); setEditingBoardSimultaneityFactor( String(board.simultaneityFactor) ); } function openBoardCreator() { setBoardSupplyType( project?.enabledDistributionBoardSupplyTypes[0] ?? "AV" ); setStructureModal("board"); } async function handleUpdateBoard(event: FormEvent) { event.preventDefault(); if (!projectId || !project || !editingBoard) { return; } const simultaneityFactor = Number( editingBoardSimultaneityFactor ); if ( !editingBoardSimultaneityFactor.trim() || !Number.isFinite(simultaneityFactor) || simultaneityFactor < 0 || simultaneityFactor > 1 ) { return; } setIsSaving(true); setError(null); try { const result = await updateDistributionBoard( projectId, editingBoard.id, { floorId: editingBoardFloorId || null, supplyType: editingBoardSupplyType, simultaneityFactor, }, project.currentRevision ); setBoards((current) => current.map((board) => board.id === result.distributionBoard.id ? result.distributionBoard : board ) ); applyProjectRevision(result.history.currentRevision); setEditingBoard(null); } catch (err) { setError( err instanceof Error ? err.message : "Verteilung konnte nicht bearbeitet werden." ); } finally { setIsSaving(false); } } async function handleExportProject() { if (!project) { return; } setIsSaving(true); setError(null); try { const transfer = await exportProjectTransfer(project.id); const blob = new Blob([JSON.stringify(transfer, null, 2)], { type: "application/json", }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = `${project.name.replace(/[^a-zA-Z0-9_-]+/g, "-") || "projekt"}.leistungsbilanz.json`; anchor.click(); URL.revokeObjectURL(url); } catch (err) { setError( err instanceof Error ? err.message : "Projekt konnte nicht exportiert werden." ); } finally { setIsSaving(false); } } async function handleImportProject( transfer: unknown, mode: "replace" | "duplicate" ) { if (!project) { return; } setIsSaving(true); setError(null); try { const result = await importProjectTransfer( project.id, mode, project.currentRevision, transfer ); if (mode === "duplicate" && "projectId" in result) { window.location.href = `/projects/${result.projectId}`; return; } window.location.reload(); } catch (err) { setError( err instanceof Error ? err.message : "Projektdatei konnte nicht importiert werden." ); setIsProjectSettingsOpen(false); } finally { setIsSaving(false); } } async function openProjectDeviceSyncPreview(projectDeviceId: string) { if (!projectId) { return; } try { 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 || !project || !syncPreview || selectedSyncRowIds.length === 0 || selectedSyncFields.length === 0 ) { return; } setIsSaving(true); setError(null); try { const result = await synchronizeProjectDeviceRows( projectId, syncPreview.projectDevice.id, selectedSyncRowIds, selectedSyncFields, project.currentRevision ); setSyncPreview(result.preview); applyProjectRevision(result.history.currentRevision); setSelectedSyncRowIds( result.preview.rows.filter((row) => row.differences.length > 0).map((row) => row.rowId) ); } catch (err) { setError(err instanceof Error ? err.message : "Gerätezeilen konnten nicht synchronisiert werden."); } finally { setIsSaving(false); } } async function handleDisconnectProjectDeviceRows() { if ( !projectId || !project || !syncPreview || selectedSyncRowIds.length === 0 ) { return; } setIsSaving(true); setError(null); try { const result = await disconnectProjectDeviceRows( projectId, syncPreview.projectDevice.id, selectedSyncRowIds, project.currentRevision ); setSyncPreview(result.preview); setSelectedSyncRowIds( result.preview.rows .filter((row) => row.differences.length > 0) .map((row) => row.rowId) ); applyProjectRevision(result.history.currentRevision); } catch (err) { setError(err instanceof Error ? err.message : "Verknüpfungen konnten nicht getrennt werden."); } finally { setIsSaving(false); } } async function handleCopyGlobalToProject(globalDeviceId: string) { if (!projectId || !project || !globalDeviceId) { return; } setIsSaving(true); setError(null); try { const result = await copyGlobalDeviceToProject( projectId, globalDeviceId, project.currentRevision ); setProjectDevices((current) => [...current, result.projectDevice]); applyProjectRevision(result.history.currentRevision); setIsProjectDeviceModalOpen(false); } 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"}

{project?.internalProjectNumber ? `Projektnummer ${project.internalProjectNumber} · ` : ""} Verteilerübersicht und Einstieg in die Stromkreislisten

Zur Projektübersicht
{error ?
{error}
: null}
{project ? (
window.location.reload()} projectId={projectId} />
) : null}
Verteilungen {boardCount}
{boards.map((board) => { const circuitList = circuitListByBoardId.get(board.id); return ( ); })} {!boards.length ? ( ) : null}
Verteilung Etage Netzart GZF Aktionen
{board.name} {board.floorId ? floorById.get(board.floorId)?.name ?? "Unbekannt" : "Ohne Etage"} {board.supplyType ? distributionBoardSupplyTypeLabels[ board.supplyType ] : "Nicht festgelegt"} {board.simultaneityFactor.toLocaleString( "de-DE", { minimumFractionDigits: 2, maximumFractionDigits: 2, } )}
{circuitList ? ( Stromkreisliste öffnen ) : ( Keine Stromkreisliste vorhanden )}
Noch keine Verteilungen vorhanden.
Etagen {floorCount}
    {floors.map((floor) => (
  • {floor.name}
  • ))} {!floors.length ? (
  • Noch keine Etagen vorhanden.
  • ) : null}
Räume {roomCount}
{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 {projectDeviceCount}
setProjectDeviceQuery(event.target.value)} placeholder="Geräte durchsuchen" type="search" value={projectDeviceQuery} />
{visibleProjectDevices.map((device) => ( ))} {!visibleProjectDevices.length ? ( ) : null}
Gerät Anschluss Leistung Kostengruppe Aktionen
{device.displayName}
{device.name} {device.category ? ` · ${device.category}` : ""}
{device.phaseType === "three_phase" ? "3-phasig" : "1-phasig"} {device.connectionKind ? (
{device.connectionKind}
) : null}
{device.totalPower} kW
{device.quantity} × {device.powerPerUnit} kW · GZF{" "} {device.simultaneityFactor}
{device.costGroup ?? "-"}
{projectDevices.length ? "Keine Projektgeräte entsprechen der Suche." : "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 Gerätezeilen im neuen Stromkreismodell vorhanden.
) : null}
{structureModal === "board" ? ( setStructureModal(null)} onSubmit={handleCreateBoard} submitDisabled={!boardName.trim()} submitLabel="Verteilung erstellen" title="Verteilung hinzufügen" > setBoardName(event.target.value)} placeholder="z. B. UV-01" required value={boardName} />
) : null} {editingBoard ? ( setEditingBoard(null)} onSubmit={handleUpdateBoard} submitDisabled={ !editingBoardSimultaneityFactor.trim() || !Number.isFinite( Number(editingBoardSimultaneityFactor) ) || Number(editingBoardSimultaneityFactor) < 0 || Number(editingBoardSimultaneityFactor) > 1 } submitLabel="Änderungen speichern" title="Verteilung bearbeiten" >
setEditingBoardSimultaneityFactor( event.target.value ) } required step="0.01" type="number" value={editingBoardSimultaneityFactor} />
Wird auf die Gesamtleistung des Verteilers angewendet.
) : null} {structureModal === "floor" ? ( setStructureModal(null)} onSubmit={handleCreateFloor} submitDisabled={!floorName.trim()} submitLabel="Etage hinzufügen" title="Etage hinzufügen" > setFloorName(event.target.value)} placeholder="z. B. EG" required value={floorName} /> ) : null} {structureModal === "room" ? ( setStructureModal(null)} onSubmit={handleCreateRoom} submitDisabled={!roomNumber.trim() || !roomName.trim()} submitLabel="Raum anlegen" title="Raum hinzufügen" >
setRoomNumber(event.target.value)} required value={roomNumber} />
setRoomName(event.target.value)} required value={roomName} />
) : null} {isProjectDeviceModalOpen ? ( setIsProjectDeviceModalOpen(false)} onImportGlobal={handleCopyGlobalToProject} onSave={handleCreateProjectDevice} /> ) : null} {editingProjectDevice ? ( setEditingProjectDevice(null)} onImportGlobal={handleCopyGlobalToProject} onSave={(input) => handleUpdateProjectDevice(editingProjectDevice, input) } /> ) : null} {project && isProjectSettingsOpen ? ( setIsProjectSettingsOpen(false)} onExport={handleExportProject} onImport={handleImportProject} onSave={handleSaveProjectSettings} project={project} usedDistributionBoardSupplyTypes={usedBoardSupplyTypes} /> ) : null}
); }