Improve editor totals and formatting

This commit is contained in:
2026-07-29 08:26:03 +02:00
parent a04c2c04d4
commit 0180f768ac
8 changed files with 134 additions and 55 deletions
+6 -1
View File
@@ -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
+2 -1
View File
@@ -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;
@@ -2636,10 +2636,17 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
<strong>{device.displayName || device.name}</strong>
<span>Name: {device.name}</span>
<span>Phasenart: {formatPhaseTypeLabel(device.phaseType)}</span>
<span>Anzahl: {device.quantity}</span>
<span>Leistung/Gerät: {device.powerPerUnit}</span>
<span>Gleichzeitigkeit: {device.simultaneityFactor}</span>
<span>Gesamtleistung: {device.totalPower}</span>
<span>Anzahl: {formatValue(device.quantity, "quantity")}</span>
<span>
Leistung/Gerät: {formatValue(device.powerPerUnit, "powerPerUnit")} kW
</span>
<span>
Gleichzeitigkeit:{" "}
{formatValue(device.simultaneityFactor, "simultaneityFactor")}
</span>
<span>
Gesamtleistung: {formatValue(device.totalPower, "rowTotalPower")} kW
</span>
<span>Kostengruppe: {device.costGroup || "-"}</span>
<span>Kategorie: {device.category || "-"}</span>
</button>
@@ -3385,7 +3392,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
}}
/>
) : (
formatValue(cell.value)
formatValue(cell.value, column.key)
)}
</td>
);
@@ -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
<th>Raumnummer</th>
<th>Raumname</th>
<th className="text-end">Anzahl</th>
<th className="text-end">Leistung / Gerät</th>
<th className="text-end">Leistung / Gerät [kW]</th>
<th className="text-end">Gleichzeitigkeit</th>
<th className="text-end">cos φ</th>
<th className="text-end">Zeilensumme</th>
<th className="text-end">Stromkreissumme</th>
<th className="text-end">Gesamtsumme [kW]</th>
<th>Schutzart</th>
<th className="text-end">Bemessungsstrom</th>
<th>Charakteristik</th>
@@ -117,7 +107,7 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
return (
<>
<tr className="section-row">
<td colSpan={22}>
<td colSpan={21}>
<strong>{section.displayName}</strong>
</td>
</tr>
@@ -127,14 +117,18 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
<tr key={circuit.id} className="reserve-row">
<td>{circuit.equipmentIdentifier}</td>
<td>{circuit.displayName?.trim() || "Reserve"}</td>
<td colSpan={12}>-</td>
<td className="text-end">{formatNumber(circuit.circuitTotalPower)}</td>
<td colSpan={11}>-</td>
<td className="text-end">
{formatValue(circuit.circuitTotalPower, "circuitTotalPower")}
</td>
<td>{circuit.protectionType ?? "-"}</td>
<td className="text-end">{formatNumber(circuit.protectionRatedCurrent)}</td>
<td className="text-end">
{formatValue(circuit.protectionRatedCurrent, "protectionRatedCurrent")}
</td>
<td>{circuit.protectionCharacteristic ?? "-"}</td>
<td>{circuit.cableType ?? "-"}</td>
<td>{circuit.cableCrossSection ?? "-"}</td>
<td className="text-end">{formatNumber(circuit.cableLength)}</td>
<td className="text-end">{formatValue(circuit.cableLength, "cableLength")}</td>
<td>{circuit.remark ?? "-"}</td>
</tr>
);
@@ -153,18 +147,21 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
<td>{row.level ?? "-"}</td>
<td>{row.roomNumberSnapshot ?? "-"}</td>
<td>{row.roomNameSnapshot ?? "-"}</td>
<td className="text-end">{formatNumber(row.quantity, 0)}</td>
<td className="text-end">{formatNumber(row.powerPerUnit)}</td>
<td className="text-end">{formatNumber(row.simultaneityFactor)}</td>
<td className="text-end">{formatNumber(row.cosPhi)}</td>
<td className="text-end">{formatNumber(row.rowTotalPower)}</td>
<td className="text-end">{formatNumber(circuit.circuitTotalPower)}</td>
<td className="text-end">{formatValue(row.quantity, "quantity")}</td>
<td className="text-end">{formatValue(row.powerPerUnit, "powerPerUnit")}</td>
<td className="text-end">
{formatValue(row.simultaneityFactor, "simultaneityFactor")}
</td>
<td className="text-end">{formatValue(row.cosPhi, "cosPhi")}</td>
<td className="text-end">{formatValue(row.rowTotalPower, "rowTotalPower")}</td>
<td>{circuit.protectionType ?? "-"}</td>
<td className="text-end">{formatNumber(circuit.protectionRatedCurrent)}</td>
<td className="text-end">
{formatValue(circuit.protectionRatedCurrent, "protectionRatedCurrent")}
</td>
<td>{circuit.protectionCharacteristic ?? "-"}</td>
<td>{circuit.cableType ?? "-"}</td>
<td>{circuit.cableCrossSection ?? "-"}</td>
<td className="text-end">{formatNumber(circuit.cableLength)}</td>
<td className="text-end">{formatValue(circuit.cableLength, "cableLength")}</td>
<td>{row.remark ?? circuit.remark ?? "-"}</td>
</tr>
);
@@ -175,14 +172,18 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
<tr key={`${circuit.id}-summary`} className="summary-row">
<td>{circuit.equipmentIdentifier}</td>
<td>{renderCircuitSummaryLabel(circuit)}</td>
<td colSpan={12}>-</td>
<td className="text-end">{formatNumber(circuit.circuitTotalPower)}</td>
<td colSpan={11}>-</td>
<td className="text-end">
{formatValue(circuit.circuitTotalPower, "circuitTotalPower")}
</td>
<td>{circuit.protectionType ?? "-"}</td>
<td className="text-end">{formatNumber(circuit.protectionRatedCurrent)}</td>
<td className="text-end">
{formatValue(circuit.protectionRatedCurrent, "protectionRatedCurrent")}
</td>
<td>{circuit.protectionCharacteristic ?? "-"}</td>
<td>{circuit.cableType ?? "-"}</td>
<td>{circuit.cableCrossSection ?? "-"}</td>
<td className="text-end">{formatNumber(circuit.cableLength)}</td>
<td className="text-end">{formatValue(circuit.cableLength, "cableLength")}</td>
<td>{circuit.remark ?? "-"}</td>
</tr>
{circuit.deviceRows.map((row) => (
@@ -196,12 +197,13 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
<td>{row.level ?? "-"}</td>
<td>{row.roomNumberSnapshot ?? "-"}</td>
<td>{row.roomNameSnapshot ?? "-"}</td>
<td className="text-end">{formatNumber(row.quantity, 0)}</td>
<td className="text-end">{formatNumber(row.powerPerUnit)}</td>
<td className="text-end">{formatNumber(row.simultaneityFactor)}</td>
<td className="text-end">{formatNumber(row.cosPhi)}</td>
<td className="text-end">{formatNumber(row.rowTotalPower)}</td>
<td className="text-end">-</td>
<td className="text-end">{formatValue(row.quantity, "quantity")}</td>
<td className="text-end">{formatValue(row.powerPerUnit, "powerPerUnit")}</td>
<td className="text-end">
{formatValue(row.simultaneityFactor, "simultaneityFactor")}
</td>
<td className="text-end">{formatValue(row.cosPhi, "cosPhi")}</td>
<td className="text-end">{formatValue(row.rowTotalPower, "rowTotalPower")}</td>
<td>-</td>
<td className="text-end">-</td>
<td>-</td>
@@ -216,7 +218,7 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
})}
<tr className="placeholder-row">
<td>-frei-</td>
<td colSpan={21}>Freie Zeile</td>
<td colSpan={20}>Freie Zeile</td>
</tr>
</>
);
+48 -6
View File
@@ -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<CellKey>([
"remark",
]);
export function formatValue(value: GridValue): string {
const numberFormatters = new Map<number, Intl.NumberFormat>();
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);
}
+10 -5
View File
@@ -47,16 +47,19 @@ export interface VisibleGridRow {
function getCircuitBlockFilterValues(circuit: CircuitTreeCircuitDto, key: CellKey): Set<string> {
const values = new Set<string>();
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 };
+8
View File
@@ -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");
+9
View File
@@ -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", () => {