forked from jappel/leistungsbilanz-ts
The printed page kept the dark app background in the page margins, the consumer column wrapped while the empty remark column took the space, an empty "bearbeitet von" label was printed, and numbers were printed as typed (11200). Print margins are white now, the consumer column is wider, empty editors are omitted and numbers print with thousands separators (11.200) like the template. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
669 lines
22 KiB
TypeScript
669 lines
22 KiB
TypeScript
"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 (
|
||
<DraftInput
|
||
ariaLabel={ariaLabel}
|
||
className="estimate-cell-input is-numeric"
|
||
disabled={disabled}
|
||
format={(current: number) => 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 (
|
||
<>
|
||
<input
|
||
aria-label={ariaLabel}
|
||
className="estimate-cell-input"
|
||
disabled={disabled}
|
||
list={list}
|
||
maxLength={maxLength}
|
||
onChange={(event) => onChange(event.target.value)}
|
||
placeholder={placeholder}
|
||
type="text"
|
||
value={value ?? ""}
|
||
/>
|
||
<span className="estimate-print-value">{value ?? ""}</span>
|
||
</>
|
||
);
|
||
}
|
||
|
||
function IconButton({
|
||
children,
|
||
disabled,
|
||
label,
|
||
onClick,
|
||
variant = "secondary",
|
||
}: {
|
||
children: ReactNode;
|
||
disabled: boolean;
|
||
label: string;
|
||
onClick: () => void;
|
||
variant?: "secondary" | "danger" | "primary";
|
||
}) {
|
||
return (
|
||
<button
|
||
aria-label={label}
|
||
className={`btn btn-sm btn-outline-${variant} estimate-icon-button`}
|
||
disabled={disabled}
|
||
onClick={onClick}
|
||
title={label}
|
||
type="button"
|
||
>
|
||
{children}
|
||
</button>
|
||
);
|
||
}
|
||
|
||
function PowerCells({ power, className = "" }: { power: EstimatePower; className?: string }) {
|
||
return (
|
||
<>
|
||
<td className={`estimate-number ${className}`}>{formatKw(power.kw)}</td>
|
||
<td className={`estimate-number ${className}`}>{formatKw(power.kva)}</td>
|
||
</>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<tr className="estimate-row">
|
||
<td className="estimate-cost-group-cell" />
|
||
<td>
|
||
<TextInput
|
||
ariaLabel={`Versorgung (${rowLabel})`}
|
||
disabled={disabled}
|
||
list={SUPPLY_LIST_ID}
|
||
maxLength={estimateTextLimits.supply}
|
||
onChange={(supply) => patch({ supply })}
|
||
placeholder="ELT"
|
||
value={row.supply}
|
||
/>
|
||
</td>
|
||
<td className="estimate-name-cell">
|
||
<TextInput
|
||
ariaLabel={`Verbraucher / Fläche (${rowLabel})`}
|
||
disabled={disabled}
|
||
maxLength={estimateTextLimits.rowName}
|
||
onChange={(name) => patch({ name })}
|
||
placeholder="Verbraucher oder Fläche"
|
||
value={row.name}
|
||
/>
|
||
</td>
|
||
<td>
|
||
<NumberInput
|
||
ariaLabel={`Anzahl / Fläche (${rowLabel})`}
|
||
disabled={disabled}
|
||
fieldKey={fieldKey("quantity")}
|
||
onCommit={(quantity) => patch({ quantity })}
|
||
onValidityChange={onValidityChange}
|
||
range={estimateNumberRanges.quantity}
|
||
value={row.quantity}
|
||
/>
|
||
</td>
|
||
<td>
|
||
<select
|
||
aria-label={`Einheit (${rowLabel})`}
|
||
className="estimate-cell-select"
|
||
disabled={disabled}
|
||
onChange={(event) => patch({ unit: event.target.value as EstimateRowUnit })}
|
||
value={row.unit}
|
||
>
|
||
{(Object.keys(estimateRowUnitLabels) as EstimateRowUnit[]).map((unit) => (
|
||
<option key={unit} value={unit}>
|
||
{estimateRowUnitLabels[unit]}
|
||
</option>
|
||
))}
|
||
</select>
|
||
<span className="estimate-print-value">{estimateRowUnitLabels[row.unit]}</span>
|
||
</td>
|
||
<td>
|
||
<select
|
||
aria-label={`Anschluss (${rowLabel})`}
|
||
className="estimate-cell-select"
|
||
disabled={disabled}
|
||
onChange={(event) =>
|
||
patch({
|
||
connection: (event.target.value || null) as EstimateRowConnection | null,
|
||
})
|
||
}
|
||
value={row.connection ?? ""}
|
||
>
|
||
<option value="">–</option>
|
||
{(Object.keys(estimateRowConnectionLabels) as EstimateRowConnection[]).map(
|
||
(connection) => (
|
||
<option key={connection} value={connection}>
|
||
{estimateRowConnectionLabels[connection]}
|
||
</option>
|
||
)
|
||
)}
|
||
</select>
|
||
<span className="estimate-print-value">
|
||
{row.connection ? estimateRowConnectionLabels[row.connection] : ""}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<div className="estimate-unit-input">
|
||
<NumberInput
|
||
ariaLabel={`Einzelleistung in ${row.unit === "area" ? "W/m²" : "W"} (${rowLabel})`}
|
||
disabled={disabled}
|
||
fieldKey={fieldKey("powerPerUnitW")}
|
||
onCommit={(powerPerUnitW) => patch({ powerPerUnitW })}
|
||
onValidityChange={onValidityChange}
|
||
range={estimateNumberRanges.powerPerUnitW}
|
||
value={row.powerPerUnitW}
|
||
/>
|
||
<span className="estimate-unit-label">{row.unit === "area" ? "W/m²" : "W"}</span>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<div className="estimate-gzf-input">
|
||
<NumberInput
|
||
ariaLabel={`Gleichzeitigkeitsfaktor (${rowLabel})`}
|
||
disabled={disabled}
|
||
fieldKey={fieldKey("simultaneityFactor")}
|
||
onCommit={(simultaneityFactor) => patch({ simultaneityFactor })}
|
||
onValidityChange={onValidityChange}
|
||
range={estimateNumberRanges.simultaneityFactor}
|
||
value={row.simultaneityFactor}
|
||
/>
|
||
<button
|
||
aria-label={`Gleichzeitigkeitsfaktor nach AMEV wählen (${rowLabel})`}
|
||
className="estimate-amev-button estimate-no-print"
|
||
disabled={disabled}
|
||
onClick={() => onOpenGzfPicker(group.id, row.id)}
|
||
title="Gleichzeitigkeitsfaktor nach AMEV wählen"
|
||
type="button"
|
||
>
|
||
AMEV
|
||
</button>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<NumberInput
|
||
ariaLabel={`cos φ (${rowLabel})`}
|
||
disabled={disabled}
|
||
fieldKey={fieldKey("cosPhi")}
|
||
onCommit={(cosPhi) => patch({ cosPhi })}
|
||
onValidityChange={onValidityChange}
|
||
range={estimateNumberRanges.cosPhi}
|
||
value={row.cosPhi}
|
||
/>
|
||
</td>
|
||
<PowerCells power={result.av} />
|
||
<td className="estimate-center">
|
||
<input
|
||
aria-label={`Sicherheitsversorgung (${rowLabel})`}
|
||
checked={row.safetySupply}
|
||
className="form-check-input estimate-no-print"
|
||
disabled={disabled}
|
||
onChange={(event) => patch({ safetySupply: event.target.checked })}
|
||
type="checkbox"
|
||
/>
|
||
<span className="estimate-print-value">{row.safetySupply ? "ja" : "nein"}</span>
|
||
</td>
|
||
<PowerCells
|
||
className={row.safetySupply ? "" : "is-muted"}
|
||
power={result.sv}
|
||
/>
|
||
<td>
|
||
<TextInput
|
||
ariaLabel={`Anmerkung (${rowLabel})`}
|
||
disabled={disabled}
|
||
maxLength={estimateTextLimits.remark}
|
||
onChange={(remark) => patch({ remark })}
|
||
value={row.remark}
|
||
/>
|
||
</td>
|
||
<td className="estimate-actions-cell estimate-no-print">
|
||
<IconButton
|
||
disabled={disabled}
|
||
label={`Zeile löschen (${rowLabel})`}
|
||
onClick={() => onChange((balance) => removeEstimateRow(balance, group.id, row.id))}
|
||
variant="danger"
|
||
>
|
||
<Trash2 aria-hidden="true" size={13} />
|
||
</IconButton>
|
||
</td>
|
||
</tr>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 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 (
|
||
<div className="estimate-sheet-wrap">
|
||
<datalist id={SUPPLY_LIST_ID}>
|
||
{supplyOptions(balance).map((supply) => (
|
||
<option key={supply} value={supply} />
|
||
))}
|
||
</datalist>
|
||
<table className="estimate-sheet">
|
||
<colgroup>
|
||
<col className="estimate-col-kg" />
|
||
<col className="estimate-col-supply" />
|
||
<col className="estimate-col-name" />
|
||
<col className="estimate-col-quantity" />
|
||
<col className="estimate-col-unit" />
|
||
<col className="estimate-col-connection" />
|
||
<col className="estimate-col-power" />
|
||
<col className="estimate-col-gzf" />
|
||
<col className="estimate-col-cosphi" />
|
||
<col className="estimate-col-result" />
|
||
<col className="estimate-col-result" />
|
||
<col className="estimate-col-sv" />
|
||
<col className="estimate-col-result" />
|
||
<col className="estimate-col-result" />
|
||
<col className="estimate-col-remark" />
|
||
<col className="estimate-col-actions" />
|
||
</colgroup>
|
||
<thead>
|
||
<tr>
|
||
<th rowSpan={2}>KG</th>
|
||
<th rowSpan={2}>
|
||
Versorgung
|
||
<span className="estimate-th-hint">ELT/MSR …</span>
|
||
</th>
|
||
<th rowSpan={2}>Verbraucher / Flächen</th>
|
||
<th colSpan={2}>
|
||
Anzahl / Fläche
|
||
<span className="estimate-th-hint">St. / m²</span>
|
||
</th>
|
||
<th rowSpan={2}>
|
||
Anschl.
|
||
<span className="estimate-th-hint">Fest/Steckd.</span>
|
||
</th>
|
||
<th rowSpan={2}>
|
||
Einzelleist.
|
||
<span className="estimate-th-hint">W bzw. W/m²</span>
|
||
</th>
|
||
<th rowSpan={2}>GL-Faktor</th>
|
||
<th rowSpan={2}>cos φ</th>
|
||
<th colSpan={2}>Nötige AV-Leistung</th>
|
||
<th colSpan={3}>Sicherheitsversorgung (SV)</th>
|
||
<th rowSpan={2}>Anmerkungen</th>
|
||
<th aria-label="Aktionen" className="estimate-no-print" rowSpan={2} />
|
||
</tr>
|
||
<tr>
|
||
<th>Wert</th>
|
||
<th>Einh.</th>
|
||
<th>kW</th>
|
||
<th>kVA</th>
|
||
<th>ja/nein</th>
|
||
<th>kW</th>
|
||
<th>kVA</th>
|
||
</tr>
|
||
</thead>
|
||
{balance.groups.map((group, groupIndex) => {
|
||
const groupResult = result.groups[groupIndex]!;
|
||
const label = groupLabel(group) || "Kostengruppe";
|
||
return (
|
||
<tbody className="estimate-group" key={group.id}>
|
||
<tr className="estimate-group-title-row">
|
||
<td className="estimate-cost-group-cell">{group.costGroup}</td>
|
||
<td colSpan={COLUMN_COUNT - 2}>
|
||
<div className="estimate-group-title">
|
||
<span className="estimate-group-name">{group.title}</span>
|
||
<label
|
||
className={
|
||
group.editor?.trim()
|
||
? "estimate-group-editor"
|
||
: "estimate-group-editor is-empty"
|
||
}
|
||
>
|
||
<span>bearbeitet von</span>
|
||
<input
|
||
aria-label={`Bearbeitet von (${label})`}
|
||
className="estimate-cell-input"
|
||
disabled={disabled}
|
||
maxLength={estimateTextLimits.groupEditor}
|
||
onChange={(event) =>
|
||
onChange((current) =>
|
||
updateEstimateGroup(current, group.id, {
|
||
editor: event.target.value,
|
||
})
|
||
)
|
||
}
|
||
placeholder="Gewerk / Name"
|
||
type="text"
|
||
value={group.editor ?? ""}
|
||
/>
|
||
<span className="estimate-print-value">{group.editor ?? ""}</span>
|
||
</label>
|
||
</div>
|
||
</td>
|
||
<td className="estimate-actions-cell estimate-no-print">
|
||
<div className="estimate-group-actions">
|
||
<IconButton
|
||
disabled={disabled}
|
||
label={`Zeile hinzufügen (${label})`}
|
||
onClick={() =>
|
||
onChange((current) => addEstimateRow(current, group.id).balance)
|
||
}
|
||
variant="primary"
|
||
>
|
||
<Plus aria-hidden="true" size={13} />
|
||
</IconButton>
|
||
<IconButton
|
||
disabled={disabled}
|
||
label={`Gruppe bearbeiten (${label})`}
|
||
onClick={() => onEditGroup(group)}
|
||
>
|
||
<Pencil aria-hidden="true" size={13} />
|
||
</IconButton>
|
||
<IconButton
|
||
disabled={disabled || groupIndex === 0}
|
||
label={`Gruppe nach oben (${label})`}
|
||
onClick={() =>
|
||
onChange((current) => moveEstimateGroup(current, group.id, -1))
|
||
}
|
||
>
|
||
<ArrowUp aria-hidden="true" size={13} />
|
||
</IconButton>
|
||
<IconButton
|
||
disabled={disabled || groupIndex === balance.groups.length - 1}
|
||
label={`Gruppe nach unten (${label})`}
|
||
onClick={() =>
|
||
onChange((current) => moveEstimateGroup(current, group.id, 1))
|
||
}
|
||
>
|
||
<ArrowDown aria-hidden="true" size={13} />
|
||
</IconButton>
|
||
<IconButton
|
||
disabled={disabled}
|
||
label={`Gruppe löschen (${label})`}
|
||
onClick={() => confirmRemoveGroup(group)}
|
||
variant="danger"
|
||
>
|
||
<Trash2 aria-hidden="true" size={13} />
|
||
</IconButton>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{group.rows.map((row, rowIndex) => (
|
||
<EstimateRowLine
|
||
disabled={disabled}
|
||
group={group}
|
||
key={row.id}
|
||
onChange={onChange}
|
||
onOpenGzfPicker={onOpenGzfPicker}
|
||
onValidityChange={onValidityChange}
|
||
row={row}
|
||
rowIndex={rowIndex}
|
||
/>
|
||
))}
|
||
{!group.rows.length ? (
|
||
<tr className="estimate-empty-row estimate-no-print">
|
||
<td />
|
||
<td colSpan={COLUMN_COUNT - 1}>
|
||
Noch keine Einträge – mit „+“ eine Zeile hinzufügen.
|
||
</td>
|
||
</tr>
|
||
) : null}
|
||
<tr className="estimate-subtotal-row">
|
||
<td colSpan={LEADING_COLUMN_COUNT} className="estimate-total-label">
|
||
Summe {label}
|
||
</td>
|
||
<PowerCells power={groupResult.av} />
|
||
<td />
|
||
<PowerCells power={groupResult.sv} />
|
||
<td />
|
||
<td className="estimate-no-print" />
|
||
</tr>
|
||
</tbody>
|
||
);
|
||
})}
|
||
<tfoot>
|
||
<tr className="estimate-no-print estimate-add-group-row">
|
||
<td colSpan={COLUMN_COUNT}>
|
||
<button
|
||
className="btn btn-sm btn-outline-primary"
|
||
disabled={disabled}
|
||
onClick={onAddGroup}
|
||
type="button"
|
||
>
|
||
<Plus aria-hidden="true" className="me-1" size={14} />
|
||
Kostengruppe hinzufügen
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
<tr className="estimate-total-row">
|
||
<td colSpan={LEADING_COLUMN_COUNT} className="estimate-total-label">
|
||
Gesamtsumme
|
||
</td>
|
||
<PowerCells power={result.total.av} />
|
||
<td />
|
||
<PowerCells power={result.total.sv} />
|
||
<td colSpan={2} />
|
||
</tr>
|
||
<tr className="estimate-total-row">
|
||
<td colSpan={LEADING_COLUMN_COUNT - 1} className="estimate-total-label">
|
||
Reserveleistung
|
||
</td>
|
||
<td>
|
||
<div className="estimate-unit-input">
|
||
<NumberInput
|
||
ariaLabel="Reserve AV in Prozent"
|
||
disabled={disabled}
|
||
fieldKey="balance:reserveAvPercent"
|
||
onCommit={(reserveAvPercent) =>
|
||
onChange((current) => ({ ...current, reserveAvPercent }))
|
||
}
|
||
onValidityChange={onValidityChange}
|
||
range={estimateNumberRanges.reservePercent}
|
||
value={balance.reserveAvPercent}
|
||
/>
|
||
<span className="estimate-unit-label">%</span>
|
||
</div>
|
||
</td>
|
||
<PowerCells power={result.reserve.av} />
|
||
<td>
|
||
<div className="estimate-unit-input">
|
||
<NumberInput
|
||
ariaLabel="Reserve SV in Prozent"
|
||
disabled={disabled}
|
||
fieldKey="balance:reserveSvPercent"
|
||
onCommit={(reserveSvPercent) =>
|
||
onChange((current) => ({ ...current, reserveSvPercent }))
|
||
}
|
||
onValidityChange={onValidityChange}
|
||
range={estimateNumberRanges.reservePercent}
|
||
value={balance.reserveSvPercent}
|
||
/>
|
||
<span className="estimate-unit-label">%</span>
|
||
</div>
|
||
</td>
|
||
<PowerCells power={result.reserve.sv} />
|
||
<td colSpan={2} />
|
||
</tr>
|
||
<tr className="estimate-total-row is-strong">
|
||
<td colSpan={LEADING_COLUMN_COUNT} className="estimate-total-label">
|
||
Leistung inkl. Reserve
|
||
</td>
|
||
<PowerCells power={result.withReserve.av} />
|
||
<td />
|
||
<PowerCells power={result.withReserve.sv} />
|
||
<td colSpan={2} />
|
||
</tr>
|
||
<tr className="estimate-total-row">
|
||
<td colSpan={LEADING_COLUMN_COUNT} className="estimate-total-label">
|
||
Maximalstrom
|
||
<span className="estimate-total-hint">
|
||
{threePhaseVoltageV > 0
|
||
? ` (3~, U = ${formatDecimalInput(threePhaseVoltageV)} V)`
|
||
: " (Drehstromspannung im Projekt fehlt)"}
|
||
</span>
|
||
</td>
|
||
<td className="estimate-number" colSpan={2}>
|
||
{formatAmpere(result.maxCurrentA.av)} A
|
||
</td>
|
||
<td />
|
||
<td className="estimate-number" colSpan={2}>
|
||
{formatAmpere(result.maxCurrentA.sv)} A
|
||
</td>
|
||
<td colSpan={2} />
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
);
|
||
}
|