"use client"; import { type FormEvent, useMemo, useState } from "react"; import { CONDUCTOR_MATERIALS, CROSS_SECTIONS_MM2, HARMONIC_NEUTRAL_LOAD_LABELS, HARMONIC_NEUTRAL_LOAD_OPTIONS, INSULATION_MATERIALS, INSULATION_MATERIAL_LABELS, LAYING_METHODS, LAYING_METHOD_GROUP, LAYING_METHOD_LABELS, MAX_GROUPING_CIRCUITS, PRACTICAL_MINIMUM_CROSS_SECTION_MM2, REFERENCE_TEMPERATURE_C, temperatureOptions, type CableSizingAlert, type CableSizingInput, type CableSizingResult, type CircuitCategory, type ConductorMaterial, type HarmonicNeutralLoad, type InsulationMaterial, type LayingMethod, } from "../../cable-sizing/domain/cable-sizing-calculation"; import { calculateCableSizing } from "./cable-sizing-api"; import type { CircuitTreeCircuitDto } from "../types"; import { deriveCircuitCosPhi, deriveCircuitPhase, toExistingProtection, } from "./cable-sizing-defaults"; import { FormModal } from "./form-modal"; interface CableSizingModalProps { circuit: CircuitTreeCircuitDto; circuitCategory?: CircuitCategory; singlePhaseVoltageV: number; threePhaseVoltageV: number; isSaving: boolean; onClose: () => void; onApply: (patch: { cableCrossSection: string; cableType?: string; cableLength?: number; }) => Promise; } // Extracts the first decimal number from a free-text cross-section entry // like "2.5 mm²" or "2,5"; used only for the manual-entry sanity check. function parseCrossSectionMm2(text: string): number | null { const match = text.match(/([0-9]+(?:[.,][0-9]+)?)/); if (!match) return null; const value = Number(match[1].replace(",", ".")); return Number.isFinite(value) ? value : null; } const formatNumber = (value: number, digits: number) => value.toLocaleString("de-DE", { minimumFractionDigits: digits, maximumFractionDigits: digits }); export function CableSizingModal({ circuit, circuitCategory, singlePhaseVoltageV, threePhaseVoltageV, isSaving, onClose, onApply, }: CableSizingModalProps) { const derivedPhase = deriveCircuitPhase(circuit, circuitCategory); const derivedCosPhi = deriveCircuitCosPhi(circuit); const existingProtection = toExistingProtection(circuit.protectionDevice); const [layingMethod, setLayingMethod] = useState("C"); const [insulation, setInsulation] = useState("pvc"); const [conductorMaterial, setConductorMaterial] = useState("copper"); const [ambientTemperatureC, setAmbientTemperatureC] = useState(30); const [groupingCircuits, setGroupingCircuits] = useState(1); const [cosPhi, setCosPhi] = useState(derivedCosPhi); const [maxVoltageDropPercent, setMaxVoltageDropPercent] = useState(3); const [harmonicNeutralLoad, setHarmonicNeutralLoad] = useState("none"); const [lengthM, setLengthM] = useState(circuit.cableLength ?? 0); const [phase, setPhase] = useState<1 | 3>(derivedPhase); // Manual entry: applying never requires a calculation. Prefilled from the // circuit so an existing cable specification is not lost. const [manualCableType, setManualCableType] = useState(circuit.cableType ?? ""); const [manualCrossSection, setManualCrossSection] = useState(circuit.cableCrossSection ?? ""); const [calculating, setCalculating] = useState(false); const [result, setResult] = useState(null); const [alerts, setAlerts] = useState([]); const [error, setError] = useState(null); const voltage = phase === derivedPhase && circuit.voltage ? circuit.voltage : phase === 3 ? threePhaseVoltageV : singlePhaseVoltageV; const group = LAYING_METHOD_GROUP[layingMethod]; const availableTemperatures = temperatureOptions(layingMethod, insulation); const input: CableSizingInput = useMemo( () => ({ phase, mode: "power", powerKw: circuit.circuitTotalPower, cosPhi, voltage, lengthM, layingMethod, conductorMaterial, insulation, ambientTemperatureC, groupingCircuits, maxVoltageDropPercent, harmonicNeutralLoad, existingProtection, circuitCategory, }), [ phase, circuit.circuitTotalPower, cosPhi, voltage, lengthM, layingMethod, conductorMaterial, insulation, ambientTemperatureC, groupingCircuits, maxVoltageDropPercent, harmonicNeutralLoad, existingProtection?.ratedCurrentA, existingProtection?.kind, circuitCategory, ] ); const manualCrossSectionWarning = useMemo(() => { const trimmed = manualCrossSection.trim(); if (!trimmed) return null; const parsed = parseCrossSectionMm2(trimmed); if (parsed == null) return null; if (!(CROSS_SECTIONS_MM2 as readonly number[]).includes(parsed)) { return `${formatNumber(parsed, parsed % 1 ? 1 : 0)} mm² ist kein Normquerschnitt (${CROSS_SECTIONS_MM2.map((value) => formatNumber(value, value % 1 ? 1 : 0)).join(", ")}).`; } if ( result?.supported && result.recommendedCrossSectionMm2 != null && parsed < result.recommendedCrossSectionMm2 ) { return `${formatNumber(parsed, parsed % 1 ? 1 : 0)} mm² ist kleiner als die zuletzt berechnete Empfehlung (${formatNumber(result.recommendedCrossSectionMm2, result.recommendedCrossSectionMm2 % 1 ? 1 : 0)} mm²).`; } const practicalMinimum = circuitCategory ? PRACTICAL_MINIMUM_CROSS_SECTION_MM2[circuitCategory] : undefined; if (practicalMinimum != null && parsed < practicalMinimum) { return `${formatNumber(parsed, parsed % 1 ? 1 : 0)} mm² liegt unter dem Praxis-Mindestquerschnitt für 1-phasige Stromkreise (${formatNumber(practicalMinimum, 1)} mm²).`; } return null; }, [manualCrossSection, result, circuitCategory]); function resetResult() { setResult(null); setAlerts([]); } function applyReferenceTemperature(method: LayingMethod, material: InsulationMaterial) { const options = temperatureOptions(method, material); const reference = REFERENCE_TEMPERATURE_C[LAYING_METHOD_GROUP[method]]; setAmbientTemperatureC(options.includes(reference) ? reference : options[0]); } async function handleCalculate() { setCalculating(true); setError(null); try { const response = await calculateCableSizing(input); setResult(response.result); setAlerts(response.alerts); // The suggestion fills the manual field, which stays the single source // of truth for what gets applied. if (response.result.supported && response.result.recommendedCrossSectionMm2) { const size = response.result.recommendedCrossSectionMm2; setManualCrossSection(`${formatNumber(size, size % 1 ? 1 : 0)} mm²`); } } catch (err) { setError(err instanceof Error ? err.message : "Berechnung fehlgeschlagen"); } finally { setCalculating(false); } } async function handleSubmit(event: FormEvent) { event.preventDefault(); if (!manualCrossSection.trim()) return; await onApply({ cableCrossSection: manualCrossSection.trim(), cableType: manualCableType.trim() || undefined, cableLength: lengthM, }); } const recommendedRow = result?.rows.find((row) => row.recommended); return (
{ setLengthM(Number(event.target.value)); resetResult(); }} type="number" value={lengthM} />
{phase !== derivedPhase ? (
Abweichend vom Stromkreis ({derivedPhase === 3 ? "3~" : "1~"}).
) : null}
{ setGroupingCircuits(Math.max(1, Math.round(Number(event.target.value) || 1))); resetResult(); }} step={1} type="number" value={groupingCircuits} />
Anzahl gemeinsam verlegter Stromkreise, einschließlich dieses.
{phase === 3 ? (
) : null}
{ setCosPhi(Math.min(1, Math.max(0.1, Number(event.target.value) || 1))); resetResult(); }} step={0.01} type="number" value={cosPhi} />
Aus den Gerätezeilen: {formatNumber(derivedCosPhi, 2)}
{ setMaxVoltageDropPercent(Number(event.target.value) || 3); resetResult(); }} step={0.5} type="number" value={maxVoltageDropPercent} />
Schutzorgan:{" "} {existingProtection ? `${existingProtection.kind === "breaker" ? "Leitungsschutzschalter" : "Sicherung gG"} ${existingProtection.ratedCurrentA} A (aus dem Stromkreis, In ≤ Iz und I₂ ≤ 1,45 · Iz)` : "keines mit Überstromschutz hinterlegt – Bemessung nur nach Betriebsstrom"}
{error ?

{error}

: null} {result ? (
{result.supported ? (
Betriebsstrom IB
{formatNumber(result.operatingCurrentA, 1)} A
Erforderliches Iz
{formatNumber(result.designCurrentA, 1)} A
Empfohlener Querschnitt
{result.recommendedCrossSectionMm2 != null ? `${formatNumber(result.recommendedCrossSectionMm2, result.recommendedCrossSectionMm2 % 1 ? 1 : 0)} mm²` : "–"}
ΔU bei Empfehlung
{result.voltageDropAtRecommendedPercent != null ? `${formatNumber(result.voltageDropAtRecommendedPercent, 2)} %` : "–"}
Korrekturfaktor gesamt
{formatNumber(result.combinedDerationFactor, 2)}
Temperatur / Häufung (gerechnet)
{result.temperatureUsedC} °C ({formatNumber(result.temperatureFactor ?? 1, 2)}) /{" "} {result.groupingUsed} ({formatNumber(result.groupingFactor ?? 1, 2)})
Max. Länge für ΔU
{recommendedRow?.maxLengthForVoltageDropM != null ? `${recommendedRow.maxLengthForVoltageDropM.toFixed(0)} m` : "–"}
) : null} {alerts.map((alert, index) => (
{alert.text}
))}
) : null}

Kabeltyp und Querschnitt direkt eingeben oder die Empfehlung übernehmen. Beides landet in denselben Feldern und kann vor dem Übernehmen angepasst werden.

setManualCableType(event.target.value)} placeholder="z. B. NYM-J 3x2,5" value={manualCableType} />
setManualCrossSection(event.target.value)} placeholder="z. B. 4 mm²" value={manualCrossSection} />
{manualCrossSectionWarning ? (
{manualCrossSectionWarning}
) : null}
); }