diff --git a/docs/current-architecture.md b/docs/current-architecture.md
index 55913aa..5bb9ca5 100644
--- a/docs/current-architecture.md
+++ b/docs/current-architecture.md
@@ -172,7 +172,12 @@ Die Projektgerätepalette belegt keine permanente Layoutspalte mehr. Sie wird
Stromkreis-Grid standardmäßig die gesamte verfügbare Breite nutzt. Auswahl,
Schnelleinfügen und die vorhandenen Drag-and-drop-Payloads bleiben im Drawer
unverändert verfügbar; während eines aktiven Projektgeräte-Drags kann er nicht
-geschlossen werden.
+geschlossen werden. Der Drawer liegt am rechten Fensterrand und verdeckt damit
+nicht die führenden BMK- und Anzeigenamenspalten. Das Grid zeigt Geräte- und
+Stromkreisleistungen in einer gemeinsamen Spalte `Gesamtsumme`: Gerätezeilen
+verwenden `rowTotalPower`, Stromkreis-Sammelzeilen `circuitTotalPower`.
+Zahlen werden ausschließlich für die Anzeige deutsch und begrenzt formatiert;
+gespeicherte Werte und Bearbeitungsentwürfe behalten ihre volle Genauigkeit.
`circuit.reorder-section` speichert die erwartete und neue Sortierposition
jedes Stromkreises eines vollständigen Abschnitts. Forward, Undo und Redo
ändern ausschließlich `sortOrder`; Stromkreisblöcke, Gerätezeilen und BMKs
diff --git a/src/app/globals.css b/src/app/globals.css
index 7eb2c4f..c460a0d 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -413,7 +413,8 @@ body {
position: fixed;
z-index: 1040;
top: 5.5rem;
- left: 1rem;
+ right: 1rem;
+ left: auto;
width: min(360px, calc(100vw - 2rem));
max-height: calc(100vh - 7rem);
overflow: auto;
diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx
index 729f4fe..b5f5880 100644
--- a/src/frontend/components/circuit-tree-editor.tsx
+++ b/src/frontend/components/circuit-tree-editor.tsx
@@ -2636,10 +2636,17 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
{device.displayName || device.name}
Name: {device.name}
Phasenart: {formatPhaseTypeLabel(device.phaseType)}
- Anzahl: {device.quantity}
- Leistung/Gerät: {device.powerPerUnit}
- Gleichzeitigkeit: {device.simultaneityFactor}
- Gesamtleistung: {device.totalPower}
+ Anzahl: {formatValue(device.quantity, "quantity")}
+
+ Leistung/Gerät: {formatValue(device.powerPerUnit, "powerPerUnit")} kW
+
+
+ Gleichzeitigkeit:{" "}
+ {formatValue(device.simultaneityFactor, "simultaneityFactor")}
+
+
+ Gesamtleistung: {formatValue(device.totalPower, "rowTotalPower")} kW
+
Kostengruppe: {device.costGroup || "-"}
Kategorie: {device.category || "-"}
@@ -3385,7 +3392,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
}}
/>
) : (
- formatValue(cell.value)
+ formatValue(cell.value, column.key)
)}
);
diff --git a/src/frontend/components/circuit-tree-preview.tsx b/src/frontend/components/circuit-tree-preview.tsx
index f584491..fc5e9ed 100644
--- a/src/frontend/components/circuit-tree-preview.tsx
+++ b/src/frontend/components/circuit-tree-preview.tsx
@@ -4,16 +4,7 @@ import { useEffect, useState } from "react";
import { Fragment } from "react";
import { getCircuitTree } from "../utils/api";
import type { CircuitTreeCircuitDto, CircuitTreeResponseDto } from "../types";
-
-function formatNumber(value: number | undefined, digits = 2) {
- if (value === undefined || Number.isNaN(value)) {
- return "-";
- }
- return new Intl.NumberFormat("de-DE", {
- minimumFractionDigits: digits,
- maximumFractionDigits: digits,
- }).format(value);
-}
+import { formatValue } from "../utils/circuit-grid-model";
function formatPhaseType(value: string | undefined) {
if (value === "three_phase") {
@@ -86,11 +77,10 @@ export function CircuitTreePreview(props: { projectId: string; circuitListId: st
Raumnummer |
Raumname |
Anzahl |
- Leistung / Gerät |
+ Leistung / Gerät [kW] |
Gleichzeitigkeit |
cos φ |
- Zeilensumme |
- Stromkreissumme |
+ Gesamtsumme [kW] |
Schutzart |
Bemessungsstrom |
Charakteristik |
@@ -117,7 +107,7 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
return (
<>
- |
+ |
{section.displayName}
|
@@ -127,14 +117,18 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
| {circuit.equipmentIdentifier} |
{circuit.displayName?.trim() || "Reserve"} |
- - |
- {formatNumber(circuit.circuitTotalPower)} |
+ - |
+
+ {formatValue(circuit.circuitTotalPower, "circuitTotalPower")}
+ |
{circuit.protectionType ?? "-"} |
- {formatNumber(circuit.protectionRatedCurrent)} |
+
+ {formatValue(circuit.protectionRatedCurrent, "protectionRatedCurrent")}
+ |
{circuit.protectionCharacteristic ?? "-"} |
{circuit.cableType ?? "-"} |
{circuit.cableCrossSection ?? "-"} |
- {formatNumber(circuit.cableLength)} |
+ {formatValue(circuit.cableLength, "cableLength")} |
{circuit.remark ?? "-"} |
);
@@ -153,18 +147,21 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
{row.level ?? "-"} |
{row.roomNumberSnapshot ?? "-"} |
{row.roomNameSnapshot ?? "-"} |
- {formatNumber(row.quantity, 0)} |
- {formatNumber(row.powerPerUnit)} |
- {formatNumber(row.simultaneityFactor)} |
- {formatNumber(row.cosPhi)} |
- {formatNumber(row.rowTotalPower)} |
- {formatNumber(circuit.circuitTotalPower)} |
+ {formatValue(row.quantity, "quantity")} |
+ {formatValue(row.powerPerUnit, "powerPerUnit")} |
+
+ {formatValue(row.simultaneityFactor, "simultaneityFactor")}
+ |
+ {formatValue(row.cosPhi, "cosPhi")} |
+ {formatValue(row.rowTotalPower, "rowTotalPower")} |
{circuit.protectionType ?? "-"} |
- {formatNumber(circuit.protectionRatedCurrent)} |
+
+ {formatValue(circuit.protectionRatedCurrent, "protectionRatedCurrent")}
+ |
{circuit.protectionCharacteristic ?? "-"} |
{circuit.cableType ?? "-"} |
{circuit.cableCrossSection ?? "-"} |
- {formatNumber(circuit.cableLength)} |
+ {formatValue(circuit.cableLength, "cableLength")} |
{row.remark ?? circuit.remark ?? "-"} |
);
@@ -175,14 +172,18 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
| {circuit.equipmentIdentifier} |
{renderCircuitSummaryLabel(circuit)} |
- - |
- {formatNumber(circuit.circuitTotalPower)} |
+ - |
+
+ {formatValue(circuit.circuitTotalPower, "circuitTotalPower")}
+ |
{circuit.protectionType ?? "-"} |
- {formatNumber(circuit.protectionRatedCurrent)} |
+
+ {formatValue(circuit.protectionRatedCurrent, "protectionRatedCurrent")}
+ |
{circuit.protectionCharacteristic ?? "-"} |
{circuit.cableType ?? "-"} |
{circuit.cableCrossSection ?? "-"} |
- {formatNumber(circuit.cableLength)} |
+ {formatValue(circuit.cableLength, "cableLength")} |
{circuit.remark ?? "-"} |
{circuit.deviceRows.map((row) => (
@@ -196,12 +197,13 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
{row.level ?? "-"} |
{row.roomNumberSnapshot ?? "-"} |
{row.roomNameSnapshot ?? "-"} |
- {formatNumber(row.quantity, 0)} |
- {formatNumber(row.powerPerUnit)} |
- {formatNumber(row.simultaneityFactor)} |
- {formatNumber(row.cosPhi)} |
- {formatNumber(row.rowTotalPower)} |
- - |
+ {formatValue(row.quantity, "quantity")} |
+ {formatValue(row.powerPerUnit, "powerPerUnit")} |
+
+ {formatValue(row.simultaneityFactor, "simultaneityFactor")}
+ |
+ {formatValue(row.cosPhi, "cosPhi")} |
+ {formatValue(row.rowTotalPower, "rowTotalPower")} |
- |
- |
- |
@@ -216,7 +218,7 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
})}
| -frei- |
- Freie Zeile |
+ Freie Zeile |
>
);
diff --git a/src/frontend/utils/circuit-grid-model.ts b/src/frontend/utils/circuit-grid-model.ts
index 41d0fc2..3173913 100644
--- a/src/frontend/utils/circuit-grid-model.ts
+++ b/src/frontend/utils/circuit-grid-model.ts
@@ -60,10 +60,9 @@ export const allColumns: ColumnDef[] = [
{ key: "equipmentIdentifier", label: "Betriebsmittelkennzeichen", defaultVisible: true, locked: true },
{ key: "displayName", label: "Anzeigename", defaultVisible: true },
{ key: "quantity", label: "Anzahl", numeric: true, defaultVisible: true },
- { key: "powerPerUnit", label: "Leistung / Gerät", numeric: true, defaultVisible: true },
+ { key: "powerPerUnit", label: "Leistung / Gerät [kW]", numeric: true, defaultVisible: true },
{ key: "simultaneityFactor", label: "Gleichzeitigkeit", numeric: true, defaultVisible: true },
- { key: "rowTotalPower", label: "Zeilensumme", numeric: true, defaultVisible: true },
- { key: "circuitTotalPower", label: "Stromkreissumme", numeric: true, defaultVisible: true },
+ { key: "rowTotalPower", label: "Gesamtsumme [kW]", numeric: true, defaultVisible: true },
{ key: "protectionSummary", label: "Schutz", defaultVisible: true },
{ key: "cableSummary", label: "Kabel", defaultVisible: true },
{ key: "roomSummary", label: "Raum", defaultVisible: true },
@@ -169,13 +168,53 @@ const circuitFieldKeys = new Set([
"remark",
]);
-export function formatValue(value: GridValue): string {
+const numberFormatters = new Map();
+
+function formatNumber(value: number, maximumFractionDigits: number): string {
+ let formatter = numberFormatters.get(maximumFractionDigits);
+ if (!formatter) {
+ formatter = new Intl.NumberFormat("de-DE", {
+ maximumFractionDigits,
+ minimumFractionDigits: 0,
+ useGrouping: false,
+ });
+ numberFormatters.set(maximumFractionDigits, formatter);
+ }
+ return formatter.format(value);
+}
+
+function getMaximumFractionDigits(key: CellKey | undefined): number {
+ if (
+ key === "powerPerUnit" ||
+ key === "rowTotalPower" ||
+ key === "circuitTotalPower"
+ ) {
+ return 3;
+ }
+ if (
+ key === "simultaneityFactor" ||
+ key === "cosPhi" ||
+ key === "protectionRatedCurrent" ||
+ key === "cableLength"
+ ) {
+ return 2;
+ }
+ if (key === "voltage") {
+ return 0;
+ }
+ return 3;
+}
+
+export function formatValue(value: GridValue, key?: CellKey): string {
if (value === undefined || value === null || value === "") {
return "-";
}
if (typeof value === "boolean") {
return value ? "Ja" : "Nein";
}
+ if (typeof value === "number") {
+ return formatNumber(value, getMaximumFractionDigits(key));
+ }
return String(value);
}
@@ -317,12 +356,15 @@ export function getCircuitValue(circuit: CircuitTreeCircuitDto, key: CellKey): G
}
}
-export function normalizeFilterValue(value: GridValue): string {
- return formatValue(value);
+export function normalizeFilterValue(key: CellKey, value: GridValue): string {
+ return formatValue(value, key);
}
export function getBlockSortValue(circuit: CircuitTreeCircuitDto, key: CellKey): GridValue {
const firstRow = circuit.deviceRows[0];
+ if (key === "rowTotalPower") {
+ return circuit.circuitTotalPower;
+ }
if (circuitOnlyColumns.has(key)) {
return getCircuitValue(circuit, key);
}
diff --git a/src/frontend/utils/circuit-grid-projection.ts b/src/frontend/utils/circuit-grid-projection.ts
index b3e2dfc..6e845bc 100644
--- a/src/frontend/utils/circuit-grid-projection.ts
+++ b/src/frontend/utils/circuit-grid-projection.ts
@@ -47,16 +47,19 @@ export interface VisibleGridRow {
function getCircuitBlockFilterValues(circuit: CircuitTreeCircuitDto, key: CellKey): Set {
const values = new Set();
+ if (key === "rowTotalPower") {
+ values.add(normalizeFilterValue(key, circuit.circuitTotalPower));
+ }
if (circuitOnlyColumns.has(key) || key === "displayName" || key === "remark") {
- values.add(normalizeFilterValue(getCircuitValue(circuit, key)));
+ values.add(normalizeFilterValue(key, getCircuitValue(circuit, key)));
}
if (!circuitOnlyColumns.has(key)) {
for (const device of circuit.deviceRows) {
- values.add(normalizeFilterValue(getDeviceValue(device, key)));
+ values.add(normalizeFilterValue(key, getDeviceValue(device, key)));
}
}
if (values.size === 0 && deviceFieldKeys.has(key)) {
- values.add(normalizeFilterValue(undefined));
+ values.add(normalizeFilterValue(key, undefined));
}
return values;
}
@@ -140,10 +143,12 @@ function makeVisibleGridRow(
value = getDeviceValue(device, column.key);
} else if (kind === "circuitField" && circuit) {
value = getCircuitValue(circuit, column.key);
- } else if (column.key === "circuitTotalPower" && circuit) {
- value = circuit.circuitTotalPower;
} else if (column.key === "rowTotalPower" && device) {
value = device.rowTotalPower;
+ } else if (column.key === "rowTotalPower" && circuit) {
+ value = circuit.circuitTotalPower;
+ } else if (column.key === "circuitTotalPower" && circuit) {
+ value = circuit.circuitTotalPower;
}
return { cellKey: column.key, editable, kind, value };
diff --git a/tests/circuit-grid-model.test.ts b/tests/circuit-grid-model.test.ts
index 50f2436..7126e24 100644
--- a/tests/circuit-grid-model.test.ts
+++ b/tests/circuit-grid-model.test.ts
@@ -4,6 +4,7 @@ import {
allColumns,
buildCircuitEditPatch,
buildDeviceRowEditPatch,
+ formatValue,
getBlockSortValue,
getCellKind,
getCircuitValue,
@@ -76,6 +77,13 @@ describe("circuit grid model", () => {
assert.equal(getCircuitValue(circuit, "cableSummary"), "NYM-J, 1.5 mm², 20 m");
});
+ it("formats displayed numeric values without changing their stored precision", () => {
+ assert.equal(formatValue(0.123456, "powerPerUnit"), "0,123");
+ assert.equal(formatValue(0.87654, "simultaneityFactor"), "0,88");
+ assert.equal(formatValue(230.4, "voltage"), "230");
+ assert.equal(formatValue(16, "protectionRatedCurrent"), "16");
+ });
+
it("uses circuit display values for block sorting and falls back to the first device", () => {
assert.equal(getBlockSortValue(circuit, "displayName"), "Lighting circuit");
assert.equal(getBlockSortValue({ ...circuit, displayName: undefined }, "displayName"), "Office light");
diff --git a/tests/circuit-grid-projection.test.ts b/tests/circuit-grid-projection.test.ts
index dde8b5d..e656084 100644
--- a/tests/circuit-grid-projection.test.ts
+++ b/tests/circuit-grid-projection.test.ts
@@ -164,6 +164,15 @@ describe("circuit grid projection", () => {
rows.at(-1)?.cells.find((cell) => cell.cellKey === "equipmentIdentifier")?.value,
"-frei-"
);
+ const circuitSummary = rows.find((row) => row.rowType === "circuitSummary");
+ assert.equal(
+ circuitSummary?.cells.find((cell) => cell.cellKey === "rowTotalPower")?.value,
+ multiDeviceCircuit.circuitTotalPower
+ );
+ assert.equal(
+ circuitSummary?.cells.some((cell) => cell.cellKey === "circuitTotalPower"),
+ false
+ );
});
it("collects filter options from both circuit and device values", () => {