Standardize phase type labels

This commit is contained in:
2026-07-29 10:46:00 +02:00
parent 084103bf54
commit 096ba8aa1c
23 changed files with 6117 additions and 52 deletions
+3
View File
@@ -37,6 +37,9 @@ The pure grid modules have no React state and are covered by focused unit tests.
- `cosPhi`
- room snapshots
- category/cost group and related row attributes
- canonical `phaseType` values `single_phase` or `three_phase`; the
frontend always presents these as `1-phasig` or `3-phasig` and edits
them through a selection control
- `ProjectDevice`
- Reusable device template entity at project level.
- Can be linked to `CircuitDeviceRow` entries, with copied display values on insert.
+2 -1
View File
@@ -689,7 +689,8 @@ body {
border-color: #c2410c;
}
.tree-grid input {
.tree-grid input,
.tree-grid select {
width: 100%;
min-width: 5rem;
border: 1px solid #9fb6e0;
+28 -5
View File
@@ -193,7 +193,7 @@ export default function ProjectsPage() {
async function handleQuickUpdateGlobalDevice(
device: GlobalDeviceDto,
key: "name" | "displayName" | "category",
key: "name" | "displayName" | "category" | "phaseCount",
value: string
) {
const payload: CreateGlobalDeviceInput = {
@@ -203,7 +203,12 @@ export default function ProjectsPage() {
quantity: device.quantity,
installedPowerPerUnitKw: device.installedPowerPerUnitKw,
demandFactor: device.demandFactor,
phaseCount: device.phaseCount ?? undefined,
phaseCount:
key === "phaseCount"
? value === "3"
? 3
: 1
: device.phaseCount ?? 1,
powerFactor: device.powerFactor ?? undefined,
note: device.note ?? undefined,
};
@@ -416,8 +421,8 @@ export default function ProjectsPage() {
setGlobalDeviceForm((current) => ({ ...current, phaseCount: event.target.value }))
}
>
<option value="1">1-ph</option>
<option value="3">3-ph</option>
<option value="1">1-phasig</option>
<option value="3">3-phasig</option>
</select>
</div>
<div className="col-6 col-md-2">
@@ -437,6 +442,7 @@ export default function ProjectsPage() {
<th>Anzahl</th>
<th>Leistung je Stück [kW]</th>
<th>GZF</th>
<th>Phasenart</th>
<th>Aktion</th>
</tr>
</thead>
@@ -479,6 +485,23 @@ export default function ProjectsPage() {
<td>{device.quantity}</td>
<td>{device.installedPowerPerUnitKw}</td>
<td>{device.demandFactor}</td>
<td>
<select
className="form-select form-select-sm"
aria-label={`Phasenart für ${device.displayName}`}
value={String(device.phaseCount ?? 1)}
onChange={(event) =>
void handleQuickUpdateGlobalDevice(
device,
"phaseCount",
event.target.value
)
}
>
<option value="1">1-phasig</option>
<option value="3">3-phasig</option>
</select>
</td>
<td>
<button
className="btn btn-sm btn-outline-danger"
@@ -492,7 +515,7 @@ export default function ProjectsPage() {
))}
{!globalDevices.length ? (
<tr>
<td colSpan={7} className="text-center text-secondary py-4">
<td colSpan={8} className="text-center text-secondary py-4">
Noch keine globalen Geräte vorhanden.
</td>
</tr>
@@ -0,0 +1,85 @@
UPDATE `project_devices`
SET `phase_type` = CASE
WHEN LOWER(REPLACE(REPLACE(TRIM(`phase_type`), '-', ''), '_', ''))
IN ('1ph', '1phase', '1phasig', 'einphasig', 'singlephase')
THEN 'single_phase'
WHEN LOWER(REPLACE(REPLACE(TRIM(`phase_type`), '-', ''), '_', ''))
IN ('3ph', '3phase', '3phasig', 'dreiphasig', 'threephase')
THEN 'three_phase'
ELSE `phase_type`
END
WHERE LOWER(REPLACE(REPLACE(TRIM(`phase_type`), '-', ''), '_', ''))
IN (
'1ph',
'1phase',
'1phasig',
'einphasig',
'singlephase',
'3ph',
'3phase',
'3phasig',
'dreiphasig',
'threephase'
);
--> statement-breakpoint
UPDATE `circuit_device_rows`
SET `phase_type` = CASE
WHEN LOWER(REPLACE(REPLACE(TRIM(`phase_type`), '-', ''), '_', ''))
IN ('1ph', '1phase', '1phasig', 'einphasig', 'singlephase')
THEN 'single_phase'
WHEN LOWER(REPLACE(REPLACE(TRIM(`phase_type`), '-', ''), '_', ''))
IN ('3ph', '3phase', '3phasig', 'dreiphasig', 'threephase')
THEN 'three_phase'
ELSE `phase_type`
END
WHERE LOWER(REPLACE(REPLACE(TRIM(`phase_type`), '-', ''), '_', ''))
IN (
'1ph',
'1phase',
'1phasig',
'einphasig',
'singlephase',
'3ph',
'3phase',
'3phasig',
'dreiphasig',
'threephase'
);
--> statement-breakpoint
WITH `derived_circuit_voltages` AS (
SELECT
`c`.`id` AS `circuit_id`,
CASE
WHEN `cs`.`key` = 'three_phase' THEN `p`.`three_phase_voltage_v`
WHEN `cs`.`key` IN ('lighting', 'single_phase') THEN `p`.`single_phase_voltage_v`
WHEN EXISTS (
SELECT 1
FROM `circuit_device_rows` AS `three_phase_row`
WHERE `three_phase_row`.`circuit_id` = `c`.`id`
AND `three_phase_row`.`phase_type` = 'three_phase'
) AND NOT EXISTS (
SELECT 1
FROM `circuit_device_rows` AS `single_phase_row`
WHERE `single_phase_row`.`circuit_id` = `c`.`id`
AND `single_phase_row`.`phase_type` = 'single_phase'
) THEN `p`.`three_phase_voltage_v`
ELSE `p`.`single_phase_voltage_v`
END AS `derived_voltage`
FROM `circuits` AS `c`
INNER JOIN `circuit_lists` AS `cl`
ON `cl`.`id` = `c`.`circuit_list_id`
INNER JOIN `projects` AS `p`
ON `p`.`id` = `cl`.`project_id`
INNER JOIN `circuit_sections` AS `cs`
ON `cs`.`id` = `c`.`section_id`
)
UPDATE `circuits`
SET `voltage` = (
SELECT `derived_voltage`
FROM `derived_circuit_voltages`
WHERE `circuit_id` = `circuits`.`id`
)
WHERE `id` IN (
SELECT `circuit_id`
FROM `derived_circuit_voltages`
);
@@ -0,0 +1,75 @@
UPDATE `circuit_device_rows`
SET `phase_type` = COALESCE(
(
SELECT `pd`.`phase_type`
FROM `project_devices` AS `pd`
WHERE `pd`.`id` = `circuit_device_rows`.`linked_project_device_id`
AND `pd`.`phase_type` IN ('single_phase', 'three_phase')
),
(
SELECT CASE
WHEN `cs`.`key` = 'three_phase' THEN 'three_phase'
WHEN `cs`.`key` IN ('lighting', 'single_phase') THEN 'single_phase'
ELSE NULL
END
FROM `circuits` AS `c`
INNER JOIN `circuit_sections` AS `cs`
ON `cs`.`id` = `c`.`section_id`
WHERE `c`.`id` = `circuit_device_rows`.`circuit_id`
)
)
WHERE `phase_type` IS NULL
AND (
EXISTS (
SELECT 1
FROM `project_devices` AS `pd`
WHERE `pd`.`id` = `circuit_device_rows`.`linked_project_device_id`
AND `pd`.`phase_type` IN ('single_phase', 'three_phase')
)
OR EXISTS (
SELECT 1
FROM `circuits` AS `c`
INNER JOIN `circuit_sections` AS `cs`
ON `cs`.`id` = `c`.`section_id`
WHERE `c`.`id` = `circuit_device_rows`.`circuit_id`
AND `cs`.`key` IN ('lighting', 'single_phase', 'three_phase')
)
);
--> statement-breakpoint
WITH `derived_circuit_voltages` AS (
SELECT
`c`.`id` AS `circuit_id`,
CASE
WHEN `cs`.`key` = 'three_phase' THEN `p`.`three_phase_voltage_v`
WHEN `cs`.`key` IN ('lighting', 'single_phase') THEN `p`.`single_phase_voltage_v`
WHEN EXISTS (
SELECT 1
FROM `circuit_device_rows` AS `three_phase_row`
WHERE `three_phase_row`.`circuit_id` = `c`.`id`
AND `three_phase_row`.`phase_type` = 'three_phase'
) AND NOT EXISTS (
SELECT 1
FROM `circuit_device_rows` AS `single_phase_row`
WHERE `single_phase_row`.`circuit_id` = `c`.`id`
AND `single_phase_row`.`phase_type` = 'single_phase'
) THEN `p`.`three_phase_voltage_v`
ELSE `p`.`single_phase_voltage_v`
END AS `derived_voltage`
FROM `circuits` AS `c`
INNER JOIN `circuit_lists` AS `cl`
ON `cl`.`id` = `c`.`circuit_list_id`
INNER JOIN `projects` AS `p`
ON `p`.`id` = `cl`.`project_id`
INNER JOIN `circuit_sections` AS `cs`
ON `cs`.`id` = `c`.`section_id`
)
UPDATE `circuits`
SET `voltage` = (
SELECT `derived_voltage`
FROM `derived_circuit_voltages`
WHERE `circuit_id` = `circuits`.`id`
)
WHERE `id` IN (
SELECT `circuit_id`
FROM `derived_circuit_voltages`
);
@@ -0,0 +1,7 @@
UPDATE `circuit_device_rows`
SET `phase_type` = 'single_phase'
WHERE `phase_type` IS NULL;
--> statement-breakpoint
UPDATE `global_devices`
SET `phase_count` = 1
WHERE `phase_count` IS NULL OR `phase_count` NOT IN (1, 3);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+21
View File
@@ -141,6 +141,27 @@
"when": 1785310907349,
"tag": "0019_normalize_project_voltages",
"breakpoints": true
},
{
"idx": 20,
"version": "6",
"when": 1785314248007,
"tag": "0020_normalize_phase_types",
"breakpoints": true
},
{
"idx": 21,
"version": "6",
"when": 1785314558555,
"tag": "0021_fill_missing_phase_types",
"breakpoints": true
},
{
"idx": 22,
"version": "6",
"when": 1785314626476,
"tag": "0022_default_remaining_phase_types",
"breakpoints": true
}
]
}
@@ -11,6 +11,7 @@ import type {
ExecuteCircuitDeviceRowUpdateCommandInput,
} from "../../domain/ports/circuit-device-row-project-command.store.js";
import { deriveOverriddenFieldsForLocalEdit } from "../../domain/services/project-device-overrides.js";
import { isElectricalPhaseType } from "../../domain/services/project-voltage.service.js";
import type { AppDatabase } from "../database-context.js";
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
import { circuitLists } from "../schema/circuit-lists.js";
@@ -81,6 +82,13 @@ export class CircuitDeviceRowProjectCommandRepository
change.value,
])
) as CircuitDeviceRowPatchInput;
if (
input.source === "user" &&
patch.phaseType !== undefined &&
!isElectricalPhaseType(patch.phaseType)
) {
throw new Error("Device-row phase type is invalid.");
}
this.assertLinkedProjectDevice(tx, input.projectId, patch);
this.assertRoom(tx, input.projectId, patch);
@@ -14,6 +14,7 @@ import type {
ExecuteCircuitDeviceRowStructureCommandInput,
} from "../../domain/ports/circuit-device-row-structure-project-command.store.js";
import type { AppDatabase } from "../database-context.js";
import { isElectricalPhaseType } from "../../domain/services/project-voltage.service.js";
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
import { circuitLists } from "../schema/circuit-lists.js";
import { circuits } from "../schema/circuits.js";
@@ -81,6 +82,9 @@ export class CircuitDeviceRowStructureProjectCommandRepository
"User commands cannot assign legacy consumer ids."
);
}
if (source === "user" && !isElectricalPhaseType(row.phaseType)) {
throw new Error("Device-row phase type is invalid.");
}
this.assertCircuitInProject(database, projectId, row.circuitId);
assertCircuitDeviceRowReferencesInProject(database, projectId, row);
@@ -13,6 +13,7 @@ import type {
CircuitStructureProjectCommandStore,
ExecuteCircuitStructureCommandInput,
} from "../../domain/ports/circuit-structure-project-command.store.js";
import { isElectricalPhaseType } from "../../domain/services/project-voltage.service.js";
import type { AppDatabase } from "../database-context.js";
import { circuitDeviceRows } from "../schema/circuit-device-rows.js";
import { circuitLists } from "../schema/circuit-lists.js";
@@ -136,6 +137,9 @@ export class CircuitStructureProjectCommandRepository
"User commands cannot assign legacy consumer ids."
);
}
if (source === "user" && !isElectricalPhaseType(row.phaseType)) {
throw new Error("Device-row phase type is invalid.");
}
assertCircuitDeviceRowReferencesInProject(
database,
projectId,
@@ -27,7 +27,7 @@ export class GlobalDeviceRepository {
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
demandFactor: input.demandFactor,
voltageV: null,
phaseCount: input.phaseCount ?? null,
phaseCount: input.phaseCount,
powerFactor: input.powerFactor ?? null,
note: input.note ?? null,
});
@@ -46,7 +46,7 @@ export class GlobalDeviceRepository {
installedPowerPerUnitKw: input.installedPowerPerUnitKw,
demandFactor: input.demandFactor,
voltageV: null,
phaseCount: input.phaseCount ?? null,
phaseCount: input.phaseCount,
powerFactor: input.powerFactor ?? null,
note: input.note ?? null,
})
@@ -6,6 +6,7 @@ import {
import {
resolveCircuitPhaseType,
resolveProjectVoltage,
normalizeKnownElectricalPhaseType,
} from "../services/project-voltage.service.js";
export const legacyProjectStateSnapshotSchemaVersion = 1 as const;
@@ -225,7 +226,7 @@ export function parseProjectStateSnapshot(
value: unknown
): ProjectStateSnapshot {
const version = isPlainObject(value) ? value.schemaVersion : undefined;
const snapshot =
const parsedSnapshot =
version === legacyProjectStateSnapshotSchemaVersion
? upgradeSupplyTypesProjectStateSnapshot(
upgradeDistributionBoardProjectStateSnapshot(
@@ -255,10 +256,57 @@ export function parseProjectStateSnapshot(
supplyTypesProjectStateSnapshotSchema.parse(value)
)
: projectStateSnapshotSchema.parse(value);
const snapshot = normalizeSnapshotPhaseTypes(parsedSnapshot);
assertProjectStateSnapshotRelations(snapshot);
return snapshot;
}
function normalizeSnapshotPhaseTypes(
snapshot: ProjectStateSnapshot
): ProjectStateSnapshot {
const sectionById = new Map(
snapshot.circuitSections.map((section) => [section.id, section])
);
const projectDeviceById = new Map(
snapshot.projectDevices.map((device) => [device.id, device])
);
return {
...snapshot,
circuits: snapshot.circuits.map((circuit) => {
const section = sectionById.get(circuit.sectionId);
const deviceRows = circuit.deviceRows.map((row) => {
const normalizedPhaseType =
normalizeKnownElectricalPhaseType(row.phaseType);
const linkedPhaseType = row.linkedProjectDeviceId
? projectDeviceById.get(row.linkedProjectDeviceId)?.phaseType
: undefined;
return {
...row,
phaseType:
normalizedPhaseType ??
linkedPhaseType ??
(section?.key === "three_phase"
? "three_phase"
: "single_phase"),
};
});
return {
...circuit,
voltage: section
? resolveProjectVoltage(
resolveCircuitPhaseType(
section.key,
deviceRows.map((row) => row.phaseType)
),
snapshot.project
)
: circuit.voltage,
deviceRows,
};
}),
};
}
function upgradeLegacyProjectStateSnapshot(
snapshot: z.infer<typeof legacyProjectStateSnapshotSchema>
): z.infer<typeof previousProjectStateSnapshotSchema> {
+35 -2
View File
@@ -1,5 +1,39 @@
export type ElectricalPhaseType = "single_phase" | "three_phase";
export function isElectricalPhaseType(
value: unknown
): value is ElectricalPhaseType {
return value === "single_phase" || value === "three_phase";
}
export function normalizeKnownElectricalPhaseType(
value: string | null
): ElectricalPhaseType | null {
if (value === null) {
return null;
}
const normalized = value
.trim()
.toLocaleLowerCase("de-DE")
.replaceAll("-", "")
.replaceAll("_", "");
if (
["1ph", "1phase", "1phasig", "einphasig", "singlephase"].includes(
normalized
)
) {
return "single_phase";
}
if (
["3ph", "3phase", "3phasig", "dreiphasig", "threephase"].includes(
normalized
)
) {
return "three_phase";
}
throw new Error(`Unknown electrical phase type: ${value}`);
}
export interface ProjectVoltageSettings {
singlePhaseVoltageV: number;
threePhaseVoltageV: number;
@@ -25,8 +59,7 @@ export function resolveCircuitPhaseType(
return "single_phase";
}
const assignedPhaseTypes = devicePhaseTypes.filter(
(phaseType): phaseType is ElectricalPhaseType =>
phaseType === "single_phase" || phaseType === "three_phase"
isElectricalPhaseType
);
return assignedPhaseTypes.length > 0 &&
assignedPhaseTypes.every((phaseType) => phaseType === "three_phase")
+107 -27
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}
@@ -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>
+1 -1
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;
}
+13
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 === "") {
@@ -7,7 +7,7 @@ export const createGlobalDeviceSchema = z.object({
quantity: z.number().min(0),
installedPowerPerUnitKw: z.number().min(0),
demandFactor: z.number().min(0).max(1),
phaseCount: z.union([z.literal(1), z.literal(3)]).optional(),
phaseCount: z.union([z.literal(1), z.literal(3)]),
powerFactor: z.number().min(0).max(1).optional(),
note: z.string().optional(),
}).strict();
+2
View File
@@ -82,6 +82,8 @@ describe("circuit grid model", () => {
assert.equal(formatValue(0.87654, "simultaneityFactor"), "0,88");
assert.equal(formatValue(230.4, "voltage"), "230");
assert.equal(formatValue(16, "protectionRatedCurrent"), "16");
assert.equal(formatValue("single_phase", "phaseType"), "1-phasig");
assert.equal(formatValue("three_phase", "phaseType"), "3-phasig");
});
it("uses circuit display values for block sorting and falls back to the first device", () => {
+13
View File
@@ -7,6 +7,7 @@ import {
import {
resolveCircuitPhaseType,
resolveProjectVoltage,
normalizeKnownElectricalPhaseType,
} from "../src/domain/services/project-voltage.service.js";
describe("circuit power calculation", () => {
@@ -43,5 +44,17 @@ describe("project voltage derivation", () => {
assert.equal(resolveCircuitPhaseType("unassigned", []), "single_phase");
assert.equal(resolveCircuitPhaseType("unassigned", [null]), "single_phase");
});
it("normalizes known legacy phase labels without guessing unknown values", () => {
assert.equal(normalizeKnownElectricalPhaseType("1phasig"), "single_phase");
assert.equal(normalizeKnownElectricalPhaseType("1-phasig"), "single_phase");
assert.equal(normalizeKnownElectricalPhaseType("3phase"), "three_phase");
assert.equal(normalizeKnownElectricalPhaseType("3-phasig"), "three_phase");
assert.equal(normalizeKnownElectricalPhaseType(null), null);
assert.throws(
() => normalizeKnownElectricalPhaseType("zweiphasig"),
/Unknown electrical phase type/
);
});
});