forked from jappel/leistungsbilanz-ts
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>
128 lines
4.2 KiB
TypeScript
128 lines
4.2 KiB
TypeScript
"use client";
|
||
|
||
import { type FormEvent, useState } from "react";
|
||
import { FormModal } from "../components/form-modal";
|
||
import {
|
||
estimateDefaultGroups,
|
||
type EstimateGroupSnapshot,
|
||
} from "../../shared/estimate-balance/estimate-balance";
|
||
import { estimateTextLimits } from "./estimate-balance-document";
|
||
|
||
export interface EstimateGroupFormValues {
|
||
costGroup: string;
|
||
title: string;
|
||
editor: string | null;
|
||
}
|
||
|
||
interface EstimateGroupModalProps {
|
||
group: EstimateGroupSnapshot | null;
|
||
onClose: () => void;
|
||
onSave: (values: EstimateGroupFormValues) => void;
|
||
}
|
||
|
||
/** Creates or edits the KG number, title and "bearbeitet von" of one group. */
|
||
export function EstimateGroupModal({ group, onClose, onSave }: EstimateGroupModalProps) {
|
||
const [costGroup, setCostGroup] = useState(group?.costGroup ?? "");
|
||
const [title, setTitle] = useState(group?.title ?? "");
|
||
const [editor, setEditor] = useState(group?.editor ?? "");
|
||
|
||
function applyTemplate(index: string) {
|
||
const template = estimateDefaultGroups[Number(index)];
|
||
if (template) {
|
||
setCostGroup(template.costGroup);
|
||
setTitle(template.title);
|
||
}
|
||
}
|
||
|
||
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||
event.preventDefault();
|
||
if (!title.trim()) {
|
||
return;
|
||
}
|
||
onSave({
|
||
costGroup: costGroup.trim(),
|
||
title: title.trim(),
|
||
editor: editor.trim() || null,
|
||
});
|
||
}
|
||
|
||
return (
|
||
<FormModal
|
||
isSaving={false}
|
||
onClose={onClose}
|
||
onSubmit={handleSubmit}
|
||
submitDisabled={!title.trim()}
|
||
submitLabel={group ? "Übernehmen" : "Kostengruppe hinzufügen"}
|
||
title={group ? "Kostengruppe bearbeiten" : "Kostengruppe hinzufügen"}
|
||
>
|
||
<div className="row g-3">
|
||
{!group ? (
|
||
<div className="col-12">
|
||
<label className="form-label" htmlFor="estimate-group-template">
|
||
Aus Vorlage KG 400 übernehmen
|
||
</label>
|
||
<select
|
||
className="form-select"
|
||
defaultValue=""
|
||
id="estimate-group-template"
|
||
onChange={(event) => applyTemplate(event.target.value)}
|
||
>
|
||
<option value="">– Vorlage wählen –</option>
|
||
{estimateDefaultGroups.map((template, index) => (
|
||
<option key={`${template.costGroup}-${template.title}`} value={index}>
|
||
{[template.costGroup, template.title].filter(Boolean).join(" ")}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
) : null}
|
||
<div className="col-12 col-md-3">
|
||
<label className="form-label" htmlFor="estimate-group-cost-group">
|
||
KG (DIN 276)
|
||
</label>
|
||
<input
|
||
className="form-control"
|
||
id="estimate-group-cost-group"
|
||
maxLength={estimateTextLimits.costGroup}
|
||
onChange={(event) => setCostGroup(event.target.value)}
|
||
placeholder="z. B. 440"
|
||
value={costGroup}
|
||
/>
|
||
</div>
|
||
<div className="col-12 col-md-9">
|
||
<label className="form-label" htmlFor="estimate-group-title">
|
||
Titel
|
||
</label>
|
||
<input
|
||
className="form-control"
|
||
id="estimate-group-title"
|
||
maxLength={estimateTextLimits.groupTitle}
|
||
onChange={(event) => setTitle(event.target.value)}
|
||
placeholder="z. B. Starkstromanlagen"
|
||
required
|
||
value={title}
|
||
/>
|
||
</div>
|
||
<div className="col-12">
|
||
<label className="form-label" htmlFor="estimate-group-editor">
|
||
Bearbeitet von
|
||
</label>
|
||
<input
|
||
className="form-control"
|
||
id="estimate-group-editor"
|
||
maxLength={estimateTextLimits.groupEditor}
|
||
onChange={(event) => setEditor(event.target.value)}
|
||
placeholder="Gewerk oder Name (optional)"
|
||
value={editor}
|
||
/>
|
||
</div>
|
||
<div className="col-12">
|
||
<p className="text-secondary small mb-0">
|
||
Ohne KG-Nummer (z. B. „Allgemein“) wird die Gruppe im Vergleich mit der
|
||
detaillierten Bilanz keiner Kostengruppe zugeordnet.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</FormModal>
|
||
);
|
||
}
|