forked from jappel/leistungsbilanz-ts
Show logical feeder loads in project and circuit views
This commit is contained in:
parent
dfeb0e3cb6
commit
042a050876
12 changed files with 319 additions and 11 deletions
|
|
@ -1541,6 +1541,16 @@ a.kpi:hover {
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tree-grid .downstream-board-load-row td {
|
||||||
|
background: var(--panel-bg-subtle);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-grid .downstream-board-load-row td:nth-child(2) {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.tree-grid .empty-circuit-row td {
|
.tree-grid .empty-circuit-row td {
|
||||||
background: var(--panel-bg-subtle);
|
background: var(--panel-bg-subtle);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ import {
|
||||||
disconnectProjectDeviceRows,
|
disconnectProjectDeviceRows,
|
||||||
exportProjectTransfer,
|
exportProjectTransfer,
|
||||||
getProject,
|
getProject,
|
||||||
|
getProjectCircuitTrees,
|
||||||
getProjectDeviceSyncPreview,
|
getProjectDeviceSyncPreview,
|
||||||
listBuildings,
|
listBuildings,
|
||||||
listCircuitLists,
|
listCircuitLists,
|
||||||
|
|
@ -73,6 +74,7 @@ import {
|
||||||
import type {
|
import type {
|
||||||
BuildingDto,
|
BuildingDto,
|
||||||
CircuitListDto,
|
CircuitListDto,
|
||||||
|
CircuitTreeResponseDto,
|
||||||
CreateRoomInput,
|
CreateRoomInput,
|
||||||
CreateProjectDeviceInput,
|
CreateProjectDeviceInput,
|
||||||
DistributionBoardDto,
|
DistributionBoardDto,
|
||||||
|
|
@ -151,6 +153,7 @@ export default function ProjectDetailPage() {
|
||||||
const [project, setProject] = useState<ProjectDto | null>(null);
|
const [project, setProject] = useState<ProjectDto | null>(null);
|
||||||
const [boards, setBoards] = useState<DistributionBoardDto[]>([]);
|
const [boards, setBoards] = useState<DistributionBoardDto[]>([]);
|
||||||
const [circuitLists, setCircuitLists] = useState<CircuitListDto[]>([]);
|
const [circuitLists, setCircuitLists] = useState<CircuitListDto[]>([]);
|
||||||
|
const [circuitTrees, setCircuitTrees] = useState<CircuitTreeResponseDto[]>([]);
|
||||||
const [buildings, setBuildings] = useState<BuildingDto[]>([]);
|
const [buildings, setBuildings] = useState<BuildingDto[]>([]);
|
||||||
const [floors, setFloors] = useState<FloorDto[]>([]);
|
const [floors, setFloors] = useState<FloorDto[]>([]);
|
||||||
const [rooms, setRooms] = useState<RoomDto[]>([]);
|
const [rooms, setRooms] = useState<RoomDto[]>([]);
|
||||||
|
|
@ -170,6 +173,8 @@ export default function ProjectDetailPage() {
|
||||||
setEditingBoardSimultaneityFactor,
|
setEditingBoardSimultaneityFactor,
|
||||||
] = useState("1");
|
] = useState("1");
|
||||||
const [editingBoardCopyName, setEditingBoardCopyName] = useState("");
|
const [editingBoardCopyName, setEditingBoardCopyName] = useState("");
|
||||||
|
const [editingFeedingBoardId, setEditingFeedingBoardId] = useState("");
|
||||||
|
const [editingFeedingCircuitId, setEditingFeedingCircuitId] = useState("");
|
||||||
const [editingBoardInfo, setEditingBoardInfo] =
|
const [editingBoardInfo, setEditingBoardInfo] =
|
||||||
useState<DistributionBoardInfoDraft>(
|
useState<DistributionBoardInfoDraft>(
|
||||||
toDistributionBoardInfoDraft({
|
toDistributionBoardInfoDraft({
|
||||||
|
|
@ -209,6 +214,7 @@ export default function ProjectDetailPage() {
|
||||||
getProject(projectId),
|
getProject(projectId),
|
||||||
listDistributionBoards(projectId),
|
listDistributionBoards(projectId),
|
||||||
listCircuitLists(projectId),
|
listCircuitLists(projectId),
|
||||||
|
getProjectCircuitTrees(projectId),
|
||||||
listBuildings(projectId),
|
listBuildings(projectId),
|
||||||
listFloors(projectId),
|
listFloors(projectId),
|
||||||
listRooms(projectId),
|
listRooms(projectId),
|
||||||
|
|
@ -219,6 +225,7 @@ export default function ProjectDetailPage() {
|
||||||
currentProject,
|
currentProject,
|
||||||
distributionBoards,
|
distributionBoards,
|
||||||
loadedCircuitLists,
|
loadedCircuitLists,
|
||||||
|
loadedCircuitTrees,
|
||||||
loadedBuildings,
|
loadedBuildings,
|
||||||
loadedFloors,
|
loadedFloors,
|
||||||
loadedRooms,
|
loadedRooms,
|
||||||
|
|
@ -228,6 +235,7 @@ export default function ProjectDetailPage() {
|
||||||
setProject(currentProject);
|
setProject(currentProject);
|
||||||
setBoards(distributionBoards);
|
setBoards(distributionBoards);
|
||||||
setCircuitLists(loadedCircuitLists);
|
setCircuitLists(loadedCircuitLists);
|
||||||
|
setCircuitTrees(loadedCircuitTrees.trees);
|
||||||
setBuildings(loadedBuildings);
|
setBuildings(loadedBuildings);
|
||||||
setFloors(loadedFloors);
|
setFloors(loadedFloors);
|
||||||
setRooms(loadedRooms);
|
setRooms(loadedRooms);
|
||||||
|
|
@ -310,6 +318,48 @@ export default function ProjectDetailPage() {
|
||||||
() => new Map(circuitLists.map((circuitList) => [circuitList.distributionBoardId, circuitList])),
|
() => new Map(circuitLists.map((circuitList) => [circuitList.distributionBoardId, circuitList])),
|
||||||
[circuitLists]
|
[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 =
|
const enabledBoardSupplyTypes =
|
||||||
project?.enabledDistributionBoardSupplyTypes ?? [];
|
project?.enabledDistributionBoardSupplyTypes ?? [];
|
||||||
const usedBoardSupplyTypes = useMemo(
|
const usedBoardSupplyTypes = useMemo(
|
||||||
|
|
@ -329,6 +379,21 @@ export default function ProjectDetailPage() {
|
||||||
[boards]
|
[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) {
|
function applyProjectRevision(currentRevision: number) {
|
||||||
setProject((current) =>
|
setProject((current) =>
|
||||||
current ? { ...current, currentRevision } : current
|
current ? { ...current, currentRevision } : current
|
||||||
|
|
@ -358,6 +423,7 @@ export default function ProjectDetailPage() {
|
||||||
]);
|
]);
|
||||||
setCircuitLists(await listCircuitLists(projectId));
|
setCircuitLists(await listCircuitLists(projectId));
|
||||||
applyProjectRevision(result.history.currentRevision);
|
applyProjectRevision(result.history.currentRevision);
|
||||||
|
setCircuitTrees((await getProjectCircuitTrees(projectId)).trees);
|
||||||
setBoardName("");
|
setBoardName("");
|
||||||
setBoardFloorId("");
|
setBoardFloorId("");
|
||||||
setBoardSupplyType(
|
setBoardSupplyType(
|
||||||
|
|
@ -488,6 +554,8 @@ export default function ProjectDetailPage() {
|
||||||
);
|
);
|
||||||
setEditingBoardCopyName(`${board.name} Kopie`);
|
setEditingBoardCopyName(`${board.name} Kopie`);
|
||||||
setEditingBoardInfo(toDistributionBoardInfoDraft(board));
|
setEditingBoardInfo(toDistributionBoardInfoDraft(board));
|
||||||
|
setEditingFeedingBoardId(board.feedingDistributionBoardId ?? "");
|
||||||
|
setEditingFeedingCircuitId(board.feedingCircuitId ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function openBoardCreator() {
|
function openBoardCreator() {
|
||||||
|
|
@ -523,6 +591,8 @@ export default function ProjectDetailPage() {
|
||||||
floorId: editingBoardFloorId || null,
|
floorId: editingBoardFloorId || null,
|
||||||
supplyType: editingBoardSupplyType,
|
supplyType: editingBoardSupplyType,
|
||||||
simultaneityFactor,
|
simultaneityFactor,
|
||||||
|
feedingDistributionBoardId: editingFeedingBoardId || null,
|
||||||
|
feedingCircuitId: editingFeedingCircuitId || null,
|
||||||
...fromDistributionBoardInfoDraft(editingBoardInfo),
|
...fromDistributionBoardInfoDraft(editingBoardInfo),
|
||||||
},
|
},
|
||||||
project.currentRevision
|
project.currentRevision
|
||||||
|
|
@ -535,6 +605,7 @@ export default function ProjectDetailPage() {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
applyProjectRevision(result.history.currentRevision);
|
applyProjectRevision(result.history.currentRevision);
|
||||||
|
setCircuitTrees((await getProjectCircuitTrees(projectId)).trees);
|
||||||
setEditingBoard(null);
|
setEditingBoard(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(
|
setError(
|
||||||
|
|
@ -590,6 +661,7 @@ export default function ProjectDetailPage() {
|
||||||
result.circuitList,
|
result.circuitList,
|
||||||
]);
|
]);
|
||||||
applyProjectRevision(result.history.currentRevision);
|
applyProjectRevision(result.history.currentRevision);
|
||||||
|
setCircuitTrees((await getProjectCircuitTrees(projectId)).trees);
|
||||||
setEditingBoard(null);
|
setEditingBoard(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(
|
setError(
|
||||||
|
|
@ -633,6 +705,7 @@ export default function ProjectDetailPage() {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
applyProjectRevision(result.history.currentRevision);
|
applyProjectRevision(result.history.currentRevision);
|
||||||
|
setCircuitTrees((await getProjectCircuitTrees(projectId)).trees);
|
||||||
setEditingBoard(null);
|
setEditingBoard(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(
|
setError(
|
||||||
|
|
@ -989,6 +1062,7 @@ export default function ProjectDetailPage() {
|
||||||
<th>Örtlichkeit</th>
|
<th>Örtlichkeit</th>
|
||||||
<th>Etage</th>
|
<th>Etage</th>
|
||||||
<th>Netzart</th>
|
<th>Netzart</th>
|
||||||
|
<th>Gespeist von</th>
|
||||||
<th>GZF</th>
|
<th>GZF</th>
|
||||||
<th className="text-end">Aktionen</th>
|
<th className="text-end">Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -1015,6 +1089,7 @@ export default function ProjectDetailPage() {
|
||||||
]
|
]
|
||||||
: "Nicht festgelegt"}
|
: "Nicht festgelegt"}
|
||||||
</td>
|
</td>
|
||||||
|
<td>{formatBoardFeedingReference(board)}</td>
|
||||||
<td>
|
<td>
|
||||||
{board.simultaneityFactor.toLocaleString(
|
{board.simultaneityFactor.toLocaleString(
|
||||||
"de-DE",
|
"de-DE",
|
||||||
|
|
@ -1392,7 +1467,7 @@ export default function ProjectDetailPage() {
|
||||||
|
|
||||||
{editingBoard ? (
|
{editingBoard ? (
|
||||||
<FormModal
|
<FormModal
|
||||||
description={`${editingBoard.name}: Etagenzuordnung, Netzart und Gleichzeitigkeitsfaktor ändern.`}
|
description={`${editingBoard.name}: Etagenzuordnung, Netzart, Einspeisung und Gleichzeitigkeitsfaktor ändern.`}
|
||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
onClose={() => setEditingBoard(null)}
|
onClose={() => setEditingBoard(null)}
|
||||||
onSubmit={handleUpdateBoard}
|
onSubmit={handleUpdateBoard}
|
||||||
|
|
@ -1479,6 +1554,91 @@ export default function ProjectDetailPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<div className="mt-4">
|
||||||
<DistributionBoardInfoCard
|
<DistributionBoardInfoCard
|
||||||
draft={editingBoardInfo}
|
draft={editingBoardInfo}
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ export interface CircuitTreeDownstreamBoardLoad {
|
||||||
|
|
||||||
export interface CircuitTreeFeedingReference {
|
export interface CircuitTreeFeedingReference {
|
||||||
distributionBoardId: string;
|
distributionBoardId: string;
|
||||||
|
circuitListId: string;
|
||||||
distributionBoardName: string;
|
distributionBoardName: string;
|
||||||
distributionBoardNumber?: string;
|
distributionBoardNumber?: string;
|
||||||
circuitId?: string;
|
circuitId?: string;
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ export function deriveCircuitPhase(
|
||||||
* Rows without cos φ count as 1; a circuit without load yields 1.
|
* Rows without cos φ count as 1; a circuit without load yields 1.
|
||||||
*/
|
*/
|
||||||
export function deriveCircuitCosPhi(circuit: CircuitTreeCircuitDto): number {
|
export function deriveCircuitCosPhi(circuit: CircuitTreeCircuitDto): number {
|
||||||
|
if (Number.isFinite(circuit.circuitCosPhi) && circuit.circuitCosPhi > 0) {
|
||||||
|
return Math.round(circuit.circuitCosPhi * 100) / 100;
|
||||||
|
}
|
||||||
let activePowerKw = 0;
|
let activePowerKw = 0;
|
||||||
let apparentPowerKva = 0;
|
let apparentPowerKva = 0;
|
||||||
for (const row of circuit.deviceRows) {
|
for (const row of circuit.deviceRows) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
import { DragEvent, KeyboardEvent, useEffect, useMemo, useRef, useState } from "react";
|
import { DragEvent, KeyboardEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ArrowDown,
|
ArrowDown,
|
||||||
|
|
@ -3293,6 +3294,23 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
threePhaseVoltageV={data.threePhaseVoltageV}
|
threePhaseVoltageV={data.threePhaseVoltageV}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{data.feedingReference ? (
|
||||||
|
<div className="alert alert-info py-2" role="status">
|
||||||
|
Gespeist von{" "}
|
||||||
|
<Link
|
||||||
|
href={`/projects/${projectId}/circuit-lists/${
|
||||||
|
data.feedingReference.circuitListId
|
||||||
|
}/tree-edit`}
|
||||||
|
>
|
||||||
|
{data.feedingReference.distributionBoardNumber ??
|
||||||
|
data.feedingReference.distributionBoardName}
|
||||||
|
{data.feedingReference.circuitEquipmentIdentifier ?? ""}
|
||||||
|
</Link>
|
||||||
|
{data.feedingReference.circuitId
|
||||||
|
? null
|
||||||
|
: " (speisender Stromkreis noch nicht festgelegt)"}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div className="editor-toolbar">
|
<div className="editor-toolbar">
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={!historyState || historyState.undoDepth === 0 || historyBusy || isSaving}
|
disabled={!historyState || historyState.undoDepth === 0 || historyBusy || isSaving}
|
||||||
|
|
@ -4062,6 +4080,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
? "summary-row"
|
? "summary-row"
|
||||||
: row.rowType === "deviceRow"
|
: row.rowType === "deviceRow"
|
||||||
? "device-row"
|
? "device-row"
|
||||||
|
: row.rowType === "downstreamBoardLoad"
|
||||||
|
? "downstream-board-load-row"
|
||||||
: row.rowType === "reserveCircuit"
|
: row.rowType === "reserveCircuit"
|
||||||
? "empty-circuit-row"
|
? "empty-circuit-row"
|
||||||
: row.rowType === "placeholder"
|
: row.rowType === "placeholder"
|
||||||
|
|
@ -4667,7 +4687,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{row.circuit && row.rowType !== "deviceRow" ? (
|
{row.circuit &&
|
||||||
|
(row.rowType === "circuitCompact" ||
|
||||||
|
row.rowType === "circuitSummary" ||
|
||||||
|
row.rowType === "reserveCircuit") ? (
|
||||||
<>
|
<>
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={isSaving}
|
disabled={isSaving}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export type DistributionBoardInfoDraft = Record<DistributionBoardInfoField, stri
|
||||||
const placeholders: Record<DistributionBoardInfoField, string> = {
|
const placeholders: Record<DistributionBoardInfoField, string> = {
|
||||||
boardNumber: "z. B. UV 1.1",
|
boardNumber: "z. B. UV 1.1",
|
||||||
location: "z. B. Raum 003 Technik",
|
location: "z. B. Raum 003 Technik",
|
||||||
fedFrom: "z. B. HV 0.1 oder HAK / Zähleranlage",
|
fedFrom: "z. B. HAK, Zähleranlage, Trafo oder NEA",
|
||||||
remark: "",
|
remark: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -98,8 +98,8 @@ export function DistributionBoardInfoCard({
|
||||||
)}
|
)}
|
||||||
{field === "fedFrom" ? (
|
{field === "fedFrom" ? (
|
||||||
<div className="form-text">
|
<div className="form-text">
|
||||||
Vorerst Freitext. Die logische Verknüpfung mit dem
|
Für externe Einspeisungen oder ergänzende Hinweise. Eine
|
||||||
speisenden Verteiler folgt.
|
speisende Projektverteilung wird oben logisch verknüpft.
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -494,6 +494,7 @@ export interface CircuitTreeDownstreamBoardLoadDto {
|
||||||
|
|
||||||
export interface CircuitTreeFeedingReferenceDto {
|
export interface CircuitTreeFeedingReferenceDto {
|
||||||
distributionBoardId: string;
|
distributionBoardId: string;
|
||||||
|
circuitListId: string;
|
||||||
distributionBoardName: string;
|
distributionBoardName: string;
|
||||||
distributionBoardNumber?: string;
|
distributionBoardNumber?: string;
|
||||||
circuitId?: string;
|
circuitId?: string;
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ export type RowType =
|
||||||
| "circuitCompact"
|
| "circuitCompact"
|
||||||
| "circuitSummary"
|
| "circuitSummary"
|
||||||
| "deviceRow"
|
| "deviceRow"
|
||||||
|
| "downstreamBoardLoad"
|
||||||
| "reserveCircuit"
|
| "reserveCircuit"
|
||||||
| "placeholder"
|
| "placeholder"
|
||||||
| "footerComponent";
|
| "footerComponent";
|
||||||
|
|
@ -614,6 +615,7 @@ export function getCellKind(rowType: RowType, key: CellKey): CellKind {
|
||||||
return "readonly";
|
return "readonly";
|
||||||
}
|
}
|
||||||
if (rowType === "deviceRow") return deviceFieldKeys.has(key) ? "deviceField" : "readonly";
|
if (rowType === "deviceRow") return deviceFieldKeys.has(key) ? "deviceField" : "readonly";
|
||||||
|
if (rowType === "downstreamBoardLoad") return "readonly";
|
||||||
if (rowType === "circuitSummary") return circuitFieldKeys.has(key) ? "circuitField" : "readonly";
|
if (rowType === "circuitSummary") return circuitFieldKeys.has(key) ? "circuitField" : "readonly";
|
||||||
if (rowType === "reserveCircuit" || rowType === "circuitCompact") {
|
if (rowType === "reserveCircuit" || rowType === "circuitCompact") {
|
||||||
if (deviceFieldKeys.has(key)) return "deviceField";
|
if (deviceFieldKeys.has(key)) return "deviceField";
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import type {
|
||||||
CircuitTreeCircuitDto,
|
CircuitTreeCircuitDto,
|
||||||
CircuitTreeComponentDto,
|
CircuitTreeComponentDto,
|
||||||
CircuitTreeDeviceRowDto,
|
CircuitTreeDeviceRowDto,
|
||||||
|
CircuitTreeDownstreamBoardLoadDto,
|
||||||
CircuitTreeSectionDto,
|
CircuitTreeSectionDto,
|
||||||
} from "../types.js";
|
} from "../types.js";
|
||||||
import { buildCircuitStructureProjection } from "./circuit-structure-projection";
|
import { buildCircuitStructureProjection } from "./circuit-structure-projection";
|
||||||
|
|
@ -45,6 +46,7 @@ export interface VisibleGridRow {
|
||||||
circuit?: CircuitTreeCircuitDto;
|
circuit?: CircuitTreeCircuitDto;
|
||||||
device?: CircuitTreeDeviceRowDto;
|
device?: CircuitTreeDeviceRowDto;
|
||||||
component?: CircuitTreeComponentDto;
|
component?: CircuitTreeComponentDto;
|
||||||
|
downstreamBoardLoad?: CircuitTreeDownstreamBoardLoadDto;
|
||||||
cells: VisibleGridCell[];
|
cells: VisibleGridCell[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,13 +128,16 @@ function makeVisibleGridRow(
|
||||||
rowType: RowType,
|
rowType: RowType,
|
||||||
sectionId: string,
|
sectionId: string,
|
||||||
circuit?: CircuitTreeCircuitDto,
|
circuit?: CircuitTreeCircuitDto,
|
||||||
device?: CircuitTreeDeviceRowDto
|
device?: CircuitTreeDeviceRowDto,
|
||||||
|
downstreamBoardLoad?: CircuitTreeDownstreamBoardLoadDto
|
||||||
): VisibleGridRow {
|
): VisibleGridRow {
|
||||||
const rowKey =
|
const rowKey =
|
||||||
rowType === "placeholder"
|
rowType === "placeholder"
|
||||||
? `placeholder:${sectionId}`
|
? `placeholder:${sectionId}`
|
||||||
: rowType === "deviceRow" && device
|
: rowType === "deviceRow" && device
|
||||||
? `device:${device.id}`
|
? `device:${device.id}`
|
||||||
|
: rowType === "downstreamBoardLoad" && downstreamBoardLoad
|
||||||
|
? `downstream-board:${circuit?.id}:${downstreamBoardLoad.distributionBoardId}`
|
||||||
: `${rowType}:${circuit?.id ?? sectionId}`;
|
: `${rowType}:${circuit?.id ?? sectionId}`;
|
||||||
|
|
||||||
const cells = allColumns.map((column): VisibleGridCell => {
|
const cells = allColumns.map((column): VisibleGridCell => {
|
||||||
|
|
@ -140,7 +145,17 @@ function makeVisibleGridRow(
|
||||||
const editable = kind === "circuitField" || kind === "deviceField";
|
const editable = kind === "circuitField" || kind === "deviceField";
|
||||||
let value: GridValue;
|
let value: GridValue;
|
||||||
|
|
||||||
if (rowType === "placeholder") {
|
if (rowType === "downstreamBoardLoad" && downstreamBoardLoad) {
|
||||||
|
value = column.key === "displayName"
|
||||||
|
? `Unterverteilung ${downstreamBoardLoad.boardNumber ?? downstreamBoardLoad.name}`
|
||||||
|
: column.key === "rowTotalPower"
|
||||||
|
? downstreamBoardLoad.activePowerKw
|
||||||
|
: column.key === "cosPhi"
|
||||||
|
? downstreamBoardLoad.cosPhi
|
||||||
|
: column.key === "remark"
|
||||||
|
? "Automatisch aus der verknüpften Verteilung"
|
||||||
|
: undefined;
|
||||||
|
} else if (rowType === "placeholder") {
|
||||||
value = column.key === "equipmentIdentifier" ? "-frei-" : undefined;
|
value = column.key === "equipmentIdentifier" ? "-frei-" : undefined;
|
||||||
} else if (kind === "deviceField" && device) {
|
} else if (kind === "deviceField" && device) {
|
||||||
value = getDeviceValue(device, column.key);
|
value = getDeviceValue(device, column.key);
|
||||||
|
|
@ -166,7 +181,15 @@ function makeVisibleGridRow(
|
||||||
return { cellKey: column.key, editable, kind, value };
|
return { cellKey: column.key, editable, kind, value };
|
||||||
});
|
});
|
||||||
|
|
||||||
return { rowKey, rowType, sectionId, circuit, device, cells };
|
return {
|
||||||
|
rowKey,
|
||||||
|
rowType,
|
||||||
|
sectionId,
|
||||||
|
circuit,
|
||||||
|
device,
|
||||||
|
downstreamBoardLoad,
|
||||||
|
cells,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildVisibleGridRowsWithStructure(
|
export function buildVisibleGridRowsWithStructure(
|
||||||
|
|
@ -221,15 +244,27 @@ export function buildVisibleGridRowsWithStructure(
|
||||||
}
|
}
|
||||||
if (row.rowType === "circuitBlock") {
|
if (row.rowType === "circuitBlock") {
|
||||||
const circuit = row.circuit;
|
const circuit = row.circuit;
|
||||||
if (circuit.deviceRows.length === 0) {
|
const downstreamLoads = circuit.downstreamBoardLoads ?? [];
|
||||||
|
if (circuit.deviceRows.length === 0 && downstreamLoads.length === 0) {
|
||||||
rows.push(makeVisibleGridRow("reserveCircuit", section.id, circuit));
|
rows.push(makeVisibleGridRow("reserveCircuit", section.id, circuit));
|
||||||
} else if (circuit.deviceRows.length === 1) {
|
} else if (circuit.deviceRows.length === 1 && downstreamLoads.length === 0) {
|
||||||
rows.push(makeVisibleGridRow("circuitCompact", section.id, circuit, circuit.deviceRows[0]));
|
rows.push(makeVisibleGridRow("circuitCompact", section.id, circuit, circuit.deviceRows[0]));
|
||||||
} else {
|
} else {
|
||||||
rows.push(makeVisibleGridRow("circuitSummary", section.id, circuit));
|
rows.push(makeVisibleGridRow("circuitSummary", section.id, circuit));
|
||||||
for (const device of circuit.deviceRows) {
|
for (const device of circuit.deviceRows) {
|
||||||
rows.push(makeVisibleGridRow("deviceRow", section.id, circuit, device));
|
rows.push(makeVisibleGridRow("deviceRow", section.id, circuit, device));
|
||||||
}
|
}
|
||||||
|
for (const load of downstreamLoads) {
|
||||||
|
rows.push(
|
||||||
|
makeVisibleGridRow(
|
||||||
|
"downstreamBoardLoad",
|
||||||
|
section.id,
|
||||||
|
circuit,
|
||||||
|
undefined,
|
||||||
|
load
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,9 @@ export async function buildProjectCircuitTrees(
|
||||||
|
|
||||||
const boardById = new Map(boards.map((board) => [board.id, board]));
|
const boardById = new Map(boards.map((board) => [board.id, board]));
|
||||||
const listById = new Map(lists.map((list) => [list.id, list]));
|
const listById = new Map(lists.map((list) => [list.id, list]));
|
||||||
|
const listByBoardId = new Map(
|
||||||
|
lists.map((list) => [list.distributionBoardId, list])
|
||||||
|
);
|
||||||
const sectionById = new Map(sections.map((section) => [section.id, section]));
|
const sectionById = new Map(sections.map((section) => [section.id, section]));
|
||||||
const circuitById = new Map(circuits.map((circuit) => [circuit.id, circuit]));
|
const circuitById = new Map(circuits.map((circuit) => [circuit.id, circuit]));
|
||||||
const rowsByCircuitId = groupBy(rows, (row) => row.circuitId);
|
const rowsByCircuitId = groupBy(rows, (row) => row.circuitId);
|
||||||
|
|
@ -158,6 +161,7 @@ export async function buildProjectCircuitTrees(
|
||||||
feedingReference: resolveFeedingReference(
|
feedingReference: resolveFeedingReference(
|
||||||
board,
|
board,
|
||||||
boardById,
|
boardById,
|
||||||
|
listByBoardId,
|
||||||
circuitById,
|
circuitById,
|
||||||
sectionById
|
sectionById
|
||||||
),
|
),
|
||||||
|
|
@ -252,7 +256,9 @@ export async function buildProjectCircuitTrees(
|
||||||
voltage: circuit.voltage ?? undefined,
|
voltage: circuit.voltage ?? undefined,
|
||||||
controlRequirement: circuit.controlRequirement ?? undefined,
|
controlRequirement: circuit.controlRequirement ?? undefined,
|
||||||
status: circuit.status ?? undefined,
|
status: circuit.status ?? undefined,
|
||||||
isReserve: Boolean(circuit.isReserve),
|
isReserve:
|
||||||
|
Boolean(circuit.isReserve) &&
|
||||||
|
circuitLoad.downstreamBoards.length === 0,
|
||||||
remark: circuit.remark ?? undefined,
|
remark: circuit.remark ?? undefined,
|
||||||
circuitTotalPower: circuitLoad.activePowerKw,
|
circuitTotalPower: circuitLoad.activePowerKw,
|
||||||
circuitApparentPower: circuitLoad.apparentPowerKva,
|
circuitApparentPower: circuitLoad.apparentPowerKva,
|
||||||
|
|
@ -292,17 +298,21 @@ export async function buildProjectCircuitTrees(
|
||||||
function resolveFeedingReference(
|
function resolveFeedingReference(
|
||||||
board: Awaited<ReturnType<typeof distributionBoardRepository.listByProject>>[number],
|
board: Awaited<ReturnType<typeof distributionBoardRepository.listByProject>>[number],
|
||||||
boardById: Map<string, Awaited<ReturnType<typeof distributionBoardRepository.listByProject>>[number]>,
|
boardById: Map<string, Awaited<ReturnType<typeof distributionBoardRepository.listByProject>>[number]>,
|
||||||
|
listByBoardId: Map<string, Awaited<ReturnType<typeof circuitListRepository.listByProject>>[number]>,
|
||||||
circuitById: Map<string, Awaited<ReturnType<typeof circuitRepository.listByCircuitLists>>[number]>,
|
circuitById: Map<string, Awaited<ReturnType<typeof circuitRepository.listByCircuitLists>>[number]>,
|
||||||
sectionById: Map<string, Awaited<ReturnType<typeof circuitSectionRepository.listByCircuitLists>>[number]>
|
sectionById: Map<string, Awaited<ReturnType<typeof circuitSectionRepository.listByCircuitLists>>[number]>
|
||||||
) {
|
) {
|
||||||
if (board.feedingDistributionBoardId === null) return undefined;
|
if (board.feedingDistributionBoardId === null) return undefined;
|
||||||
const feedingBoard = boardById.get(board.feedingDistributionBoardId);
|
const feedingBoard = boardById.get(board.feedingDistributionBoardId);
|
||||||
if (!feedingBoard) throw new Error("Feeding distribution board is missing.");
|
if (!feedingBoard) throw new Error("Feeding distribution board is missing.");
|
||||||
|
const feedingList = listByBoardId.get(feedingBoard.id);
|
||||||
|
if (!feedingList) throw new Error("Feeding circuit list is missing.");
|
||||||
const feedingCircuit = board.feedingCircuitId === null
|
const feedingCircuit = board.feedingCircuitId === null
|
||||||
? undefined
|
? undefined
|
||||||
: circuitById.get(board.feedingCircuitId);
|
: circuitById.get(board.feedingCircuitId);
|
||||||
return {
|
return {
|
||||||
distributionBoardId: feedingBoard.id,
|
distributionBoardId: feedingBoard.id,
|
||||||
|
circuitListId: feedingList.id,
|
||||||
distributionBoardName: feedingBoard.name,
|
distributionBoardName: feedingBoard.name,
|
||||||
distributionBoardNumber: feedingBoard.boardNumber ?? undefined,
|
distributionBoardNumber: feedingBoard.boardNumber ?? undefined,
|
||||||
circuitId: feedingCircuit?.id,
|
circuitId: feedingCircuit?.id,
|
||||||
|
|
|
||||||
|
|
@ -267,6 +267,15 @@ describe("cable sizing defaults from the circuit", () => {
|
||||||
sortOrder: 1,
|
sortOrder: 1,
|
||||||
isReserve: rows.length === 0,
|
isReserve: rows.length === 0,
|
||||||
circuitTotalPower: rows.reduce((sum, row) => sum + row.power, 0),
|
circuitTotalPower: rows.reduce((sum, row) => sum + row.power, 0),
|
||||||
|
circuitApparentPower: rows.reduce(
|
||||||
|
(sum, row) => sum + row.power / (row.cosPhi ?? 1),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
circuitCosPhi: rows.length
|
||||||
|
? rows.reduce((sum, row) => sum + row.power, 0) /
|
||||||
|
rows.reduce((sum, row) => sum + row.power / (row.cosPhi ?? 1), 0)
|
||||||
|
: 1,
|
||||||
|
downstreamBoardLoads: [],
|
||||||
deviceRows: rows.map((row, index) => ({
|
deviceRows: rows.map((row, index) => ({
|
||||||
id: `row-${index}`,
|
id: `row-${index}`,
|
||||||
sortOrder: index,
|
sortOrder: index,
|
||||||
|
|
@ -288,6 +297,19 @@ describe("cable sizing defaults from the circuit", () => {
|
||||||
assert.equal(deriveCircuitCosPhi(circuit([])), 1);
|
assert.equal(deriveCircuitCosPhi(circuit([])), 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses the projected cos φ including downstream boards", () => {
|
||||||
|
const feeder = circuit([{ power: 1, cosPhi: 1 }]);
|
||||||
|
feeder.circuitCosPhi = 0.67;
|
||||||
|
feeder.downstreamBoardLoads = [{
|
||||||
|
distributionBoardId: "uv-2",
|
||||||
|
name: "UV 2",
|
||||||
|
activePowerKw: 3,
|
||||||
|
apparentPowerKva: 5,
|
||||||
|
cosPhi: 0.6,
|
||||||
|
}];
|
||||||
|
assert.equal(deriveCircuitCosPhi(feeder), 0.67);
|
||||||
|
});
|
||||||
|
|
||||||
it("derives the phase from the group category or three-phase rows", () => {
|
it("derives the phase from the group category or three-phase rows", () => {
|
||||||
assert.equal(deriveCircuitPhase(circuit([{ power: 1, phaseType: "single_phase" }]), "single_phase"), 1);
|
assert.equal(deriveCircuitPhase(circuit([{ power: 1, phaseType: "single_phase" }]), "single_phase"), 1);
|
||||||
assert.equal(deriveCircuitPhase(circuit([{ power: 1, phaseType: "three_phase" }]), "single_phase"), 3);
|
assert.equal(deriveCircuitPhase(circuit([{ power: 1, phaseType: "three_phase" }]), "single_phase"), 3);
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,10 @@ function makeCircuit(
|
||||||
sortOrder,
|
sortOrder,
|
||||||
isReserve: deviceRows.length === 0,
|
isReserve: deviceRows.length === 0,
|
||||||
circuitTotalPower: deviceRows.reduce((total, row) => total + row.rowTotalPower, 0),
|
circuitTotalPower: deviceRows.reduce((total, row) => total + row.rowTotalPower, 0),
|
||||||
|
circuitApparentPower: deviceRows.reduce((total, row) => total + row.rowTotalPower, 0),
|
||||||
|
circuitCosPhi: 1,
|
||||||
deviceRows,
|
deviceRows,
|
||||||
|
downstreamBoardLoads: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -176,6 +179,44 @@ describe("circuit grid projection", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders linked downstream boards as separate read-only rows", () => {
|
||||||
|
const feeder = {
|
||||||
|
...reserveCircuit,
|
||||||
|
isReserve: false,
|
||||||
|
circuitTotalPower: 4,
|
||||||
|
circuitApparentPower: 5,
|
||||||
|
circuitCosPhi: 0.8,
|
||||||
|
downstreamBoardLoads: [{
|
||||||
|
distributionBoardId: "board-child",
|
||||||
|
name: "Unterverteilung",
|
||||||
|
boardNumber: "UV-02",
|
||||||
|
activePowerKw: 4,
|
||||||
|
apparentPowerKva: 5,
|
||||||
|
cosPhi: 0.8,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
const rows = buildVisibleGridRowsWithStructure(
|
||||||
|
[{ ...sections[0], circuits: [feeder] }],
|
||||||
|
{ headerComponents: [], footerComponents: [] }
|
||||||
|
);
|
||||||
|
assert.deepEqual(rows.map((row) => row.rowType), [
|
||||||
|
"section",
|
||||||
|
"circuitSummary",
|
||||||
|
"downstreamBoardLoad",
|
||||||
|
"placeholder",
|
||||||
|
]);
|
||||||
|
const loadRow = rows[2];
|
||||||
|
assert.equal(
|
||||||
|
loadRow.cells.find((cell) => cell.cellKey === "displayName")?.value,
|
||||||
|
"Unterverteilung UV-02"
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
loadRow.cells.find((cell) => cell.cellKey === "rowTotalPower")?.value,
|
||||||
|
4
|
||||||
|
);
|
||||||
|
assert.equal(loadRow.cells.every((cell) => !cell.editable), true);
|
||||||
|
});
|
||||||
|
|
||||||
it("shows circuit protection once on each visible circuit row", () => {
|
it("shows circuit protection once on each visible circuit row", () => {
|
||||||
const protectedSections = sections.map((section) => ({
|
const protectedSections = sections.map((section) => ({
|
||||||
...section,
|
...section,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue