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, Stromkreis-Grid standardmäßig die gesamte verfügbare Breite nutzt. Auswahl,
Schnelleinfügen und die vorhandenen Drag-and-drop-Payloads bleiben im Drawer 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 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 `circuit.reorder-section` speichert die erwartete und neue Sortierposition
jedes Stromkreises eines vollständigen Abschnitts. Forward, Undo und Redo jedes Stromkreises eines vollständigen Abschnitts. Forward, Undo und Redo
ändern ausschließlich `sortOrder`; Stromkreisblöcke, Gerätezeilen und BMKs ändern ausschließlich `sortOrder`; Stromkreisblöcke, Gerätezeilen und BMKs
+2 -1
View File
@@ -413,7 +413,8 @@ body {
position: fixed; position: fixed;
z-index: 1040; z-index: 1040;
top: 5.5rem; top: 5.5rem;
left: 1rem; right: 1rem;
left: auto;
width: min(360px, calc(100vw - 2rem)); width: min(360px, calc(100vw - 2rem));
max-height: calc(100vh - 7rem); max-height: calc(100vh - 7rem);
overflow: auto; overflow: auto;
@@ -2636,10 +2636,17 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
<strong>{device.displayName || device.name}</strong> <strong>{device.displayName || device.name}</strong>
<span>Name: {device.name}</span> <span>Name: {device.name}</span>
<span>Phasenart: {formatPhaseTypeLabel(device.phaseType)}</span> <span>Phasenart: {formatPhaseTypeLabel(device.phaseType)}</span>
<span>Anzahl: {device.quantity}</span> <span>Anzahl: {formatValue(device.quantity, "quantity")}</span>
<span>Leistung/Gerät: {device.powerPerUnit}</span> <span>
<span>Gleichzeitigkeit: {device.simultaneityFactor}</span> Leistung/Gerät: {formatValue(device.powerPerUnit, "powerPerUnit")} kW
<span>Gesamtleistung: {device.totalPower}</span> </span>
<span>
Gleichzeitigkeit:{" "}
{formatValue(device.simultaneityFactor, "simultaneityFactor")}
</span>
<span>
Gesamtleistung: {formatValue(device.totalPower, "rowTotalPower")} kW
</span>
<span>Kostengruppe: {device.costGroup || "-"}</span> <span>Kostengruppe: {device.costGroup || "-"}</span>
<span>Kategorie: {device.category || "-"}</span> <span>Kategorie: {device.category || "-"}</span>
</button> </button>
@@ -3385,7 +3392,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
}} }}
/> />
) : ( ) : (
formatValue(cell.value) formatValue(cell.value, column.key)
)} )}
</td> </td>
); );
@@ -4,16 +4,7 @@ import { useEffect, useState } from "react";
import { Fragment } from "react"; import { Fragment } from "react";
import { getCircuitTree } from "../utils/api"; import { getCircuitTree } from "../utils/api";
import type { CircuitTreeCircuitDto, CircuitTreeResponseDto } from "../types"; import type { CircuitTreeCircuitDto, CircuitTreeResponseDto } from "../types";
import { formatValue } from "../utils/circuit-grid-model";
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);
}
function formatPhaseType(value: string | undefined) { function formatPhaseType(value: string | undefined) {
if (value === "three_phase") { if (value === "three_phase") {
@@ -86,11 +77,10 @@ export function CircuitTreePreview(props: { projectId: string; circuitListId: st
<th>Raumnummer</th> <th>Raumnummer</th>
<th>Raumname</th> <th>Raumname</th>
<th className="text-end">Anzahl</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">Gleichzeitigkeit</th>
<th className="text-end">cos φ</th> <th className="text-end">cos φ</th>
<th className="text-end">Zeilensumme</th> <th className="text-end">Gesamtsumme [kW]</th>
<th className="text-end">Stromkreissumme</th>
<th>Schutzart</th> <th>Schutzart</th>
<th className="text-end">Bemessungsstrom</th> <th className="text-end">Bemessungsstrom</th>
<th>Charakteristik</th> <th>Charakteristik</th>
@@ -117,7 +107,7 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
return ( return (
<> <>
<tr className="section-row"> <tr className="section-row">
<td colSpan={22}> <td colSpan={21}>
<strong>{section.displayName}</strong> <strong>{section.displayName}</strong>
</td> </td>
</tr> </tr>
@@ -127,14 +117,18 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
<tr key={circuit.id} className="reserve-row"> <tr key={circuit.id} className="reserve-row">
<td>{circuit.equipmentIdentifier}</td> <td>{circuit.equipmentIdentifier}</td>
<td>{circuit.displayName?.trim() || "Reserve"}</td> <td>{circuit.displayName?.trim() || "Reserve"}</td>
<td colSpan={12}>-</td> <td colSpan={11}>-</td>
<td className="text-end">{formatNumber(circuit.circuitTotalPower)}</td> <td className="text-end">
{formatValue(circuit.circuitTotalPower, "circuitTotalPower")}
</td>
<td>{circuit.protectionType ?? "-"}</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.protectionCharacteristic ?? "-"}</td>
<td>{circuit.cableType ?? "-"}</td> <td>{circuit.cableType ?? "-"}</td>
<td>{circuit.cableCrossSection ?? "-"}</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> <td>{circuit.remark ?? "-"}</td>
</tr> </tr>
); );
@@ -153,18 +147,21 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
<td>{row.level ?? "-"}</td> <td>{row.level ?? "-"}</td>
<td>{row.roomNumberSnapshot ?? "-"}</td> <td>{row.roomNumberSnapshot ?? "-"}</td>
<td>{row.roomNameSnapshot ?? "-"}</td> <td>{row.roomNameSnapshot ?? "-"}</td>
<td className="text-end">{formatNumber(row.quantity, 0)}</td> <td className="text-end">{formatValue(row.quantity, "quantity")}</td>
<td className="text-end">{formatNumber(row.powerPerUnit)}</td> <td className="text-end">{formatValue(row.powerPerUnit, "powerPerUnit")}</td>
<td className="text-end">{formatNumber(row.simultaneityFactor)}</td> <td className="text-end">
<td className="text-end">{formatNumber(row.cosPhi)}</td> {formatValue(row.simultaneityFactor, "simultaneityFactor")}
<td className="text-end">{formatNumber(row.rowTotalPower)}</td> </td>
<td className="text-end">{formatNumber(circuit.circuitTotalPower)}</td> <td className="text-end">{formatValue(row.cosPhi, "cosPhi")}</td>
<td className="text-end">{formatValue(row.rowTotalPower, "rowTotalPower")}</td>
<td>{circuit.protectionType ?? "-"}</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.protectionCharacteristic ?? "-"}</td>
<td>{circuit.cableType ?? "-"}</td> <td>{circuit.cableType ?? "-"}</td>
<td>{circuit.cableCrossSection ?? "-"}</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> <td>{row.remark ?? circuit.remark ?? "-"}</td>
</tr> </tr>
); );
@@ -175,14 +172,18 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
<tr key={`${circuit.id}-summary`} className="summary-row"> <tr key={`${circuit.id}-summary`} className="summary-row">
<td>{circuit.equipmentIdentifier}</td> <td>{circuit.equipmentIdentifier}</td>
<td>{renderCircuitSummaryLabel(circuit)}</td> <td>{renderCircuitSummaryLabel(circuit)}</td>
<td colSpan={12}>-</td> <td colSpan={11}>-</td>
<td className="text-end">{formatNumber(circuit.circuitTotalPower)}</td> <td className="text-end">
{formatValue(circuit.circuitTotalPower, "circuitTotalPower")}
</td>
<td>{circuit.protectionType ?? "-"}</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.protectionCharacteristic ?? "-"}</td>
<td>{circuit.cableType ?? "-"}</td> <td>{circuit.cableType ?? "-"}</td>
<td>{circuit.cableCrossSection ?? "-"}</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> <td>{circuit.remark ?? "-"}</td>
</tr> </tr>
{circuit.deviceRows.map((row) => ( {circuit.deviceRows.map((row) => (
@@ -196,12 +197,13 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
<td>{row.level ?? "-"}</td> <td>{row.level ?? "-"}</td>
<td>{row.roomNumberSnapshot ?? "-"}</td> <td>{row.roomNumberSnapshot ?? "-"}</td>
<td>{row.roomNameSnapshot ?? "-"}</td> <td>{row.roomNameSnapshot ?? "-"}</td>
<td className="text-end">{formatNumber(row.quantity, 0)}</td> <td className="text-end">{formatValue(row.quantity, "quantity")}</td>
<td className="text-end">{formatNumber(row.powerPerUnit)}</td> <td className="text-end">{formatValue(row.powerPerUnit, "powerPerUnit")}</td>
<td className="text-end">{formatNumber(row.simultaneityFactor)}</td> <td className="text-end">
<td className="text-end">{formatNumber(row.cosPhi)}</td> {formatValue(row.simultaneityFactor, "simultaneityFactor")}
<td className="text-end">{formatNumber(row.rowTotalPower)}</td> </td>
<td className="text-end">-</td> <td className="text-end">{formatValue(row.cosPhi, "cosPhi")}</td>
<td className="text-end">{formatValue(row.rowTotalPower, "rowTotalPower")}</td>
<td>-</td> <td>-</td>
<td className="text-end">-</td> <td className="text-end">-</td>
<td>-</td> <td>-</td>
@@ -216,7 +218,7 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
})} })}
<tr className="placeholder-row"> <tr className="placeholder-row">
<td>-frei-</td> <td>-frei-</td>
<td colSpan={21}>Freie Zeile</td> <td colSpan={20}>Freie Zeile</td>
</tr> </tr>
</> </>
); );
+48 -6
View File
@@ -60,10 +60,9 @@ export const allColumns: ColumnDef[] = [
{ key: "equipmentIdentifier", label: "Betriebsmittelkennzeichen", defaultVisible: true, locked: true }, { key: "equipmentIdentifier", label: "Betriebsmittelkennzeichen", defaultVisible: true, locked: true },
{ key: "displayName", label: "Anzeigename", defaultVisible: true }, { key: "displayName", label: "Anzeigename", defaultVisible: true },
{ key: "quantity", label: "Anzahl", numeric: true, 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: "simultaneityFactor", label: "Gleichzeitigkeit", numeric: true, defaultVisible: true },
{ key: "rowTotalPower", label: "Zeilensumme", numeric: true, defaultVisible: true }, { key: "rowTotalPower", label: "Gesamtsumme [kW]", numeric: true, defaultVisible: true },
{ key: "circuitTotalPower", label: "Stromkreissumme", numeric: true, defaultVisible: true },
{ key: "protectionSummary", label: "Schutz", defaultVisible: true }, { key: "protectionSummary", label: "Schutz", defaultVisible: true },
{ key: "cableSummary", label: "Kabel", defaultVisible: true }, { key: "cableSummary", label: "Kabel", defaultVisible: true },
{ key: "roomSummary", label: "Raum", defaultVisible: true }, { key: "roomSummary", label: "Raum", defaultVisible: true },
@@ -169,13 +168,53 @@ const circuitFieldKeys = new Set<CellKey>([
"remark", "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 === "") { if (value === undefined || value === null || value === "") {
return "-"; return "-";
} }
if (typeof value === "boolean") { if (typeof value === "boolean") {
return value ? "Ja" : "Nein"; return value ? "Ja" : "Nein";
} }
if (typeof value === "number") {
return formatNumber(value, getMaximumFractionDigits(key));
}
return String(value); return String(value);
} }
@@ -317,12 +356,15 @@ export function getCircuitValue(circuit: CircuitTreeCircuitDto, key: CellKey): G
} }
} }
export function normalizeFilterValue(value: GridValue): string { export function normalizeFilterValue(key: CellKey, value: GridValue): string {
return formatValue(value); return formatValue(value, key);
} }
export function getBlockSortValue(circuit: CircuitTreeCircuitDto, key: CellKey): GridValue { export function getBlockSortValue(circuit: CircuitTreeCircuitDto, key: CellKey): GridValue {
const firstRow = circuit.deviceRows[0]; const firstRow = circuit.deviceRows[0];
if (key === "rowTotalPower") {
return circuit.circuitTotalPower;
}
if (circuitOnlyColumns.has(key)) { if (circuitOnlyColumns.has(key)) {
return getCircuitValue(circuit, key); return getCircuitValue(circuit, key);
} }
+10 -5
View File
@@ -47,16 +47,19 @@ export interface VisibleGridRow {
function getCircuitBlockFilterValues(circuit: CircuitTreeCircuitDto, key: CellKey): Set<string> { function getCircuitBlockFilterValues(circuit: CircuitTreeCircuitDto, key: CellKey): Set<string> {
const values = new Set<string>(); const values = new Set<string>();
if (key === "rowTotalPower") {
values.add(normalizeFilterValue(key, circuit.circuitTotalPower));
}
if (circuitOnlyColumns.has(key) || key === "displayName" || key === "remark") { 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)) { if (!circuitOnlyColumns.has(key)) {
for (const device of circuit.deviceRows) { 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)) { if (values.size === 0 && deviceFieldKeys.has(key)) {
values.add(normalizeFilterValue(undefined)); values.add(normalizeFilterValue(key, undefined));
} }
return values; return values;
} }
@@ -140,10 +143,12 @@ function makeVisibleGridRow(
value = getDeviceValue(device, column.key); value = getDeviceValue(device, column.key);
} else if (kind === "circuitField" && circuit) { } else if (kind === "circuitField" && circuit) {
value = getCircuitValue(circuit, column.key); value = getCircuitValue(circuit, column.key);
} else if (column.key === "circuitTotalPower" && circuit) {
value = circuit.circuitTotalPower;
} else if (column.key === "rowTotalPower" && device) { } else if (column.key === "rowTotalPower" && device) {
value = device.rowTotalPower; 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 }; return { cellKey: column.key, editable, kind, value };
+8
View File
@@ -4,6 +4,7 @@ import {
allColumns, allColumns,
buildCircuitEditPatch, buildCircuitEditPatch,
buildDeviceRowEditPatch, buildDeviceRowEditPatch,
formatValue,
getBlockSortValue, getBlockSortValue,
getCellKind, getCellKind,
getCircuitValue, getCircuitValue,
@@ -76,6 +77,13 @@ describe("circuit grid model", () => {
assert.equal(getCircuitValue(circuit, "cableSummary"), "NYM-J, 1.5 mm², 20 m"); 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", () => { 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"), "Lighting circuit");
assert.equal(getBlockSortValue({ ...circuit, displayName: undefined }, "displayName"), "Office light"); 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, rows.at(-1)?.cells.find((cell) => cell.cellKey === "equipmentIdentifier")?.value,
"-frei-" "-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", () => { it("collects filter options from both circuit and device values", () => {