From 0180f768acf098270a689518b432b9c0204506b9 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Wed, 29 Jul 2026 08:26:03 +0200 Subject: [PATCH] Improve editor totals and formatting --- docs/current-architecture.md | 7 +- src/app/globals.css | 3 +- .../components/circuit-tree-editor.tsx | 17 +++-- .../components/circuit-tree-preview.tsx | 76 ++++++++++--------- src/frontend/utils/circuit-grid-model.ts | 54 +++++++++++-- src/frontend/utils/circuit-grid-projection.ts | 15 ++-- tests/circuit-grid-model.test.ts | 8 ++ tests/circuit-grid-projection.test.ts | 9 +++ 8 files changed, 134 insertions(+), 55 deletions(-) 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", () => {