"use client"; import Link from "next/link"; import { useParams } from "next/navigation"; import { FormEvent, useEffect, useMemo, useState } from "react"; import { ArrowLeft, FileUp, Library, Link2, ListTree, Pencil, Plus, RefreshCw, Settings, Trash2, Unlink, X, } from "lucide-react"; import { IconButton } from "../../../frontend/components/icon-button"; import { IconLink } from "../../../frontend/components/icon-link"; import { CollapsibleFilterPanel, FilterSearchField, } from "../../../frontend/components/filter-panel"; import { MultiSelectDropdown } from "../../../frontend/components/multi-select-dropdown"; import { circuitGroupCategories, circuitGroupCategoryLabels, } from "../../../shared/constants/circuit-group"; import { distributionBoardSupplyTypes } from "../../../shared/constants/distribution-board"; import { buildCostGroupFilterOptions, buildFloorFilterOptions, buildSupplyTypeFilterOptions, emptyDistributionBoardFilters, emptyProjectDeviceFilters, filterDistributionBoards, filterProjectDevices, isDistributionBoardFilterActive, isProjectDeviceFilterActive, retainAvailableFilterValues, type DistributionBoardFilters, type FilterOption, type ProjectDeviceFilters, } from "../../../frontend/utils/overview-filters"; import { copyGlobalDeviceToProject, copyDistributionBoard, createFeederCircuitAndUpdateDistributionBoard, copyProjectDeviceToGlobal, createDistributionBoard, createProjectDevice, createRoom, deleteProjectDevice, deleteDistributionBoard, disconnectProjectDeviceRows, exportProjectTransfer, getProject, getProjectCircuitTrees, getProjectDeviceSyncPreview, listBuildings, listCircuitLists, listDistributionBoards, listFloors, listGlobalDevices, listProjectDevices, listRooms, importProjectTransfer, synchronizeProjectDeviceRows, updateDistributionBoard, updateProjectDevice, updateProjectSettings, } from "../../../frontend/utils/api"; import { createClientId } from "../../../frontend/utils/client-id"; import { buildFeederCircuitSnapshot, getDefaultFeederCircuitSection, getNextFeederCircuitIdentifier, } from "../../../frontend/utils/distribution-board-feeder-circuit"; import type { BuildingDto, CircuitListDto, CircuitTreeResponseDto, CreateRoomInput, 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 { DistributionBoardInfoCard, fromDistributionBoardInfoDraft, toDistributionBoardInfoDraft, type DistributionBoardInfoDraft, } from "../../../frontend/components/distribution-board-info-card"; import { Printer } from "lucide-react"; 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"; import { RevitCsvModal } from "../../../frontend/components/revit-csv-modal"; import { FloorSelectOptions } from "../../../frontend/components/floor-select-options"; 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", }; const projectDeviceCategoryFilterOptions: FilterOption[] = circuitGroupCategories.map((category) => ({ id: category, label: circuitGroupCategoryLabels[category], })); const projectDevicePhaseTypeLabels: Record = { single_phase: "1-phasig", three_phase: "3-phasig", }; const projectDevicePhaseTypeFilterOptions: FilterOption[] = ( ["single_phase", "three_phase"] as const ).map((phaseType) => ({ id: phaseType, label: projectDevicePhaseTypeLabels[phaseType], })); const boardFilterCollapsedStorageKey = "leistungsbilanz:overview-filter-collapsed:distribution-boards"; const projectDeviceFilterCollapsedStorageKey = "leistungsbilanz:overview-filter-collapsed:project-devices"; const createFeederCircuitSelectValue = "__create_feeder_circuit__"; 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 [circuitTrees, setCircuitTrees] = useState([]); const [buildings, setBuildings] = 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 [editingBoardCopyName, setEditingBoardCopyName] = useState(""); const [editingFeedingBoardId, setEditingFeedingBoardId] = useState(""); const [editingFeedingCircuitId, setEditingFeedingCircuitId] = useState(""); const [isCreatingFeederCircuit, setIsCreatingFeederCircuit] = useState(false); const [editingFeederCircuitSectionId, setEditingFeederCircuitSectionId] = useState(""); const [editingFeederCircuitDisplayName, setEditingFeederCircuitDisplayName] = useState(""); const [editingBoardInfo, setEditingBoardInfo] = useState( toDistributionBoardInfoDraft({ boardNumber: null, location: null, fedFrom: null, remark: null, }) ); const [isProjectSettingsOpen, setIsProjectSettingsOpen] = useState(false); const [isRevitCsvOpen, setIsRevitCsvOpen] = useState(false); const [structureModal, setStructureModal] = useState<"board" | null>(null); const [isProjectDeviceModalOpen, setIsProjectDeviceModalOpen] = useState(false); const [editingProjectDevice, setEditingProjectDevice] = useState(null); const [boardFilters, setBoardFilters] = useState( emptyDistributionBoardFilters ); const [projectDeviceFilters, setProjectDeviceFilters] = useState(emptyProjectDeviceFilters); 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([ getProject(projectId), listDistributionBoards(projectId), listCircuitLists(projectId), getProjectCircuitTrees(projectId), listBuildings(projectId), listFloors(projectId), listRooms(projectId), listProjectDevices(projectId), listGlobalDevices(), ]) .then(([ currentProject, distributionBoards, loadedCircuitLists, loadedCircuitTrees, loadedBuildings, loadedFloors, loadedRooms, loadedProjectDevices, loadedGlobalDevices, ]) => { setProject(currentProject); setBoards(distributionBoards); setCircuitLists(loadedCircuitLists); setCircuitTrees(loadedCircuitTrees.trees); setBuildings(loadedBuildings); 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 buildingCount = useMemo(() => buildings.length, [buildings.length]); const floorCount = useMemo(() => floors.length, [floors.length]); const roomCount = useMemo(() => rooms.length, [rooms.length]); const projectDeviceCount = useMemo(() => projectDevices.length, [projectDevices.length]); const costGroupFilterOptions = useMemo( () => buildCostGroupFilterOptions(projectDevices), [projectDevices] ); const effectiveProjectDeviceFilters = useMemo( () => ({ ...projectDeviceFilters, costGroups: retainAvailableFilterValues( projectDeviceFilters.costGroups, costGroupFilterOptions ), }), [costGroupFilterOptions, projectDeviceFilters] ); const visibleProjectDevices = useMemo( () => filterProjectDevices( projectDevices, effectiveProjectDeviceFilters, circuitGroupCategoryLabels ), [effectiveProjectDeviceFilters, projectDevices] ); const floorById = useMemo(() => new Map(floors.map((item) => [item.id, item])), [floors]); const floorFilterOptions = useMemo( () => buildFloorFilterOptions(floors), [floors] ); const supplyTypeFilterOptions = useMemo( () => buildSupplyTypeFilterOptions( distributionBoardSupplyTypes, project?.enabledDistributionBoardSupplyTypes ?? [], boards, distributionBoardSupplyTypeLabels ), [boards, project?.enabledDistributionBoardSupplyTypes] ); const effectiveBoardFilters = useMemo( () => ({ ...boardFilters, floorIds: retainAvailableFilterValues( boardFilters.floorIds, floorFilterOptions ), supplyTypes: retainAvailableFilterValues( boardFilters.supplyTypes, supplyTypeFilterOptions ), }), [boardFilters, floorFilterOptions, supplyTypeFilterOptions] ); const visibleBoards = useMemo( () => filterDistributionBoards( boards, effectiveBoardFilters, new Map(floors.map((floor) => [floor.id, floor.name])) ), [boards, effectiveBoardFilters, floors] ); const circuitListByBoardId = useMemo( () => new Map(circuitLists.map((circuitList) => [circuitList.distributionBoardId, circuitList])), [circuitLists] ); const circuitTreeByBoardId = useMemo( () => new Map(circuitTrees.map((tree) => [tree.distributionBoardId, tree])), [circuitTrees] ); const feedingBoardOptions = useMemo(() => { if (!editingBoard) return []; const boardById = new Map(boards.map((board) => [board.id, board])); return boards.filter((candidate) => { if (candidate.id === editingBoard.id) return false; const visited = new Set(); let current: DistributionBoardDto | undefined = candidate; while (current && current.feedingDistributionBoardId && !visited.has(current.id)) { if (current.feedingDistributionBoardId === editingBoard.id) return false; visited.add(current.id); current = boardById.get(current.feedingDistributionBoardId); } return true; }); }, [boards, editingBoard]); const selectedFeedingTree = editingFeedingBoardId ? circuitTreeByBoardId.get(editingFeedingBoardId) : undefined; const selectedFeedingCircuits = useMemo( () => selectedFeedingTree?.sections.flatMap((section) => section.circuits.map((circuit) => ({ circuit, section })) ) ?? [], [selectedFeedingTree] ); const selectedFeedingCircuit = selectedFeedingCircuits.find( ({ circuit }) => circuit.id === editingFeedingCircuitId ); const feederCircuitSections = selectedFeedingTree?.sections.filter( (section) => section.category !== undefined ) ?? []; const selectedNewFeederCircuitSection = feederCircuitSections.find( (section) => section.id === editingFeederCircuitSectionId ); const newFeederCircuitIdentifier = selectedNewFeederCircuitSection ? getNextFeederCircuitIdentifier(selectedNewFeederCircuitSection) : null; const selectedFeedingCircuitCategory = selectedFeedingCircuit?.section.category ?? (isCreatingFeederCircuit ? selectedNewFeederCircuitSection?.category : undefined); const selectedFeedingBoard = boards.find( (board) => board.id === editingFeedingBoardId ); const selectedFeedingCircuitUseCount = editingFeedingCircuitId ? boards.filter( (board) => board.id !== editingBoard?.id && board.feedingCircuitId === editingFeedingCircuitId ).length : 0; const enabledBoardSupplyTypes = project?.enabledDistributionBoardSupplyTypes ?? []; const usedBoardSupplyTypes = useMemo( () => [ ...new Set( boards .map((board) => board.supplyType) .filter( ( supplyType ): supplyType is DistributionBoardSupplyType => supplyType !== null ) ), ], [boards] ); function formatBoardFeedingReference(board: DistributionBoardDto) { if (!board.feedingDistributionBoardId) return board.fedFrom ?? "–"; const feedingBoard = boards.find( (candidate) => candidate.id === board.feedingDistributionBoardId ); if (!feedingBoard) return "Unbekannte Verteilung"; const boardLabel = feedingBoard.boardNumber ?? feedingBoard.name; if (!board.feedingCircuitId) return boardLabel; const circuit = circuitTreeByBoardId .get(feedingBoard.id) ?.sections.flatMap((section) => section.circuits) .find((candidate) => candidate.id === board.feedingCircuitId); return `${boardLabel}${circuit?.equipmentIdentifier ?? ""}`; } 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); setCircuitTrees((await getProjectCircuitTrees(projectId)).trees); 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 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; } const device = projectDevices.find((item) => item.id === projectDeviceId); const deviceName = device?.displayName || device?.name || "Projektgerät"; if (!window.confirm(`Projektgerät „${deviceName}“ wirklich löschen?`)) { 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) ); setEditingBoardCopyName(`${board.name} Kopie`); setEditingBoardInfo(toDistributionBoardInfoDraft(board)); setEditingFeedingBoardId(board.feedingDistributionBoardId ?? ""); setEditingFeedingCircuitId(board.feedingCircuitId ?? ""); setIsCreatingFeederCircuit(false); setEditingFeederCircuitSectionId(""); setEditingFeederCircuitDisplayName(""); } 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 boardInput = { floorId: editingBoardFloorId || null, supplyType: editingBoardSupplyType, simultaneityFactor, feedingDistributionBoardId: editingFeedingBoardId || null, feedingCircuitId: editingFeedingCircuitId || null, ...fromDistributionBoardInfoDraft(editingBoardInfo), }; let result; if (isCreatingFeederCircuit) { if (!editingFeedingBoardId || !selectedFeedingTree) { throw new Error("Bitte eine speisende Verteilung wählen."); } if (!selectedNewFeederCircuitSection) { throw new Error( "Bitte eine Stromkreisgruppe für den neuen Abgang wählen." ); } const circuit = buildFeederCircuitSnapshot({ id: createClientId(), tree: selectedFeedingTree, sectionId: selectedNewFeederCircuitSection.id, displayName: editingFeederCircuitDisplayName, }); result = await createFeederCircuitAndUpdateDistributionBoard( projectId, editingBoard.id, { ...boardInput, feedingDistributionBoardId: editingFeedingBoardId, feedingCircuitId: circuit.id, }, circuit, project.currentRevision ); } else { result = await updateDistributionBoard( projectId, editingBoard.id, boardInput, project.currentRevision ); } applyProjectRevision(result.history.currentRevision); const [updatedBoards, updatedTrees] = await Promise.all([ listDistributionBoards(projectId), getProjectCircuitTrees(projectId), ]); setBoards(updatedBoards); setCircuitTrees(updatedTrees.trees); setEditingBoard(null); } catch (err) { setError( err instanceof Error ? err.message : "Verteilung konnte nicht bearbeitet werden." ); } finally { setIsSaving(false); } } async function handleCreateRevitRoom(input: CreateRoomInput) { if (!projectId || !project) throw new Error("Projekt ist nicht geladen."); const result = await createRoom(projectId, input, project.currentRevision); setRooms((current) => [...current, result.room]); applyProjectRevision(result.history.currentRevision); return result; } async function handleCreateRevitProjectDevice(input: CreateProjectDeviceInput) { if (!projectId || !project) throw new Error("Projekt ist nicht geladen."); const result = await createProjectDevice(projectId, input, project.currentRevision); setProjectDevices((current) => [...current, result.projectDevice]); applyProjectRevision(result.history.currentRevision); return result; } async function handleCopyBoard() { if ( !projectId || !project || !editingBoard || !editingBoardCopyName.trim() ) { return; } setIsSaving(true); setError(null); try { const result = await copyDistributionBoard( projectId, editingBoard.id, editingBoardCopyName.trim(), project.currentRevision ); setBoards((current) => [ ...current, result.distributionBoard, ]); setCircuitLists((current) => [ ...current, result.circuitList, ]); applyProjectRevision(result.history.currentRevision); setCircuitTrees((await getProjectCircuitTrees(projectId)).trees); setEditingBoard(null); } catch (err) { setError( err instanceof Error ? err.message : "Verteilung konnte nicht kopiert werden." ); } finally { setIsSaving(false); } } async function handleDeleteBoard() { if (!projectId || !project || !editingBoard) { return; } const confirmed = window.confirm( `Verteilung „${editingBoard.name}“ wirklich löschen?\n\n` + "Dabei werden die Stromkreisliste, alle Gruppen, Stromkreise, Gerätezeilen, Schutzgeräte und weiteren Verteilergeräte entfernt. " + "Der Vorgang kann anschließend über die projektweite Historie rückgängig gemacht werden." ); if (!confirmed) { return; } setIsSaving(true); setError(null); try { const result = await deleteDistributionBoard( projectId, editingBoard.id, project.currentRevision ); setBoards((current) => current.filter((board) => board.id !== result.distributionBoardId) ); setCircuitLists((current) => current.filter( (circuitList) => circuitList.distributionBoardId !== result.distributionBoardId ) ); applyProjectRevision(result.history.currentRevision); setCircuitTrees((await getProjectCircuitTrees(projectId)).trees); setEditingBoard(null); } catch (err) { setError( err instanceof Error ? err.message : "Verteilung konnte nicht gelöscht 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 (
Projekt

{project?.name ?? "Projekt"}

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

{error ?
{error}
: null}
Verteilungen
{boardCount}
Gebäude
{buildingCount}
Etagen
{floorCount}
Räume
{roomCount}
Projektgeräte
{projectDeviceCount}
{project ? (
window.location.reload()} projectId={projectId} />
) : null}
Verteilungen {boardCount}
setBoardFilters(emptyDistributionBoardFilters)} storageKey={boardFilterCollapsedStorageKey} totalCount={boards.length} visibleCount={visibleBoards.length} > setBoardFilters((current) => ({ ...current, query })) } placeholder="Verteilungen durchsuchen …" value={boardFilters.query} /> setBoardFilters((current) => ({ ...current, floorIds })) } options={floorFilterOptions} selected={effectiveBoardFilters.floorIds} /> setBoardFilters((current) => ({ ...current, supplyTypes })) } options={supplyTypeFilterOptions} selected={effectiveBoardFilters.supplyTypes} />
{visibleBoards.map((board) => { const circuitList = circuitListByBoardId.get(board.id); return ( ); })} {!boards.length ? ( ) : null}
Nr. Verteilung Örtlichkeit Etage Netzart Gespeist von GZF Aktionen
{board.boardNumber ?? "–"} {board.name} {board.location ?? "–"} {board.floorId ? floorById.get(board.floorId)?.name ?? "Unbekannt" : "Ohne Etage"} {board.supplyType ? distributionBoardSupplyTypeLabels[ board.supplyType ] : "Nicht festgelegt"} {formatBoardFeedingReference(board)} {board.simultaneityFactor.toLocaleString( "de-DE", { minimumFractionDigits: 2, maximumFractionDigits: 2, } )}
openBoardEditor(board)} /> {circuitList ? ( ) : ( Keine Stromkreisliste vorhanden )}
Noch keine Verteilungen vorhanden.
{boards.length > 0 && visibleBoards.length === 0 ? (

Keine Verteilungen entsprechen den Filtern.

) : null}
Projektgeräte {projectDeviceCount}
setIsProjectDeviceModalOpen(true)} showLabel variant="primary" />
setProjectDeviceFilters(emptyProjectDeviceFilters)} storageKey={projectDeviceFilterCollapsedStorageKey} totalCount={projectDevices.length} visibleCount={visibleProjectDevices.length} > setProjectDeviceFilters((current) => ({ ...current, query })) } placeholder="Gerät, Anschlussart, KG …" value={projectDeviceFilters.query} /> setProjectDeviceFilters((current) => ({ ...current, categories })) } options={projectDeviceCategoryFilterOptions} selected={effectiveProjectDeviceFilters.categories} /> setProjectDeviceFilters((current) => ({ ...current, phaseTypes })) } options={projectDevicePhaseTypeFilterOptions} selected={effectiveProjectDeviceFilters.phaseTypes} /> setProjectDeviceFilters((current) => ({ ...current, costGroups })) } options={costGroupFilterOptions} selected={effectiveProjectDeviceFilters.costGroups} />
{visibleProjectDevices.map((device) => ( ))} {!visibleProjectDevices.length ? ( ) : null}
Gerät Anschluss Leistung Kostengruppe Aktionen
{device.displayName}
{device.name} {device.category ? ` · ${circuitGroupCategoryLabels[device.category] ?? device.category}` : ""}
{projectDevicePhaseTypeLabels[device.phaseType]} {device.connectionKind ? (
{device.connectionKind}
) : null}
{device.totalPower} kW
{device.quantity} × {device.powerPerUnit} kW · GZF{" "} {device.simultaneityFactor}
{device.costGroup ?? "-"}
setEditingProjectDevice(device)} /> void openProjectDeviceSyncPreview(device.id)} /> void handleCopyProjectToGlobal(device.id)} /> void handleDeleteProjectDevice(device.id)} variant="danger" />
{projectDevices.length ? "Keine Projektgeräte entsprechen den Filtern." : "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.

setSyncPreview(null)} />
{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.
void handleSynchronizeProjectDeviceRows()} showLabel variant="primary" /> void handleDisconnectProjectDeviceRows()} showLabel variant="danger" />
) : 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.
Logische Einspeisung
{isCreatingFeederCircuit ? (
Vorgesehenes BMK: {newFeederCircuitIdentifier ?? "–"}
setEditingFeederCircuitDisplayName(event.target.value) } placeholder="z. B. Steigetrasse Ost" value={editingFeederCircuitDisplayName} />
Ohne Zusatzbezeichnung wird der Name der gespeisten Verteilung angezeigt.
Der Abgang wird erst beim Speichern angelegt und gemeinsam mit dieser Verteilung verknüpft. Es entsteht keine Gerätezeile.
) : null} {editingFeedingBoardId && !editingFeedingCircuitId && !isCreatingFeederCircuit ? (
Ohne speisenden Abgang wird die Hierarchie angezeigt, die Leistung dieser Verteilung aber keinem Stromkreis zugerechnet.
) : null} {selectedFeedingBoard?.supplyType && editingBoardSupplyType !== selectedFeedingBoard.supplyType ? (
Die Netzarten weichen ab: {selectedFeedingBoard.supplyType} {" → "}{editingBoardSupplyType}. Bitte Umschaltung und Versorgungskonzept prüfen.
) : null} {selectedFeedingCircuitCategory !== undefined && selectedFeedingCircuitCategory !== "three_phase" ? (
Der gewählte Abgang ist 1-phasig. Bitte prüfen, ob diese Einspeisung für die Unterverteilung geeignet ist.
) : null} {selectedFeedingCircuitUseCount > 0 ? (
Dieser Abgang speist bereits {selectedFeedingCircuitUseCount} {selectedFeedingCircuitUseCount === 1 ? " weitere Verteilung." : " weitere Verteilungen."}
) : null}

Verteilung kopieren

Erstellt eine vollständige Kopie des zuletzt gespeicherten Stands einschließlich Gruppen, Stromkreisen, Gerätezeilen und Schutzgeräten. Noch nicht gespeicherte Änderungen oben werden nicht übernommen.

setEditingBoardCopyName(event.target.value) } value={editingBoardCopyName} />

Gefahrenbereich

Entfernt die vollständige Verteilung. Die Aktion wird erst nach einer ausdrücklichen Warnung ausgeführt und kann über die projektweite Historie rückgängig gemacht werden.

) : 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} {project && isRevitCsvOpen ? ( setIsRevitCsvOpen(false)} onCreateProjectDevice={handleCreateRevitProjectDevice} onCreateRoom={handleCreateRevitRoom} onRevisionChange={applyProjectRevision} projectId={projectId} /> ) : null}
); }