Show logical feeder loads in project and circuit views

This commit is contained in:
Julian Appel 2026-09-27 22:37:04 +02:00
parent dfeb0e3cb6
commit 042a050876
12 changed files with 319 additions and 11 deletions

View file

@ -56,6 +56,7 @@ import {
disconnectProjectDeviceRows,
exportProjectTransfer,
getProject,
getProjectCircuitTrees,
getProjectDeviceSyncPreview,
listBuildings,
listCircuitLists,
@ -73,6 +74,7 @@ import {
import type {
BuildingDto,
CircuitListDto,
CircuitTreeResponseDto,
CreateRoomInput,
CreateProjectDeviceInput,
DistributionBoardDto,
@ -151,6 +153,7 @@ export default function ProjectDetailPage() {
const [project, setProject] = useState<ProjectDto | null>(null);
const [boards, setBoards] = useState<DistributionBoardDto[]>([]);
const [circuitLists, setCircuitLists] = useState<CircuitListDto[]>([]);
const [circuitTrees, setCircuitTrees] = useState<CircuitTreeResponseDto[]>([]);
const [buildings, setBuildings] = useState<BuildingDto[]>([]);
const [floors, setFloors] = useState<FloorDto[]>([]);
const [rooms, setRooms] = useState<RoomDto[]>([]);
@ -170,6 +173,8 @@ export default function ProjectDetailPage() {
setEditingBoardSimultaneityFactor,
] = useState("1");
const [editingBoardCopyName, setEditingBoardCopyName] = useState("");
const [editingFeedingBoardId, setEditingFeedingBoardId] = useState("");
const [editingFeedingCircuitId, setEditingFeedingCircuitId] = useState("");
const [editingBoardInfo, setEditingBoardInfo] =
useState<DistributionBoardInfoDraft>(
toDistributionBoardInfoDraft({
@ -209,6 +214,7 @@ export default function ProjectDetailPage() {
getProject(projectId),
listDistributionBoards(projectId),
listCircuitLists(projectId),
getProjectCircuitTrees(projectId),
listBuildings(projectId),
listFloors(projectId),
listRooms(projectId),
@ -219,6 +225,7 @@ export default function ProjectDetailPage() {
currentProject,
distributionBoards,
loadedCircuitLists,
loadedCircuitTrees,
loadedBuildings,
loadedFloors,
loadedRooms,
@ -228,6 +235,7 @@ export default function ProjectDetailPage() {
setProject(currentProject);
setBoards(distributionBoards);
setCircuitLists(loadedCircuitLists);
setCircuitTrees(loadedCircuitTrees.trees);
setBuildings(loadedBuildings);
setFloors(loadedFloors);
setRooms(loadedRooms);
@ -310,6 +318,48 @@ export default function ProjectDetailPage() {
() => 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<string>();
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 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(
@ -329,6 +379,21 @@ export default function ProjectDetailPage() {
[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
@ -358,6 +423,7 @@ export default function ProjectDetailPage() {
]);
setCircuitLists(await listCircuitLists(projectId));
applyProjectRevision(result.history.currentRevision);
setCircuitTrees((await getProjectCircuitTrees(projectId)).trees);
setBoardName("");
setBoardFloorId("");
setBoardSupplyType(
@ -488,6 +554,8 @@ export default function ProjectDetailPage() {
);
setEditingBoardCopyName(`${board.name} Kopie`);
setEditingBoardInfo(toDistributionBoardInfoDraft(board));
setEditingFeedingBoardId(board.feedingDistributionBoardId ?? "");
setEditingFeedingCircuitId(board.feedingCircuitId ?? "");
}
function openBoardCreator() {
@ -523,6 +591,8 @@ export default function ProjectDetailPage() {
floorId: editingBoardFloorId || null,
supplyType: editingBoardSupplyType,
simultaneityFactor,
feedingDistributionBoardId: editingFeedingBoardId || null,
feedingCircuitId: editingFeedingCircuitId || null,
...fromDistributionBoardInfoDraft(editingBoardInfo),
},
project.currentRevision
@ -535,6 +605,7 @@ export default function ProjectDetailPage() {
)
);
applyProjectRevision(result.history.currentRevision);
setCircuitTrees((await getProjectCircuitTrees(projectId)).trees);
setEditingBoard(null);
} catch (err) {
setError(
@ -590,6 +661,7 @@ export default function ProjectDetailPage() {
result.circuitList,
]);
applyProjectRevision(result.history.currentRevision);
setCircuitTrees((await getProjectCircuitTrees(projectId)).trees);
setEditingBoard(null);
} catch (err) {
setError(
@ -633,6 +705,7 @@ export default function ProjectDetailPage() {
)
);
applyProjectRevision(result.history.currentRevision);
setCircuitTrees((await getProjectCircuitTrees(projectId)).trees);
setEditingBoard(null);
} catch (err) {
setError(
@ -989,6 +1062,7 @@ export default function ProjectDetailPage() {
<th>Örtlichkeit</th>
<th>Etage</th>
<th>Netzart</th>
<th>Gespeist von</th>
<th>GZF</th>
<th className="text-end">Aktionen</th>
</tr>
@ -1015,6 +1089,7 @@ export default function ProjectDetailPage() {
]
: "Nicht festgelegt"}
</td>
<td>{formatBoardFeedingReference(board)}</td>
<td>
{board.simultaneityFactor.toLocaleString(
"de-DE",
@ -1392,7 +1467,7 @@ export default function ProjectDetailPage() {
{editingBoard ? (
<FormModal
description={`${editingBoard.name}: Etagenzuordnung, Netzart und Gleichzeitigkeitsfaktor ändern.`}
description={`${editingBoard.name}: Etagenzuordnung, Netzart, Einspeisung und Gleichzeitigkeitsfaktor ändern.`}
isSaving={isSaving}
onClose={() => setEditingBoard(null)}
onSubmit={handleUpdateBoard}
@ -1479,6 +1554,91 @@ export default function ProjectDetailPage() {
</div>
</div>
</div>
<section className="card mt-4" aria-labelledby="feeding-reference-heading">
<div className="card-header" id="feeding-reference-heading">
Logische Einspeisung
</div>
<div className="card-body row g-3">
<div className="col-12 col-md-6">
<label className="form-label" htmlFor="feeding-distribution-board">
Speisende Verteilung
</label>
<select
className="form-select"
id="feeding-distribution-board"
onChange={(event) => {
setEditingFeedingBoardId(event.target.value);
setEditingFeedingCircuitId("");
}}
value={editingFeedingBoardId}
>
<option value="">Keine Projektverteilung</option>
{feedingBoardOptions.map((board) => (
<option key={board.id} value={board.id}>
{board.boardNumber ?? board.name}
</option>
))}
</select>
</div>
<div className="col-12 col-md-6">
<label className="form-label" htmlFor="feeding-circuit">
Speisender Abgang
</label>
<select
className="form-select"
disabled={!editingFeedingBoardId}
id="feeding-circuit"
onChange={(event) => setEditingFeedingCircuitId(event.target.value)}
value={editingFeedingCircuitId}
>
<option value="">Nur Verteilung bekannt</option>
{selectedFeedingCircuits.map(({ circuit }) => (
<option key={circuit.id} value={circuit.id}>
{circuit.equipmentIdentifier}
{circuit.displayName ? ` · ${circuit.displayName}` : ""}
</option>
))}
</select>
</div>
{editingFeedingBoardId && !editingFeedingCircuitId ? (
<div className="col-12">
<div className="alert alert-warning mb-0 py-2">
Ohne speisenden Abgang wird die Hierarchie angezeigt, die
Leistung dieser Verteilung aber keinem Stromkreis zugerechnet.
</div>
</div>
) : null}
{selectedFeedingBoard?.supplyType &&
editingBoardSupplyType !== selectedFeedingBoard.supplyType ? (
<div className="col-12">
<div className="alert alert-warning mb-0 py-2">
Die Netzarten weichen ab: {selectedFeedingBoard.supplyType}
{" → "}{editingBoardSupplyType}. Bitte Umschaltung und
Versorgungskonzept prüfen.
</div>
</div>
) : null}
{selectedFeedingCircuit &&
selectedFeedingCircuit.section.category !== "three_phase" ? (
<div className="col-12">
<div className="alert alert-warning mb-0 py-2">
Der gewählte Abgang ist 1-phasig. Bitte prüfen, ob diese
Einspeisung für die Unterverteilung geeignet ist.
</div>
</div>
) : null}
{selectedFeedingCircuitUseCount > 0 ? (
<div className="col-12">
<div className="alert alert-info mb-0 py-2">
Dieser Abgang speist bereits {selectedFeedingCircuitUseCount}
{selectedFeedingCircuitUseCount === 1
? " weitere Verteilung."
: " weitere Verteilungen."}
</div>
</div>
) : null}
</div>
</section>
<div className="mt-4">
<DistributionBoardInfoCard
draft={editingBoardInfo}