forked from jappel/leistungsbilanz-ts
Add distribution power summaries
This commit is contained in:
parent
dcb303284c
commit
9ea081179d
28 changed files with 2668 additions and 77 deletions
|
|
@ -27,7 +27,11 @@ import {
|
|||
deviceFieldKeys,
|
||||
formatPhaseTypeLabel,
|
||||
formatValue,
|
||||
getCircuitSectionLabel,
|
||||
getProjectColumnLayoutStorageKey,
|
||||
isGridEditorControlTarget,
|
||||
normalizeColumnOrder,
|
||||
parseStoredColumnLayout,
|
||||
} from "../utils/circuit-grid-model";
|
||||
import {
|
||||
buildCircuitDeviceRowInsertSnapshot,
|
||||
|
|
@ -131,7 +135,8 @@ type CircuitReorderDropIntent =
|
|||
| { kind: "after-circuit"; sectionId: string; targetCircuitId: string; valid: boolean }
|
||||
| { kind: "section-end"; sectionId: string; valid: boolean };
|
||||
|
||||
const COLUMN_LAYOUT_STORAGE_KEY = "circuitTreeEditor.columnLayout.v1";
|
||||
const LEGACY_COLUMN_LAYOUT_STORAGE_KEY =
|
||||
"circuitTreeEditor.columnLayout.v1";
|
||||
|
||||
function normalizeUiError(err: unknown): string {
|
||||
const message = err instanceof Error ? err.message : "Der Vorgang ist fehlgeschlagen.";
|
||||
|
|
@ -222,6 +227,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
const [filterValueSearch, setFilterValueSearch] = useState("");
|
||||
const [visibleColumnKeys, setVisibleColumnKeys] = useState<CellKey[]>(defaultVisibleColumnKeys);
|
||||
const [columnOrder, setColumnOrder] = useState<CellKey[]>(allColumns.map((column) => column.key));
|
||||
const [
|
||||
loadedColumnLayoutProjectId,
|
||||
setLoadedColumnLayoutProjectId,
|
||||
] = useState<string | null>(null);
|
||||
const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false);
|
||||
const [columnSearch, setColumnSearch] = useState("");
|
||||
const [draggingColumnKey, setDraggingColumnKey] = useState<CellKey | null>(null);
|
||||
|
|
@ -267,13 +276,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
}
|
||||
}
|
||||
|
||||
function normalizeColumnOrder(keys: CellKey[]) {
|
||||
const unique = [...new Set(keys)];
|
||||
const allKeys = allColumns.map((column) => column.key);
|
||||
const merged = [...unique, ...allKeys.filter((key) => !unique.includes(key))];
|
||||
return ["equipmentIdentifier" as CellKey, ...merged.filter((key) => key !== "equipmentIdentifier")];
|
||||
}
|
||||
|
||||
// Initial/identity-change tree load for current route context.
|
||||
useEffect(() => {
|
||||
void loadTree({ showLoading: true });
|
||||
|
|
@ -292,32 +294,48 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
}, [projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoadedColumnLayoutProjectId(null);
|
||||
setColumnOrder(allColumns.map((column) => column.key));
|
||||
setVisibleColumnKeys(defaultVisibleColumnKeys);
|
||||
try {
|
||||
const raw = localStorage.getItem(COLUMN_LAYOUT_STORAGE_KEY);
|
||||
if (!raw) {
|
||||
const parsed = parseStoredColumnLayout(
|
||||
localStorage.getItem(
|
||||
getProjectColumnLayoutStorageKey(projectId)
|
||||
) ??
|
||||
localStorage.getItem(
|
||||
LEGACY_COLUMN_LAYOUT_STORAGE_KEY
|
||||
)
|
||||
);
|
||||
if (!parsed) {
|
||||
return;
|
||||
}
|
||||
const parsed = JSON.parse(raw) as { order?: string[]; visible?: string[] };
|
||||
const validKeys = new Set(allColumns.map((column) => column.key));
|
||||
const parsedOrder = (parsed.order ?? []).filter((key): key is CellKey => validKeys.has(key as CellKey));
|
||||
const parsedVisible = (parsed.visible ?? []).filter((key): key is CellKey => validKeys.has(key as CellKey));
|
||||
if (!parsedOrder.length || !parsedVisible.length || !parsedVisible.includes("equipmentIdentifier")) {
|
||||
return;
|
||||
}
|
||||
setColumnOrder(normalizeColumnOrder(parsedOrder));
|
||||
setVisibleColumnKeys(parsedVisible);
|
||||
setColumnOrder(parsed.order);
|
||||
setVisibleColumnKeys(parsed.visible);
|
||||
} catch {
|
||||
// ignore invalid local storage and use defaults
|
||||
} finally {
|
||||
setLoadedColumnLayoutProjectId(projectId);
|
||||
}
|
||||
}, []);
|
||||
}, [projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loadedColumnLayoutProjectId !== projectId) {
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
order: columnOrder,
|
||||
visible: visibleColumnKeys,
|
||||
};
|
||||
localStorage.setItem(COLUMN_LAYOUT_STORAGE_KEY, JSON.stringify(payload));
|
||||
}, [columnOrder, visibleColumnKeys]);
|
||||
localStorage.setItem(
|
||||
getProjectColumnLayoutStorageKey(projectId),
|
||||
JSON.stringify(payload)
|
||||
);
|
||||
}, [
|
||||
columnOrder,
|
||||
loadedColumnLayoutProjectId,
|
||||
projectId,
|
||||
visibleColumnKeys,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const visible = new Set(visibleColumnKeys);
|
||||
|
|
@ -474,6 +492,51 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
);
|
||||
}
|
||||
|
||||
function renderDistributionPowerSummary() {
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<section
|
||||
className="distribution-power-summary"
|
||||
aria-label="Leistungszusammenfassung des Verteilers"
|
||||
>
|
||||
<div>
|
||||
<span>Gesamtleistung Verteiler</span>
|
||||
<strong>
|
||||
{formatValue(
|
||||
data.distributionBoardTotalPower,
|
||||
"rowTotalPower"
|
||||
)}{" "}
|
||||
kW
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Verteilerweiter Gleichzeitigkeitsfaktor</span>
|
||||
<strong>
|
||||
{formatValue(
|
||||
data.distributionBoardSimultaneityFactor,
|
||||
"simultaneityFactor"
|
||||
)}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>
|
||||
Gesamtleistung Verteiler unter Berücksichtigung des
|
||||
Gleichzeitigkeitsfaktors
|
||||
</span>
|
||||
<strong>
|
||||
{formatValue(
|
||||
data.distributionBoardTotalPowerWithSimultaneityFactor,
|
||||
"rowTotalPower"
|
||||
)}{" "}
|
||||
kW
|
||||
</strong>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function closeColumnSettingsMenu() {
|
||||
setIsColumnMenuOpen(false);
|
||||
setColumnSearch("");
|
||||
|
|
@ -2490,6 +2553,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
{renderColumnSettingsMenu("col-empty")}
|
||||
</div>
|
||||
{renderActiveViewSummary()}
|
||||
{renderDistributionPowerSummary()}
|
||||
{error ? (
|
||||
<div className="notice error editor-error-notice" role="alert">
|
||||
<span>{error}</span>
|
||||
|
|
@ -2598,6 +2662,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
{renderColumnSettingsMenu("col")}
|
||||
</div>
|
||||
{renderActiveViewSummary()}
|
||||
{renderDistributionPowerSummary()}
|
||||
{hasActiveSortOrFilter ? (
|
||||
<div className="notice muted">Sortierung und Filter verändern nur die Ansicht. Die Neunummerierung ist währenddessen deaktiviert.</div>
|
||||
) : null}
|
||||
|
|
@ -2701,14 +2766,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
const valid = !selectedProjectDevice || isProjectDevicePlacementValid(selectedProjectDevice, section);
|
||||
return (
|
||||
<option key={section.id} value={section.id} disabled={!valid}>
|
||||
{section.displayName}{valid ? "" : " (nicht zulässig)"}
|
||||
{getCircuitSectionLabel(section)}
|
||||
{valid ? "" : " (nicht zulässig)"}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</label>
|
||||
{selectedProjectDevice && suggestedSection ? (
|
||||
<p className="notice muted">Vorgeschlagener Bereich: {suggestedSection.displayName}</p>
|
||||
<p className="notice muted">
|
||||
Vorgeschlagener Bereich:{" "}
|
||||
{getCircuitSectionLabel(suggestedSection)}
|
||||
</p>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -2980,7 +3049,19 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
>
|
||||
<td colSpan={visibleColumns.length + 1} className="section-drop-cell">
|
||||
<div className="section-content">
|
||||
<strong>{section.displayName}</strong>
|
||||
<div className="section-title">
|
||||
<strong>
|
||||
{getCircuitSectionLabel(section)}
|
||||
</strong>
|
||||
<span>
|
||||
Gesamtleistung Abschnitt:{" "}
|
||||
{formatValue(
|
||||
section.sectionTotalPower,
|
||||
"rowTotalPower"
|
||||
)}{" "}
|
||||
kW
|
||||
</span>
|
||||
</div>
|
||||
<div className="section-actions">
|
||||
<button type="button" tabIndex={-1} onClick={() => void handleAddReserveCircuit(section.id)}>
|
||||
Stromkreis hinzufügen
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import { useEffect, useState } from "react";
|
|||
import { Fragment } from "react";
|
||||
import { getCircuitTree } from "../utils/api";
|
||||
import type { CircuitTreeCircuitDto, CircuitTreeResponseDto } from "../types";
|
||||
import { formatValue } from "../utils/circuit-grid-model";
|
||||
import {
|
||||
formatValue,
|
||||
getCircuitSectionLabel,
|
||||
} from "../utils/circuit-grid-model";
|
||||
|
||||
function renderCircuitSummaryLabel(circuit: CircuitTreeCircuitDto) {
|
||||
if (circuit.displayName?.trim()) {
|
||||
|
|
@ -98,7 +101,7 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
|
|||
<>
|
||||
<tr className="section-row">
|
||||
<td colSpan={21}>
|
||||
<strong>{section.displayName}</strong>
|
||||
<strong>{getCircuitSectionLabel(section)}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
{section.circuits.map((circuit) => {
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ export interface DistributionBoardDto {
|
|||
name: string;
|
||||
floorId: string | null;
|
||||
supplyType: DistributionBoardSupplyType | null;
|
||||
simultaneityFactor: number;
|
||||
}
|
||||
|
||||
export interface DistributionBoardCommandResultDto
|
||||
|
|
@ -296,6 +297,7 @@ export interface CircuitTreeSectionDto {
|
|||
displayName: string;
|
||||
prefix: string;
|
||||
sortOrder: number;
|
||||
sectionTotalPower: number;
|
||||
circuits: CircuitTreeCircuitDto[];
|
||||
}
|
||||
|
||||
|
|
@ -315,6 +317,9 @@ export interface CircuitTreeResponseDto {
|
|||
currentRevision: number;
|
||||
singlePhaseVoltageV: number;
|
||||
threePhaseVoltageV: number;
|
||||
distributionBoardSimultaneityFactor: number;
|
||||
distributionBoardTotalPower: number;
|
||||
distributionBoardTotalPowerWithSimultaneityFactor: number;
|
||||
sections: CircuitTreeSectionDto[];
|
||||
migrationReport?: CircuitTreeMigrationReportDto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@ export function updateDistributionBoard(
|
|||
input: {
|
||||
floorId: string | null;
|
||||
supplyType: NonNullable<DistributionBoardDto["supplyType"]>;
|
||||
simultaneityFactor: number;
|
||||
},
|
||||
expectedRevision: number
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -124,6 +124,96 @@ export const defaultVisibleColumnKeys = allColumns
|
|||
.filter((column) => column.defaultVisible)
|
||||
.map((column) => column.key);
|
||||
|
||||
const projectColumnLayoutStoragePrefix =
|
||||
"circuitTreeEditor.columnLayout.v2";
|
||||
|
||||
export interface CircuitColumnLayout {
|
||||
order: CellKey[];
|
||||
visible: CellKey[];
|
||||
}
|
||||
|
||||
const circuitSectionLabels: Record<string, string> = {
|
||||
lighting: "Licht",
|
||||
single_phase: "Einphasig",
|
||||
three_phase: "Dreiphasig",
|
||||
unassigned: "Nicht zugeordnet",
|
||||
};
|
||||
|
||||
export function getCircuitSectionLabel(
|
||||
section: { key: string; displayName: string }
|
||||
): string {
|
||||
return circuitSectionLabels[section.key] ?? section.displayName;
|
||||
}
|
||||
|
||||
export function getProjectColumnLayoutStorageKey(
|
||||
projectId: string
|
||||
): string {
|
||||
return `${projectColumnLayoutStoragePrefix}.${projectId}`;
|
||||
}
|
||||
|
||||
export function normalizeColumnOrder(keys: CellKey[]): CellKey[] {
|
||||
const unique = [...new Set(keys)];
|
||||
const allKeys = allColumns.map((column) => column.key);
|
||||
const merged = [
|
||||
...unique,
|
||||
...allKeys.filter((key) => !unique.includes(key)),
|
||||
];
|
||||
return [
|
||||
"equipmentIdentifier",
|
||||
...merged.filter((key) => key !== "equipmentIdentifier"),
|
||||
];
|
||||
}
|
||||
|
||||
export function parseStoredColumnLayout(
|
||||
serialized: string | null
|
||||
): CircuitColumnLayout | null {
|
||||
if (!serialized) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(serialized) as {
|
||||
order?: unknown;
|
||||
visible?: unknown;
|
||||
};
|
||||
if (
|
||||
!Array.isArray(parsed.order) ||
|
||||
!Array.isArray(parsed.visible)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const validKeys = new Set(
|
||||
allColumns.map((column) => column.key)
|
||||
);
|
||||
const order = parsed.order.filter(
|
||||
(key): key is CellKey =>
|
||||
typeof key === "string" &&
|
||||
validKeys.has(key as CellKey)
|
||||
);
|
||||
const visible = [
|
||||
...new Set(
|
||||
parsed.visible.filter(
|
||||
(key): key is CellKey =>
|
||||
typeof key === "string" &&
|
||||
validKeys.has(key as CellKey)
|
||||
)
|
||||
),
|
||||
];
|
||||
if (
|
||||
!order.length ||
|
||||
!visible.length ||
|
||||
!visible.includes("equipmentIdentifier")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
order: normalizeColumnOrder(order),
|
||||
visible,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const deviceOnlyColumns = new Set<CellKey>([
|
||||
"quantity",
|
||||
"powerPerUnit",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue