forked from jappel/leistungsbilanz-ts
cable-sizing: size for the protective device, not just the load; manual entry; icon position
- calculateCableSizing now selects cross-section against designCurrentA = max(operatingCurrentA, existingProtectionRatedCurrentA) instead of the load current alone. A breaker only trips at its own rated current (In), so a cable sized for the actual load could overheat under sustained current below that threshold - the standard In <= Iz rule. Voltage-drop calculation still uses the real operating current, only the capacity check changed. Alerts and the ok-summary now name the breaker as the limiting factor when it is one. - Added a manual cable type/cross-section entry in the modal itself: running a calculation pre-fills these fields as a suggestion, but they are the single field that actually gets applied, and stay editable - this restores the direct manual-entry capability the modal replaced when it took over the cableSummary/cableCrossSection cell click. - Fixed calculator icon position from a trailing ::after (inconsistent position depending on cell text length, easy to miss on empty cells) to a fixed-position ::before, so it is always in the same place regardless of whether cable data is already filled in. - No new display for the existing protection device - it already has its own Schutz column in this app.
This commit is contained in:
parent
a35d04965e
commit
8562d6eb83
5 changed files with 145 additions and 29 deletions
|
|
@ -1227,10 +1227,17 @@ a.kpi:hover {
|
|||
box-shadow: inset 0 0 0 1px var(--color-primary);
|
||||
}
|
||||
|
||||
.tree-grid .cell-cable-sizing-trigger::after {
|
||||
content: " 🧮";
|
||||
.tree-grid .cell-cable-sizing-trigger {
|
||||
position: relative;
|
||||
padding-left: 1.4em;
|
||||
}
|
||||
|
||||
.tree-grid .cell-cable-sizing-trigger::before {
|
||||
content: "🧮";
|
||||
position: absolute;
|
||||
left: 0.2em;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.tree-grid .device-drag-handle {
|
||||
|
|
|
|||
|
|
@ -259,6 +259,13 @@ export interface CableSizingResult {
|
|||
* combination - every field below is null/empty, never a guess. */
|
||||
dataVerified: boolean;
|
||||
operatingCurrentA: number;
|
||||
/** max(operatingCurrentA, existingProtectionRatedCurrentA). The cable
|
||||
* capacity (Iz) must cover the protective device's rated current (In),
|
||||
* not just the actual load current - a breaker only trips at In, so a
|
||||
* cable sized for Ib alone could overheat under sustained load below the
|
||||
* trip threshold. This is the value cross-section selection actually
|
||||
* uses; operatingCurrentA is kept for display/voltage-drop only. */
|
||||
designCurrentA: number;
|
||||
crossSectionByCapacityMm2: number | null;
|
||||
crossSectionByVoltageDropMm2: number | null;
|
||||
recommendedCrossSectionMm2: number | null;
|
||||
|
|
@ -282,10 +289,16 @@ function operatingCurrentA(input: CableSizingInput): number {
|
|||
export function calculateCableSizing(input: CableSizingInput): CableSizingResult {
|
||||
const ib = operatingCurrentA(input);
|
||||
|
||||
const designCurrentA =
|
||||
input.existingProtectionRatedCurrentA != null
|
||||
? Math.max(ib, input.existingProtectionRatedCurrentA)
|
||||
: ib;
|
||||
|
||||
if (!isCableSizingDataVerified(input.layingMethod, input.insulation)) {
|
||||
return {
|
||||
dataVerified: false,
|
||||
operatingCurrentA: ib,
|
||||
designCurrentA,
|
||||
crossSectionByCapacityMm2: null,
|
||||
crossSectionByVoltageDropMm2: null,
|
||||
recommendedCrossSectionMm2: null,
|
||||
|
|
@ -328,7 +341,7 @@ export function calculateCableSizing(input: CableSizingInput): CableSizingResult
|
|||
);
|
||||
|
||||
const indexByCapacity = correctedCurrents.findIndex(
|
||||
(value) => value != null && value >= ib
|
||||
(value) => value != null && value >= designCurrentA
|
||||
);
|
||||
const indexByVoltageDrop = voltageDrops.findIndex(
|
||||
(value) => value <= input.maxVoltageDropPercent
|
||||
|
|
@ -341,7 +354,7 @@ export function calculateCableSizing(input: CableSizingInput): CableSizingResult
|
|||
const rows: CrossSectionRow[] = CROSS_SECTIONS_MM2.map((crossSection, i) => {
|
||||
const rated = ratedCurrents[i];
|
||||
const corrected = correctedCurrents[i];
|
||||
const currentSufficient = corrected == null ? null : corrected >= ib;
|
||||
const currentSufficient = corrected == null ? null : corrected >= designCurrentA;
|
||||
const voltageDropSufficient = voltageDrops[i] <= input.maxVoltageDropPercent;
|
||||
return {
|
||||
crossSectionMm2: crossSection,
|
||||
|
|
@ -368,6 +381,7 @@ export function calculateCableSizing(input: CableSizingInput): CableSizingResult
|
|||
return {
|
||||
dataVerified: true,
|
||||
operatingCurrentA: ib,
|
||||
designCurrentA,
|
||||
crossSectionByCapacityMm2:
|
||||
indexByCapacity >= 0 ? CROSS_SECTIONS_MM2[indexByCapacity] : null,
|
||||
crossSectionByVoltageDropMm2:
|
||||
|
|
@ -407,9 +421,14 @@ export function buildCableSizingAlerts(
|
|||
const recommended = result.recommendedCrossSectionMm2;
|
||||
|
||||
if (byCapacity == null) {
|
||||
const dueToBreaker =
|
||||
input.existingProtectionRatedCurrentA != null &&
|
||||
input.existingProtectionRatedCurrentA > result.operatingCurrentA;
|
||||
alerts.push({
|
||||
kind: "critical",
|
||||
text: `Betriebsstrom ${result.operatingCurrentA.toFixed(1)} A übersteigt die Belastbarkeit aller Standardquerschnitte bei Verlegeart ${input.layingMethod}.`,
|
||||
text: dueToBreaker
|
||||
? `Vorhandene Sicherung (${input.existingProtectionRatedCurrentA} A) übersteigt die Belastbarkeit aller Standardquerschnitte bei Verlegeart ${input.layingMethod} - nicht der Betriebsstrom (${result.operatingCurrentA.toFixed(1)} A). Größere Verlegeart oder kleinere Sicherung prüfen.`
|
||||
: `Betriebsstrom ${result.operatingCurrentA.toFixed(1)} A übersteigt die Belastbarkeit aller Standardquerschnitte bei Verlegeart ${input.layingMethod}.`,
|
||||
});
|
||||
} else if (byDrop != null && byDrop > byCapacity) {
|
||||
alerts.push({
|
||||
|
|
@ -435,16 +454,23 @@ export function buildCableSizingAlerts(
|
|||
text: `Korrekturfaktor angewendet: ${result.combinedDerationFactor.toFixed(2)} (Temperatur × Häufung${result.harmonicReductionApplied ? " × Oberschwingungen" : ""}).`,
|
||||
});
|
||||
}
|
||||
if (result.protectionCoordination && !result.protectionCoordination.coordinated) {
|
||||
alerts.push({
|
||||
kind: "warn",
|
||||
text: `Vorhandener Schutz (${result.protectionCoordination.ratedCurrentA} A) übersteigt die Belastbarkeit des empfohlenen Querschnitts - Koordination prüfen (vereinfachte Prüfung, ersetzt keine vollständige Überlast-/Kurzschlussprüfung).`,
|
||||
});
|
||||
if (result.protectionCoordination) {
|
||||
alerts.push(
|
||||
result.protectionCoordination.coordinated
|
||||
? {
|
||||
kind: "info",
|
||||
text: `Vorhandene Sicherung (${result.protectionCoordination.ratedCurrentA} A) ist bei der Dimensionierung berücksichtigt (In ≤ Iz, vereinfachte Prüfung, ersetzt keine vollständige Überlast-/Kurzschlussprüfung).`,
|
||||
}
|
||||
: {
|
||||
kind: "warn",
|
||||
text: `Vorhandene Sicherung (${result.protectionCoordination.ratedCurrentA} A) übersteigt die Belastbarkeit des empfohlenen Querschnitts - Koordination prüfen.`,
|
||||
}
|
||||
);
|
||||
}
|
||||
if (alerts.length === 0 && recommended != null) {
|
||||
alerts.push({
|
||||
kind: "ok",
|
||||
text: `${recommended} mm² (${input.conductorMaterial === "aluminum" ? "Alu" : "Cu"}) deckt bei ${input.lengthM} m Länge sowohl Belastbarkeit (${result.operatingCurrentA.toFixed(1)} A) als auch Spannungsfall (≤ ${input.maxVoltageDropPercent}%) ab.`,
|
||||
text: `${recommended} mm² (${input.conductorMaterial === "aluminum" ? "Alu" : "Cu"}) deckt bei ${input.lengthM} m Länge sowohl Belastbarkeit (${result.designCurrentA.toFixed(1)} A${input.existingProtectionRatedCurrentA != null ? ", inkl. vorhandener Sicherung" : ""}) als auch Spannungsfall (≤ ${input.maxVoltageDropPercent}%) ab.`,
|
||||
});
|
||||
}
|
||||
return alerts;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,11 @@ interface CableSizingModalProps {
|
|||
isSaving: boolean;
|
||||
projectId: string;
|
||||
onClose: () => void;
|
||||
onApply: (patch: { cableCrossSection: string; cableLength?: number }) => Promise<void>;
|
||||
onApply: (patch: {
|
||||
cableCrossSection: string;
|
||||
cableType?: string;
|
||||
cableLength?: number;
|
||||
}) => Promise<void>;
|
||||
}
|
||||
|
||||
function temperatureOptions(method: LayingMethod) {
|
||||
|
|
@ -58,6 +62,12 @@ export function CableSizingModal({
|
|||
const [lengthM, setLengthM] = useState(circuit.cableLength ?? 0);
|
||||
const [phase, setPhase] = useState<1 | 3>(circuit.voltage === 400 ? 3 : 1);
|
||||
|
||||
// Manual override: applying this never requires a calculation. Prefilled
|
||||
// from the circuit's current values so opening the modal on an already
|
||||
// specified cable doesn't lose that data.
|
||||
const [manualCableType, setManualCableType] = useState(circuit.cableType ?? "");
|
||||
const [manualCrossSection, setManualCrossSection] = useState(circuit.cableCrossSection ?? "");
|
||||
|
||||
const [calculating, setCalculating] = useState(false);
|
||||
const [calculationId, setCalculationId] = useState<string | null>(null);
|
||||
const [result, setResult] = useState<CableSizingResult | null>(null);
|
||||
|
|
@ -121,6 +131,12 @@ export function CableSizingModal({
|
|||
setResult(response.result);
|
||||
setAlerts(response.alerts);
|
||||
setCalculationId(response.calculationId);
|
||||
// Calculating fills the manual field as a suggestion, but that field
|
||||
// stays the single source of truth for what gets applied - the user
|
||||
// can still edit it by hand before submitting.
|
||||
if (response.result.dataVerified && response.result.recommendedCrossSectionMm2) {
|
||||
setManualCrossSection(`${response.result.recommendedCrossSectionMm2} mm²`);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Berechnung fehlgeschlagen");
|
||||
} finally {
|
||||
|
|
@ -130,21 +146,22 @@ export function CableSizingModal({
|
|||
|
||||
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!result?.recommendedCrossSectionMm2) return;
|
||||
if (!manualCrossSection.trim()) return;
|
||||
await onApply({
|
||||
cableCrossSection: `${result.recommendedCrossSectionMm2} mm²`,
|
||||
cableCrossSection: manualCrossSection.trim(),
|
||||
cableType: manualCableType.trim() || undefined,
|
||||
cableLength: lengthM,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<FormModal
|
||||
description="Empfehlung auf Basis von Verlegeart, Isolierstoff, Temperatur und Häufung nach DIN VDE 0298-4. Ersetzt keine vollständige Norm-Prüfung; nicht verifizierte Verlegearten liefern bewusst kein Ergebnis. Du kannst das Ergebnis vor der Übernahme frei prüfen."
|
||||
description="Empfehlung auf Basis von Verlegeart, Isolierstoff, Temperatur und Häufung nach DIN VDE 0298-4, oder Kabeltyp/Querschnitt direkt manuell eintragen. Ersetzt keine vollständige Norm-Prüfung; nicht verifizierte Verlegearten liefern bewusst kein Ergebnis."
|
||||
isSaving={isSaving}
|
||||
onClose={onClose}
|
||||
onSubmit={handleSubmit}
|
||||
submitDisabled={!result?.dataVerified || !result.recommendedCrossSectionMm2}
|
||||
submitLabel="Empfehlung übernehmen"
|
||||
submitDisabled={!manualCrossSection.trim()}
|
||||
submitLabel="Übernehmen"
|
||||
title={`Kabel dimensionieren - ${circuit.equipmentIdentifier}`}
|
||||
>
|
||||
<div className="row g-3">
|
||||
|
|
@ -183,7 +200,6 @@ export function CableSizingModal({
|
|||
value={`${circuit.circuitTotalPower.toFixed(2)} kW`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label" htmlFor="cable-sizing-method">
|
||||
Verlegeart (DIN VDE 0298-4)
|
||||
|
|
@ -355,6 +371,14 @@ export function CableSizingModal({
|
|||
<div className="text-muted small">Betriebsstrom Ib</div>
|
||||
<div>{result.operatingCurrentA.toFixed(1)} A</div>
|
||||
</div>
|
||||
<div className="col-6 col-md-3">
|
||||
<div className="text-muted small">
|
||||
Bemessungsstrom (für Querschnittswahl){circuit.protectionDevice ? " *" : ""}
|
||||
</div>
|
||||
<div>
|
||||
<strong>{result.designCurrentA.toFixed(1)} A</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-6 col-md-3">
|
||||
<div className="text-muted small">Empfohlener Querschnitt</div>
|
||||
<div>
|
||||
|
|
@ -394,14 +418,48 @@ export function CableSizingModal({
|
|||
{alert.text}
|
||||
</div>
|
||||
))}
|
||||
{circuit.protectionDevice && (
|
||||
<p className="text-muted small mt-2 mb-0">
|
||||
* Bemessungsstrom = Maximum aus Betriebsstrom und vorhandener Sicherung
|
||||
({circuit.protectionDevice.ratedCurrentA} A) - die Sicherung löst erst bei ihrem
|
||||
eigenen Bemessungsstrom aus, das Kabel muss also dafür ausgelegt sein, nicht nur
|
||||
für die tatsächliche Last.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{calculationId ? null : (
|
||||
<p className="text-muted small mt-2">
|
||||
Zuerst berechnen, dann prüfen und ggf. übernehmen - nichts wird automatisch
|
||||
geschrieben.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<hr className="my-3" />
|
||||
<p className="text-muted small mb-2">
|
||||
Querschnitt manuell eingeben oder eine Berechnung oben übernehmen - beides schreibt in
|
||||
dasselbe Feld, du kannst es vor dem Übernehmen frei anpassen.
|
||||
</p>
|
||||
<div className="row g-3">
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label" htmlFor="cable-sizing-manual-type">
|
||||
Kabeltyp
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="cable-sizing-manual-type"
|
||||
onChange={(event) => setManualCableType(event.target.value)}
|
||||
placeholder="z.B. NYM-J 3x1.5"
|
||||
value={manualCableType}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label" htmlFor="cable-sizing-manual-cross-section">
|
||||
Querschnitt
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="cable-sizing-manual-cross-section"
|
||||
onChange={(event) => setManualCrossSection(event.target.value)}
|
||||
placeholder="z.B. 4 mm²"
|
||||
value={manualCrossSection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</FormModal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1403,6 +1403,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
|
||||
async function handleApplyCableSizing(patch: {
|
||||
cableCrossSection: string;
|
||||
cableType?: string;
|
||||
cableLength?: number;
|
||||
}) {
|
||||
const circuit = cableSizingEditorCircuit;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue