Add the rough power balance module for early design phases
In LPH 2/3 there are no circuits yet, but the office needs a power balance by DIN 276 cost group, as in the Excel template "Leistungsbilanz - KG400": each trade enters consumers by piece or by area (W/m²) with GLF and cos phi; the balance sums AV and SV per cost group, adds a reserve and derives the maximum current. The module is separate from the circuit structure: own tables (migration 0010), own folder src/estimate-balance and an import-free shared calculation used by API and UI. Writes go through project commands (estimate-balance.insert/.update/.delete with complete documents), so revision checks and Undo/Redo apply; it is not part of project snapshots or transfer yet. A comparison endpoint sums the device rows of all circuit lists by cost group (longest prefix) against the estimate. The UI adds a sidebar entry, a list of Stände (template KG 400, empty or copy) and a spreadsheet-like editor with autosave, AMEV picker for the GLF, totals, reserve, maximum current, comparison table and print layout. The tests reproduce the template example exactly (143.24 kW, 148.48 kVA, 157.56 kW / 163.33 kVA with 10 % reserve, 235.75 A). See docs/estimate-balance.md and issue #7. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
80dbc73230
commit
37a70b6b36
37 changed files with 8537 additions and 4 deletions
162
src/frontend/estimate-balance/estimate-balance-create-modal.tsx
Normal file
162
src/frontend/estimate-balance/estimate-balance-create-modal.tsx
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
"use client";
|
||||
|
||||
import { type FormEvent, useState } from "react";
|
||||
import { FormModal } from "../components/form-modal";
|
||||
import type {
|
||||
CreateEstimateBalanceInput,
|
||||
EstimateBalanceDto,
|
||||
EstimateBalanceTemplate,
|
||||
} from "../types";
|
||||
import { estimateTextLimits } from "./estimate-balance-document";
|
||||
import { getTodayIsoDate } from "./estimate-balance-format";
|
||||
|
||||
export interface EstimateBalanceCreateDefaults {
|
||||
name?: string;
|
||||
workStage?: string | null;
|
||||
template?: EstimateBalanceTemplate;
|
||||
copyFromBalanceId?: string;
|
||||
}
|
||||
|
||||
interface EstimateBalanceCreateModalProps {
|
||||
balances: EstimateBalanceDto[];
|
||||
defaults: EstimateBalanceCreateDefaults;
|
||||
isSaving: boolean;
|
||||
onClose: () => void;
|
||||
onCreate: (input: CreateEstimateBalanceInput) => void;
|
||||
}
|
||||
|
||||
/** "Neuer Stand": name, LPH, date and the starting content. */
|
||||
export function EstimateBalanceCreateModal({
|
||||
balances,
|
||||
defaults,
|
||||
isSaving,
|
||||
onClose,
|
||||
onCreate,
|
||||
}: EstimateBalanceCreateModalProps) {
|
||||
const [name, setName] = useState(defaults.name ?? "");
|
||||
const [workStage, setWorkStage] = useState(defaults.workStage ?? "");
|
||||
const [dateIso, setDateIso] = useState(getTodayIsoDate());
|
||||
const [template, setTemplate] = useState<EstimateBalanceTemplate>(
|
||||
defaults.template ?? "kg400"
|
||||
);
|
||||
const [copyFromBalanceId, setCopyFromBalanceId] = useState(
|
||||
defaults.copyFromBalanceId ?? balances[0]?.id ?? ""
|
||||
);
|
||||
const isCopyInvalid = template === "copy" && !copyFromBalanceId;
|
||||
|
||||
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!name.trim() || isCopyInvalid) {
|
||||
return;
|
||||
}
|
||||
onCreate({
|
||||
name: name.trim(),
|
||||
workStage: workStage.trim() || null,
|
||||
dateIso: dateIso || null,
|
||||
template,
|
||||
...(template === "copy" ? { copyFromBalanceId } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<FormModal
|
||||
isSaving={isSaving}
|
||||
onClose={onClose}
|
||||
onSubmit={handleSubmit}
|
||||
submitDisabled={!name.trim() || isCopyInvalid}
|
||||
submitLabel="Stand anlegen"
|
||||
title="Neuer Stand"
|
||||
>
|
||||
<div className="row g-3">
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label" htmlFor="estimate-create-name">
|
||||
Bezeichnung
|
||||
</label>
|
||||
<input
|
||||
autoFocus
|
||||
className="form-control"
|
||||
id="estimate-create-name"
|
||||
maxLength={estimateTextLimits.balanceName}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
placeholder="z. B. Entwurfsplanung"
|
||||
required
|
||||
value={name}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-md-3">
|
||||
<label className="form-label" htmlFor="estimate-create-work-stage">
|
||||
LPH
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="estimate-create-work-stage"
|
||||
maxLength={estimateTextLimits.workStage}
|
||||
onChange={(event) => setWorkStage(event.target.value)}
|
||||
placeholder="z. B. LPH 3"
|
||||
value={workStage}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6 col-md-3">
|
||||
<label className="form-label" htmlFor="estimate-create-date">
|
||||
Datum
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
id="estimate-create-date"
|
||||
onChange={(event) => setDateIso(event.target.value)}
|
||||
type="date"
|
||||
value={dateIso}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label" htmlFor="estimate-create-template">
|
||||
Inhalt
|
||||
</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="estimate-create-template"
|
||||
onChange={(event) =>
|
||||
setTemplate(event.target.value as EstimateBalanceTemplate)
|
||||
}
|
||||
value={template}
|
||||
>
|
||||
<option value="kg400">Vorlage KG 400</option>
|
||||
<option value="empty">Leer</option>
|
||||
<option disabled={!balances.length} value="copy">
|
||||
Kopie von …
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
{template === "copy" ? (
|
||||
<div className="col-12 col-md-6">
|
||||
<label className="form-label" htmlFor="estimate-create-copy-from">
|
||||
Kopie von Stand
|
||||
</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="estimate-create-copy-from"
|
||||
onChange={(event) => setCopyFromBalanceId(event.target.value)}
|
||||
required
|
||||
value={copyFromBalanceId}
|
||||
>
|
||||
{balances.map((balance) => (
|
||||
<option key={balance.id} value={balance.id}>
|
||||
{balance.workStage ? `${balance.name} (${balance.workStage})` : balance.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="col-12">
|
||||
<p className="text-secondary small mb-0">
|
||||
{template === "kg400"
|
||||
? "Legt die Kostengruppen 410 bis 480 und „Allgemein“ ohne Einträge an."
|
||||
: template === "empty"
|
||||
? "Legt einen Stand ohne Kostengruppen an."
|
||||
: "Übernimmt alle Kostengruppen und Zeilen des gewählten Stands."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</FormModal>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue