"use client"; import type { ReactNode } from "react"; import { ArrowDown, ArrowUp, Pencil, Plus, Trash2 } from "lucide-react"; import { calculateEstimateBalance, calculateEstimateRow, estimateRowConnectionLabels, estimateRowUnitLabels, type EstimateBalanceSnapshot, type EstimateGroupSnapshot, type EstimatePower, type EstimateRowConnection, type EstimateRowSnapshot, type EstimateRowUnit, } from "../../shared/estimate-balance/estimate-balance"; import { addEstimateRow, estimateTextLimits, moveEstimateGroup, removeEstimateGroup, removeEstimateRow, updateEstimateGroup, updateEstimateRow, type EstimateRowPatch, } from "./estimate-balance-document"; import { estimateNumberRanges, formatAmpere, formatDecimalInput, formatKw, parseGermanDecimal, type DecimalRange, formatGermanNumber, } from "./estimate-balance-format"; import { DraftInput } from "./estimate-draft-input"; export type EstimateBalanceUpdate = ( update: (balance: EstimateBalanceSnapshot) => EstimateBalanceSnapshot ) => void; interface EstimateBalanceSheetProps { balance: EstimateBalanceSnapshot; disabled: boolean; onAddGroup: () => void; onChange: EstimateBalanceUpdate; onEditGroup: (group: EstimateGroupSnapshot) => void; onOpenGzfPicker: (groupId: string, rowId: string) => void; onValidityChange: (fieldKey: string, isValid: boolean) => void; threePhaseVoltageV: number; } // Leading columns up to cos φ; the power columns follow. const LEADING_COLUMN_COUNT = 9; const COLUMN_COUNT = 16; /** Suggested supply values; free text stays possible. */ const SUPPLY_SUGGESTIONS = ["ELT", "MSR"]; const SUPPLY_LIST_ID = "estimate-supply-options"; function NumberInput({ ariaLabel, disabled, fieldKey, onCommit, onValidityChange, range, value, }: { ariaLabel: string; disabled: boolean; fieldKey: string; onCommit: (value: number) => void; onValidityChange: (fieldKey: string, isValid: boolean) => void; range: DecimalRange; value: number; }) { return ( formatDecimalInput(current)} inputMode="decimal" printFormat={(current: number) => formatGermanNumber(current, 0, 3)} onCommit={onCommit} onValidityChange={(isValid) => onValidityChange(fieldKey, isValid)} parse={(text) => parseGermanDecimal(text, range)} value={value} /> ); } function supplyOptions(balance: EstimateBalanceSnapshot): string[] { const used = balance.groups.flatMap((group) => group.rows.map((row) => row.supply?.trim() ?? "") ); return [...new Set([...SUPPLY_SUGGESTIONS, ...used.filter(Boolean)])]; } function TextInput({ ariaLabel, disabled, list, maxLength, onChange, placeholder, value, }: { ariaLabel: string; disabled: boolean; list?: string; maxLength: number; onChange: (value: string) => void; placeholder?: string; value: string | null; }) { return ( <> onChange(event.target.value)} placeholder={placeholder} type="text" value={value ?? ""} /> {value ?? ""} ); } function IconButton({ children, disabled, label, onClick, variant = "secondary", }: { children: ReactNode; disabled: boolean; label: string; onClick: () => void; variant?: "secondary" | "danger" | "primary"; }) { return ( ); } function PowerCells({ power, className = "" }: { power: EstimatePower; className?: string }) { return ( <> {formatKw(power.kw)} {formatKw(power.kva)} ); } function groupLabel(group: EstimateGroupSnapshot) { return [group.costGroup, group.title].filter(Boolean).join(" "); } function EstimateRowLine({ disabled, group, onChange, onOpenGzfPicker, onValidityChange, row, rowIndex, }: { disabled: boolean; group: EstimateGroupSnapshot; onChange: EstimateBalanceUpdate; onOpenGzfPicker: (groupId: string, rowId: string) => void; onValidityChange: (fieldKey: string, isValid: boolean) => void; row: EstimateRowSnapshot; rowIndex: number; }) { const result = calculateEstimateRow(row); const rowLabel = `${groupLabel(group)}, Zeile ${rowIndex + 1}`; const fieldKey = (field: string) => `${row.id}:${field}`; function patch(rowPatch: EstimateRowPatch) { onChange((balance) => updateEstimateRow(balance, group.id, row.id, rowPatch)); } return ( patch({ supply })} placeholder="ELT" value={row.supply} /> patch({ name })} placeholder="Verbraucher oder Fläche" value={row.name} /> patch({ quantity })} onValidityChange={onValidityChange} range={estimateNumberRanges.quantity} value={row.quantity} /> {estimateRowUnitLabels[row.unit]} {row.connection ? estimateRowConnectionLabels[row.connection] : ""}
patch({ powerPerUnitW })} onValidityChange={onValidityChange} range={estimateNumberRanges.powerPerUnitW} value={row.powerPerUnitW} /> {row.unit === "area" ? "W/m²" : "W"}
patch({ simultaneityFactor })} onValidityChange={onValidityChange} range={estimateNumberRanges.simultaneityFactor} value={row.simultaneityFactor} />
patch({ cosPhi })} onValidityChange={onValidityChange} range={estimateNumberRanges.cosPhi} value={row.cosPhi} /> patch({ safetySupply: event.target.checked })} type="checkbox" /> {row.safetySupply ? "ja" : "nein"} patch({ remark })} value={row.remark} /> onChange((balance) => removeEstimateRow(balance, group.id, row.id))} variant="danger" > ); } /** * Spreadsheet-like editor in the layout of the office template * "Leistungsbilanz – KG400": one section per cost group with title and * subtotal row, totals, reserve and maximum current at the end. */ export function EstimateBalanceSheet({ balance, disabled, onAddGroup, onChange, onEditGroup, onOpenGzfPicker, onValidityChange, threePhaseVoltageV, }: EstimateBalanceSheetProps) { const result = calculateEstimateBalance(balance, threePhaseVoltageV); function confirmRemoveGroup(group: EstimateGroupSnapshot) { const rowText = group.rows.length === 1 ? "1 Zeile" : `${group.rows.length} Zeilen`; if ( window.confirm( `Kostengruppe „${groupLabel(group)}“ mit ${rowText} wirklich löschen?` ) ) { onChange((current) => removeEstimateGroup(current, group.id)); } } return (
{supplyOptions(balance).map((supply) => ( {balance.groups.map((group, groupIndex) => { const groupResult = result.groups[groupIndex]!; const label = groupLabel(group) || "Kostengruppe"; return ( {group.rows.map((row, rowIndex) => ( ))} {!group.rows.length ? ( ) : null} ); })}
KG Versorgung ELT/MSR … Verbraucher / Flächen Anzahl / Fläche St. / m² Anschl. Fest/Steckd. Einzelleist. W bzw. W/m² GL-Faktor cos φ Nötige AV-Leistung Sicherheitsversorgung (SV) Anmerkungen
Wert Einh. kW kVA ja/nein kW kVA
{group.costGroup}
{group.title}
onChange((current) => addEstimateRow(current, group.id).balance) } variant="primary" > onEditGroup(group)} > onChange((current) => moveEstimateGroup(current, group.id, -1)) } > onChange((current) => moveEstimateGroup(current, group.id, 1)) } > confirmRemoveGroup(group)} variant="danger" >
Noch keine Einträge – mit „+“ eine Zeile hinzufügen.
Summe {label}
Gesamtsumme
Reserveleistung
onChange((current) => ({ ...current, reserveAvPercent })) } onValidityChange={onValidityChange} range={estimateNumberRanges.reservePercent} value={balance.reserveAvPercent} /> %
onChange((current) => ({ ...current, reserveSvPercent })) } onValidityChange={onValidityChange} range={estimateNumberRanges.reservePercent} value={balance.reserveSvPercent} /> %
Leistung inkl. Reserve
Maximalstrom {threePhaseVoltageV > 0 ? ` (3~, U = ${formatDecimalInput(threePhaseVoltageV)} V)` : " (Drehstromspannung im Projekt fehlt)"} {formatAmpere(result.maxCurrentA.av)} A {formatAmpere(result.maxCurrentA.sv)} A
); }