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:
Grovy311 2026-09-26 14:50:48 +02:00
parent 80dbc73230
commit 37a70b6b36
37 changed files with 8537 additions and 4 deletions

View file

@ -1602,3 +1602,493 @@ button.structure-entry-main:focus-visible {
color: var(--color-ink-soft);
font-size: 0.9rem;
}
/* ── Grobe Leistungsbilanz ──────────────────────────────────────────── */
/* Tabelle im Stil der Büro-Vorlage „Leistungsbilanz – KG400“: kompakte
Zellen-Inputs statt Bootstrap-Formularfelder, Druck als A4 quer. */
.estimate-toolbar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-sm);
}
.estimate-save-status {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-sm);
font-size: 0.8rem;
}
.estimate-save-badge {
display: inline-block;
padding: 0.2rem 0.55rem;
border-radius: 999px;
font-weight: 600;
border: 1px solid var(--color-border);
color: var(--color-ink-soft);
background: var(--color-bg);
}
.estimate-save-badge.is-saved {
color: var(--color-signal-dark);
border-color: var(--color-signal);
}
.estimate-save-badge.is-busy {
color: var(--color-warn);
border-color: var(--color-warn);
}
.estimate-save-badge.is-error {
color: var(--color-danger);
border-color: var(--color-danger);
background: var(--color-danger-bg);
}
.estimate-save-hint {
color: var(--color-danger);
}
.estimate-header-fields {
display: flex;
flex-wrap: wrap;
gap: var(--space-sm) var(--space-lg);
margin-bottom: var(--space-md);
}
.estimate-header-field {
display: flex;
align-items: center;
gap: var(--space-sm);
min-width: 10rem;
}
.estimate-header-field.is-wide {
flex: 1 1 18rem;
}
.estimate-header-label {
font-size: 0.7rem;
letter-spacing: 0.06em;
text-transform: uppercase;
font-weight: 600;
color: var(--color-ink-soft);
}
.estimate-header-value {
font-weight: 600;
}
.estimate-header-input {
font-size: 0.95rem;
font-weight: 600;
border-color: var(--panel-border);
}
.estimate-sheet-wrap {
overflow-x: auto;
}
.estimate-sheet {
width: 100%;
min-width: 1400px;
table-layout: fixed;
border-collapse: collapse;
font-size: 0.82rem;
background: var(--panel-bg);
color: var(--text-strong);
}
.estimate-comparison {
min-width: 640px;
table-layout: auto;
}
.estimate-sheet th,
.estimate-sheet td {
border: 1px solid var(--panel-border);
padding: 2px 4px;
vertical-align: middle;
}
.estimate-sheet thead th {
background: var(--surface-header-row);
font-size: 0.72rem;
font-weight: 700;
text-align: center;
line-height: 1.2;
}
.estimate-th-hint {
display: block;
font-size: 0.68rem;
font-weight: 400;
color: var(--text-faint);
}
.estimate-col-kg { width: 3rem; }
.estimate-col-supply { width: 4rem; }
.estimate-col-circuit { width: 4.6rem; }
.estimate-col-name { width: auto; }
.estimate-col-quantity { width: 4.8rem; }
.estimate-col-unit { width: 3.8rem; }
.estimate-col-connection { width: 5rem; }
.estimate-col-power { width: 6.4rem; }
.estimate-col-gzf { width: 6.8rem; }
.estimate-col-cosphi { width: 3.8rem; }
.estimate-col-result { width: 4.6rem; }
.estimate-col-sv { width: 3.2rem; }
.estimate-col-remark { width: 9rem; }
.estimate-col-actions { width: 2.4rem; }
.estimate-cell-input,
.estimate-cell-select {
width: 100%;
min-width: 0;
border: 1px solid transparent;
border-radius: 3px;
background: transparent;
color: inherit;
font: inherit;
padding: 1px 4px;
line-height: 1.4;
}
.estimate-cell-select {
padding-right: 0;
cursor: pointer;
}
.estimate-cell-input:hover:not(:disabled),
.estimate-cell-select:hover:not(:disabled) {
border-color: var(--panel-border-strong);
}
.estimate-cell-input:focus,
.estimate-cell-select:focus {
outline: none;
border-color: var(--accent-blue);
background: var(--panel-bg);
box-shadow: 0 0 0 2px var(--accent-blue-soft-bg);
}
.estimate-cell-input.is-numeric {
text-align: right;
font-variant-numeric: tabular-nums;
}
.estimate-cell-input.is-invalid,
.estimate-cell-input.is-invalid:focus,
.estimate-cell-input.is-invalid:hover {
border-color: var(--color-danger);
background: var(--color-danger-bg);
}
.estimate-cell-input::placeholder {
color: var(--text-faint);
opacity: 0.7;
}
.estimate-unit-input,
.estimate-gzf-input {
display: flex;
align-items: center;
gap: 2px;
}
.estimate-unit-label {
flex: 0 0 auto;
font-size: 0.7rem;
color: var(--text-faint);
min-width: 1.9rem;
}
.estimate-amev-button {
flex: 0 0 auto;
border: 1px solid var(--panel-border-strong);
border-radius: 3px;
background: var(--panel-bg-subtle);
color: var(--text-subtle);
font-size: 0.6rem;
font-weight: 700;
letter-spacing: 0.03em;
padding: 1px 3px;
line-height: 1.3;
}
.estimate-amev-button:hover:not(:disabled) {
border-color: var(--accent-blue);
color: var(--accent-blue);
}
.estimate-number {
text-align: right;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.estimate-number.is-muted {
color: var(--text-faint);
}
.estimate-number.is-over {
color: var(--color-attention-dark);
}
.estimate-number.is-under {
color: var(--color-signal-dark);
}
.estimate-center {
text-align: center;
}
.estimate-cost-group-cell {
font-weight: 700;
text-align: center;
font-variant-numeric: tabular-nums;
}
.estimate-group-title-row td {
background: var(--surface-component-header);
font-weight: 700;
}
.estimate-group-title {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-xs) var(--space-lg);
}
.estimate-group-name {
font-size: 0.9rem;
}
.estimate-group-editor {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
font-weight: 400;
font-size: 0.75rem;
color: var(--text-muted);
}
.estimate-group-editor .estimate-cell-input {
width: 12rem;
}
.estimate-group-actions {
display: flex;
justify-content: flex-end;
gap: 2px;
}
.estimate-group-title-row .estimate-actions-cell {
width: auto;
white-space: nowrap;
}
.estimate-icon-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.15rem 0.3rem;
line-height: 1;
}
.estimate-actions-cell {
text-align: center;
}
.estimate-empty-row td {
color: var(--text-faint);
font-style: italic;
}
.estimate-subtotal-row td {
background: var(--panel-bg-subtle);
font-weight: 700;
}
.estimate-total-label {
text-align: right;
font-weight: 700;
}
.estimate-total-hint {
font-weight: 400;
color: var(--text-faint);
}
.estimate-total-row td {
background: var(--surface-header-row);
font-weight: 600;
}
.estimate-total-row.is-strong td {
font-weight: 800;
border-top: 2px solid var(--panel-border-strong);
}
.estimate-add-group-row td {
background: var(--panel-bg);
padding: var(--space-sm) 4px;
}
.estimate-unassigned-row td {
font-style: italic;
color: var(--text-muted);
}
.estimate-collapse-toggle {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
border: 0;
background: transparent;
color: inherit;
font: inherit;
font-weight: 700;
padding: 0;
}
.estimate-list-table .estimate-number {
font-variant-numeric: tabular-nums;
}
/* Druck-Textwerte: auf dem Bildschirm unsichtbar, im Druck statt Inputs */
.estimate-print-value {
display: none;
}
@page estimate-balance {
size: A4 landscape;
margin: 10mm;
}
@media print {
body:has(.estimate-print-root) .sidebar,
.estimate-print-root .estimate-no-print,
.estimate-print-root .alert,
.estimate-print-root input,
.estimate-print-root select,
.estimate-print-root button {
display: none !important;
}
body:has(.estimate-print-root) {
background: #fff !important;
}
body:has(.estimate-print-root) .app-shell {
display: block;
}
.estimate-print-root {
page: estimate-balance;
padding: 0 !important;
max-width: none;
color: #000;
}
.estimate-print-root .estimate-print-value {
display: inline;
}
.estimate-print-root .page-header {
margin-bottom: 4mm;
}
.estimate-print-root .page-header h1 {
font-size: 14pt;
color: #000;
}
.estimate-print-root .card {
border: 0;
box-shadow: none;
background: transparent;
}
.estimate-print-root .card-body,
.estimate-print-root .card-header {
padding: 0;
background: transparent;
border: 0;
}
.estimate-print-root .estimate-comparison-card {
margin-top: 6mm;
break-inside: avoid;
}
.estimate-sheet-wrap {
overflow: visible;
}
.estimate-sheet {
min-width: 0;
table-layout: auto;
font-size: 7.5pt;
background: #fff;
color: #000;
}
.estimate-sheet th,
.estimate-sheet td {
border-color: #999;
padding: 1px 3px;
}
.estimate-sheet thead {
display: table-header-group;
}
.estimate-sheet thead th,
.estimate-total-row td {
background: #e8e8e8 !important;
}
.estimate-group-title-row td {
background: #d9d9d9 !important;
}
.estimate-subtotal-row td {
background: #f2f2f2 !important;
}
.estimate-group-title-row,
.estimate-subtotal-row,
.estimate-total-row {
break-inside: avoid;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.estimate-group-title-row {
break-after: avoid;
}
/* Totals print once at the end, not repeated on every page. */
.estimate-sheet tfoot {
display: table-row-group;
break-inside: avoid;
}
.estimate-col-actions {
width: 0;
}
.estimate-unit-input,
.estimate-gzf-input {
justify-content: flex-end;
}
.estimate-header-fields {
margin-bottom: 3mm;
}
}