import type { CircuitTreeCircuitDto, CircuitTreeDeviceRowDto } from "../types.js"; import type { CircuitUpdatePatch } from "../../domain/models/circuit-project-command.model.js"; import type { CircuitDeviceRowUpdatePatch } from "../../domain/models/circuit-device-row-project-command.model.js"; export type CellKey = | "equipmentIdentifier" | "displayName" | "quantity" | "powerPerUnit" | "simultaneityFactor" | "rowTotalPower" | "circuitTotalPower" | "protectionSummary" | "cableSummary" | "roomSummary" | "remark" | "technicalName" | "connectionKind" | "phaseType" | "costGroup" | "category" | "level" | "roomNumberSnapshot" | "roomNameSnapshot" | "cosPhi" | "protectionType" | "protectionRatedCurrent" | "protectionCharacteristic" | "cableType" | "cableCrossSection" | "cableLength" | "rcdAssignment" | "terminalDesignation" | "voltage" | "controlRequirement" | "status" | "isReserve"; export type RowType = | "section" | "circuitCompact" | "circuitSummary" | "deviceRow" | "reserveCircuit" | "placeholder"; export type CellKind = "circuitField" | "deviceField" | "computed" | "readonly"; export type GridValue = string | number | boolean | undefined; export interface ColumnDef { key: CellKey; label: string; numeric?: boolean; defaultVisible?: boolean; locked?: boolean; } // BMK stays locked as the first column because circuit identity and circuit drag // handles depend on an always-visible equipment identifier. 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 [kW]", numeric: true, defaultVisible: true }, { key: "simultaneityFactor", label: "Gleichzeitigkeit", 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 }, { key: "remark", label: "Bemerkung", defaultVisible: true }, { key: "technicalName", label: "Technischer Name" }, { key: "connectionKind", label: "Anschlussart" }, { key: "phaseType", label: "Phasenart" }, { key: "costGroup", label: "Kostengruppe" }, { key: "category", label: "Kategorie" }, { key: "level", label: "Ebene" }, { key: "roomNumberSnapshot", label: "Raumnummer" }, { key: "roomNameSnapshot", label: "Raumname" }, { key: "cosPhi", label: "cos φ", numeric: true }, { key: "protectionType", label: "Schutzart" }, { key: "protectionRatedCurrent", label: "Bemessungsstrom", numeric: true }, { key: "protectionCharacteristic", label: "Charakteristik" }, { key: "cableType", label: "Kabeltyp" }, { key: "cableCrossSection", label: "Kabelquerschnitt" }, { key: "cableLength", label: "Kabellänge", numeric: true }, { key: "rcdAssignment", label: "RCD-Zuordnung" }, { key: "terminalDesignation", label: "Klemmenbezeichnung" }, { key: "voltage", label: "Spannung", numeric: true }, { key: "controlRequirement", label: "Steuerungsanforderung" }, { key: "status", label: "Status" }, { key: "isReserve", label: "Reserve" }, ]; export const defaultVisibleColumnKeys = allColumns .filter((column) => column.defaultVisible) .map((column) => column.key); const deviceOnlyColumns = new Set([ "quantity", "powerPerUnit", "simultaneityFactor", "rowTotalPower", "roomSummary", "technicalName", "connectionKind", "phaseType", "costGroup", "category", "level", "roomNumberSnapshot", "roomNameSnapshot", "cosPhi", ]); export const circuitOnlyColumns = new Set([ "equipmentIdentifier", "protectionSummary", "cableSummary", "circuitTotalPower", "protectionType", "protectionRatedCurrent", "protectionCharacteristic", "cableType", "cableCrossSection", "cableLength", "rcdAssignment", "terminalDesignation", "voltage", "controlRequirement", "status", "isReserve", ]); export const deviceFieldKeys = new Set([ "displayName", "technicalName", "connectionKind", "phaseType", "costGroup", "category", "level", "roomSummary", "roomNumberSnapshot", "roomNameSnapshot", "quantity", "powerPerUnit", "simultaneityFactor", "cosPhi", "remark", ]); const circuitFieldKeys = new Set([ "equipmentIdentifier", "displayName", "protectionType", "protectionRatedCurrent", "protectionCharacteristic", "protectionSummary", "cableType", "cableCrossSection", "cableLength", "cableSummary", "rcdAssignment", "terminalDesignation", "controlRequirement", "status", "isReserve", "remark", ]); const numberFormatters = new Map(); 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)); } 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 isGridEditorControlTarget(target: unknown): boolean { if ( target === null || typeof target !== "object" || !("closest" in target) || typeof target.closest !== "function" ) { return false; } return Boolean(target.closest("input, select, textarea")); } export function parseNumeric(cellKey: CellKey, draft: string): number | undefined { const trimmed = draft.trim(); if (trimmed === "") { return undefined; } const parsed = Number(trimmed); if (Number.isNaN(parsed)) { throw new Error(`Ungültiger Zahlenwert in ${cellKey}`); } return parsed; } export function buildCircuitEditPatch( key: CellKey, draft: string ): CircuitUpdatePatch { if (key === "protectionSummary" || key === "cableSummary") { return { remark: draft.trim() || null }; } if ( key === "protectionRatedCurrent" || key === "cableLength" || key === "voltage" ) { return { [key]: parseNumeric(key, draft) ?? null }; } if (key === "isReserve") { const normalized = draft.trim().toLowerCase(); return { isReserve: ["1", "true", "yes", "ja"].includes(normalized), }; } if (key === "equipmentIdentifier") { return { equipmentIdentifier: draft.trim() }; } return { [key]: draft.trim() || null } as CircuitUpdatePatch; } export function buildDeviceRowEditPatch( key: CellKey, draft: string ): CircuitDeviceRowUpdatePatch { if ( key === "quantity" || key === "powerPerUnit" || key === "simultaneityFactor" ) { const value = parseNumeric(key, draft); if (value === undefined) { throw new Error("Dieses Zahlenfeld darf nicht leer sein."); } return { [key]: value }; } if (key === "cosPhi") { return { cosPhi: parseNumeric(key, draft) ?? null }; } if (key === "roomSummary") { const trimmed = draft.trim(); if (!trimmed) { return { roomNumberSnapshot: null, roomNameSnapshot: null, }; } const firstSpace = trimmed.indexOf(" "); return firstSpace > 0 ? { roomNumberSnapshot: trimmed.slice(0, firstSpace).trim(), roomNameSnapshot: trimmed.slice(firstSpace + 1).trim() || null, } : { roomNumberSnapshot: trimmed, roomNameSnapshot: null, }; } if (key === "technicalName") { return { name: draft.trim() }; } if (key === "displayName") { return { displayName: draft.trim() }; } return { [key]: draft.trim() || null } as CircuitDeviceRowUpdatePatch; } export function getDeviceValue(device: CircuitTreeDeviceRowDto, key: CellKey): GridValue { switch (key) { case "technicalName": return device.name; case "displayName": return device.displayName || device.name; case "phaseType": return device.phaseType; case "connectionKind": return device.connectionKind; case "costGroup": return device.costGroup; case "category": return device.category; case "level": return device.level; case "roomSummary": return [device.roomNumberSnapshot, device.roomNameSnapshot].filter(Boolean).join(" ").trim() || undefined; case "roomNumberSnapshot": return device.roomNumberSnapshot; case "roomNameSnapshot": return device.roomNameSnapshot; case "quantity": return device.quantity; case "powerPerUnit": return device.powerPerUnit; case "simultaneityFactor": return device.simultaneityFactor; case "cosPhi": return device.cosPhi; case "rowTotalPower": return device.rowTotalPower; case "remark": return device.remark; default: return undefined; } } export function getCircuitValue(circuit: CircuitTreeCircuitDto, key: CellKey): GridValue { switch (key) { case "equipmentIdentifier": return circuit.equipmentIdentifier; case "displayName": return circuit.displayName; case "circuitTotalPower": return circuit.circuitTotalPower; case "protectionType": return circuit.protectionType; case "protectionRatedCurrent": return circuit.protectionRatedCurrent; case "protectionCharacteristic": return circuit.protectionCharacteristic; case "protectionSummary": { const current = circuit.protectionRatedCurrent !== undefined && circuit.protectionRatedCurrent !== null ? `${circuit.protectionRatedCurrent}A` : ""; return [circuit.protectionType, current, circuit.protectionCharacteristic].filter(Boolean).join(" ").trim() || undefined; } case "cableSummary": { const length = circuit.cableLength !== undefined && circuit.cableLength !== null ? `${circuit.cableLength} m` : ""; return [circuit.cableType, circuit.cableCrossSection, length].filter(Boolean).join(", ").trim() || undefined; } case "cableType": return circuit.cableType; case "cableCrossSection": return circuit.cableCrossSection; case "cableLength": return circuit.cableLength; case "rcdAssignment": return circuit.rcdAssignment; case "terminalDesignation": return circuit.terminalDesignation; case "voltage": return circuit.voltage; case "controlRequirement": return circuit.controlRequirement; case "status": return circuit.status; case "isReserve": return circuit.isReserve; case "remark": return circuit.remark; default: return undefined; } } 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); } if (deviceOnlyColumns.has(key)) { return firstRow ? getDeviceValue(firstRow, key) : undefined; } if (key === "displayName") { if (circuit.displayName) return circuit.displayName; return firstRow ? getDeviceValue(firstRow, key) : undefined; } if (key === "remark") { if (circuit.remark) return circuit.remark; return firstRow ? getDeviceValue(firstRow, key) : undefined; } return getCircuitValue(circuit, key); } export function compareSortValues(left: GridValue, right: GridValue): number { const leftValue = left === undefined || left === null ? "" : left; const rightValue = right === undefined || right === null ? "" : right; if (typeof leftValue === "number" && typeof rightValue === "number") { return leftValue - rightValue; } return String(leftValue).localeCompare(String(rightValue), undefined, { sensitivity: "base", numeric: true }); } export function getCellKind(rowType: RowType, key: CellKey): CellKind { if (key === "rowTotalPower" || key === "circuitTotalPower") return "computed"; if (key === "voltage") return "readonly"; if (rowType === "section") return "readonly"; if (rowType === "placeholder") { if (deviceFieldKeys.has(key)) return "deviceField"; if (circuitFieldKeys.has(key)) return "circuitField"; return "readonly"; } if (rowType === "deviceRow") return deviceFieldKeys.has(key) ? "deviceField" : "readonly"; if (rowType === "circuitSummary") return circuitFieldKeys.has(key) ? "circuitField" : "readonly"; if (rowType === "reserveCircuit" || rowType === "circuitCompact") { if (deviceFieldKeys.has(key)) return "deviceField"; if (circuitFieldKeys.has(key)) return "circuitField"; } return "readonly"; }