Improve editor totals and formatting
This commit is contained in:
@@ -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
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user