Standardize phase type labels

This commit is contained in:
Julian Appel 2026-07-29 10:46:00 +02:00
parent 084103bf54
commit 096ba8aa1c
23 changed files with 6117 additions and 52 deletions

View file

@ -25,6 +25,7 @@ import {
buildDeviceRowEditPatch,
defaultVisibleColumnKeys,
deviceFieldKeys,
formatPhaseTypeLabel,
formatValue,
} from "../utils/circuit-grid-model";
import {
@ -153,16 +154,6 @@ function normalizeUiError(err: unknown): string {
return message;
}
function formatPhaseTypeLabel(value: string): string {
if (value === "three_phase") {
return "Dreiphasig";
}
if (value === "single_phase") {
return "Einphasig";
}
return value;
}
function isPrintableKey(event: KeyboardEvent<HTMLElement>) {
return event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey;
}
@ -235,7 +226,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
const pendingSelectedDeviceRowIdsAfterReload = useRef<string[] | null>(null);
const pendingSelectedCircuitIdsAfterReload = useRef<string[] | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
const inputRef = useRef<HTMLInputElement | HTMLSelectElement | null>(null);
const focusTokenRef = useRef(1);
const projectRevisionRef = useRef<number | null>(null);
@ -449,7 +440,14 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
</button>
) : null}
{activeColumnFilters.map(({ column, values }) => {
const shownValues = values.slice(0, 2).join(", ");
const shownValues = values
.slice(0, 2)
.map((value) =>
column.key === "phaseType"
? formatPhaseTypeLabel(value)
: value
)
.join(", ");
const valueSummary = values.length > 2 ? `${shownValues} +${values.length - 2}` : shownValues;
return (
<button
@ -1213,9 +1211,12 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
return;
}
inputRef.current.focus();
if (editingCell.mode === "selectExisting") {
if (
inputRef.current instanceof HTMLInputElement &&
editingCell.mode === "selectExisting"
) {
inputRef.current.select();
} else {
} else if (inputRef.current instanceof HTMLInputElement) {
const len = inputRef.current.value.length;
inputRef.current.setSelectionRange(len, len);
}
@ -1245,7 +1246,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
return;
}
// Spreadsheet behavior: typing on a selected cell starts overwrite mode.
const currentDisplay = mode === "replaceWithTypedChar" ? typedChar ?? "" : String(visibleCell.value ?? "");
const currentDisplay =
mode === "replaceWithTypedChar" && cell.cellKey !== "phaseType"
? typedChar ?? ""
: String(visibleCell.value ?? "");
focusTokenRef.current += 1;
setEditingCell({
...cell,
@ -1384,7 +1388,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
}
// Commits the active editor input through command history so edits remain undoable.
async function commitEdit(direction: SaveDirection = "stay") {
async function commitEdit(
direction: SaveDirection = "stay",
draftOverride?: string
) {
if (!editingCell) {
return;
}
@ -1394,10 +1401,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
setEditingCell(null);
return;
}
const draft = draftOverride ?? editingCell.draft;
if (
editingCell.cellKey === "equipmentIdentifier" &&
row.circuit &&
hasEquipmentIdentifierConflict(allCircuits, row.circuit.id, editingCell.draft)
hasEquipmentIdentifierConflict(allCircuits, row.circuit.id, draft)
) {
setError("Dieses Betriebsmittelkennzeichen ist bereits vorhanden. Die Änderung wurde nicht gespeichert.");
return;
@ -1424,7 +1432,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
const command: HistoryCommand = {
label: "Aus freier Zeile erstellen",
redo: async () => {
const selection = await createFromPlaceholder(row.sectionId, editingCell.cellKey, editingCell.draft);
const selection = await createFromPlaceholder(
row.sectionId,
editingCell.cellKey,
draft
);
targetSelection = selection;
return nextSelectionIntent();
},
@ -1435,7 +1447,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
}
if (cell.kind === "circuitField" && row.circuit) {
const next = editingCell.draft;
const next = draft;
const command: HistoryCommand = {
label: "Stromkreisfeld bearbeiten",
redo: async () => {
@ -1449,7 +1461,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
}
if (cell.kind === "deviceField" && row.device) {
const next = editingCell.draft;
const next = draft;
const command: HistoryCommand = {
label: "Gerätefeld bearbeiten",
redo: async () => {
@ -1463,7 +1475,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
}
if (cell.kind === "deviceField" && row.rowType === "reserveCircuit" && row.circuit) {
const next = editingCell.draft;
const next = draft;
const command: HistoryCommand = {
label: "Gerätezeile im Reservestromkreis erstellen",
redo: async () => {
@ -2816,7 +2828,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
</div>
<div className="header-filter-values">
{(distinctValuesByColumn[column.key] ?? [])
.filter((value) => value.toLocaleLowerCase("de-DE").includes(filterValueSearch.trim().toLocaleLowerCase("de-DE")))
.filter((value) =>
(column.key === "phaseType"
? formatPhaseTypeLabel(value)
: value
)
.toLocaleLowerCase("de-DE")
.includes(
filterValueSearch
.trim()
.toLocaleLowerCase("de-DE")
)
)
.map((value) => {
const selected = filterDraftValues.includes(value);
return (
@ -2834,12 +2857,26 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
}
>
<span className="selection-marker" aria-hidden="true">{selected ? "✓" : ""}</span>
<span>{value}</span>
<span>
{column.key === "phaseType"
? formatPhaseTypeLabel(value)
: value}
</span>
</button>
);
})}
{(distinctValuesByColumn[column.key] ?? []).filter((value) =>
value.toLocaleLowerCase("de-DE").includes(filterValueSearch.trim().toLocaleLowerCase("de-DE"))
{(distinctValuesByColumn[column.key] ?? []).filter(
(value) =>
(column.key === "phaseType"
? formatPhaseTypeLabel(value)
: value
)
.toLocaleLowerCase("de-DE")
.includes(
filterValueSearch
.trim()
.toLocaleLowerCase("de-DE")
)
).length === 0 ? (
<div className="header-filter-empty">Keine passenden Werte gefunden.</div>
) : null}
@ -3375,9 +3412,52 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
}
}}
>
{isEditing ? (
{isEditing && column.key === "phaseType" ? (
<select
ref={(element) => {
inputRef.current = element;
}}
value={editingCell.draft}
aria-label="Phasenart auswählen"
onChange={(event) =>
void commitEdit("stay", event.target.value)
}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
void commitEdit("stay");
} else if (event.key === "Escape") {
event.preventDefault();
cancelEdit();
} else if (event.key === "Tab") {
event.preventDefault();
void commitEdit(
event.shiftKey ? "prev" : "next"
);
}
}}
onBlur={() => {
requestAnimationFrame(() => {
const active =
document.activeElement as HTMLElement | null;
if (
!active ||
!containerRef.current?.contains(active)
) {
setEditingCell(null);
focusGridWithoutScroll();
}
});
}}
>
<option value="single_phase">1-phasig</option>
<option value="three_phase">3-phasig</option>
</select>
) : isEditing ? (
<input
ref={inputRef}
ref={(element) => {
inputRef.current = element;
}}
value={editingCell.draft}
aria-invalid={hasDraftIdentifierConflict}
title={hasDraftIdentifierConflict ? "Dieses Betriebsmittelkennzeichen ist bereits vorhanden." : undefined}

View file

@ -6,16 +6,6 @@ import { getCircuitTree } from "../utils/api";
import type { CircuitTreeCircuitDto, CircuitTreeResponseDto } from "../types";
import { formatValue } from "../utils/circuit-grid-model";
function formatPhaseType(value: string | undefined) {
if (value === "three_phase") {
return "Dreiphasig";
}
if (value === "single_phase") {
return "Einphasig";
}
return value ?? "-";
}
function renderCircuitSummaryLabel(circuit: CircuitTreeCircuitDto) {
if (circuit.displayName?.trim()) {
return circuit.displayName;
@ -140,7 +130,7 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
<tr key={circuit.id} className={circuit.isReserve ? "reserve-row" : ""}>
<td>{circuit.equipmentIdentifier}</td>
<td>{row.displayName || row.name}</td>
<td>{formatPhaseType(row.phaseType)}</td>
<td>{formatValue(row.phaseType ?? undefined, "phaseType")}</td>
<td>{row.connectionKind ?? "-"}</td>
<td>{row.costGroup ?? "-"}</td>
<td>{row.category ?? "-"}</td>
@ -190,7 +180,7 @@ function SectionRows(props: { section: CircuitTreeResponseDto["sections"][number
<tr key={row.id} className="device-row">
<td className="text-muted"> </td>
<td className="indented-cell">{row.displayName || row.name}</td>
<td>{formatPhaseType(row.phaseType)}</td>
<td>{formatValue(row.phaseType ?? undefined, "phaseType")}</td>
<td>{row.connectionKind ?? "-"}</td>
<td>{row.costGroup ?? "-"}</td>
<td>{row.category ?? "-"}</td>

View file

@ -187,7 +187,7 @@ export interface CreateGlobalDeviceInput {
quantity: number;
installedPowerPerUnitKw: number;
demandFactor: number;
phaseCount?: 1 | 3;
phaseCount: 1 | 3;
powerFactor?: number;
note?: string;
}

View file

@ -214,9 +214,22 @@ export function formatValue(value: GridValue, key?: CellKey): string {
if (typeof value === "number") {
return formatNumber(value, getMaximumFractionDigits(key));
}
if (key === "phaseType") {
return formatPhaseTypeLabel(String(value));
}
return String(value);
}
export function formatPhaseTypeLabel(value: string): string {
if (value === "single_phase") {
return "1-phasig";
}
if (value === "three_phase") {
return "3-phasig";
}
return value;
}
export function parseNumeric(cellKey: CellKey, draft: string): number | undefined {
const trimmed = draft.trim();
if (trimmed === "") {