diff --git a/docs/estimate-balance.md b/docs/estimate-balance.md index afe4780..4e43cec 100644 --- a/docs/estimate-balance.md +++ b/docs/estimate-balance.md @@ -34,7 +34,7 @@ das Frontend sie direkt nutzt (Turbopack löst die `.js`-Suffixe der API nicht a |---|---| | `estimate_balances` | ein **Stand** je Projekt (z. B. Vorplanung, Entwurfsplanung): Name, LPH, Datum, Reserve AV/SV in %, Sortierung | | `estimate_groups` | Kostengruppe (`"410"` … `"480"`, `""` für „Allgemein“), Titel, „bearbeitet von“, Sortierung | -| `estimate_rows` | Versorgung (Auswahl ELT/MSR oder Freitext), Verbraucher/Fläche, Anzahl bzw. Fläche, Einheit (`piece` = St., `area` = m²), Anschluss (fest/Steckdose), Einzelleistung in W bzw. W/m², GLF, cos φ, SV ja/nein, Anmerkung | +| `estimate_rows` | Versorgung, Stromkreis-Nr. (Freitext), Verbraucher/Fläche, Anzahl bzw. Fläche, Einheit (`piece` = St., `area` = m²), Anschluss (fest/Steckdose), Einzelleistung in W bzw. W/m², GLF, cos φ, SV ja/nein, Anmerkung | Neue Stände entstehen aus der Vorlage KG 400 (410 Sanitär, 420 Wärme, 430 Luft, 434 Kälte, 440 Starkstrom, 450 Schwachstrom, 460 Förder, 470 Nutzungsspezifisch, diff --git a/src/app/globals.css b/src/app/globals.css index dbf49fb..b3f526a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1731,6 +1731,7 @@ button.structure-entry-main:focus-visible { .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; } @@ -1964,53 +1965,14 @@ button.structure-entry-main:focus-visible { display: none; } -/* Ausrichtung: alle Zellen zentriert, nur „Verbraucher / Flächen“, die - Gruppentitel und die Summenbeschriftungen stehen links bzw. rechts. */ -.estimate-sheet td, -.estimate-sheet td.estimate-number, -.estimate-sheet .estimate-cell-input, -.estimate-sheet .estimate-cell-input.is-numeric { - text-align: center; -} - -.estimate-sheet .estimate-cell-select { - text-align: center; - text-align-last: center; -} - -.estimate-sheet .estimate-unit-input, -.estimate-sheet .estimate-gzf-input { - justify-content: center; -} - -.estimate-sheet td.estimate-name-cell, -.estimate-sheet td.estimate-name-cell .estimate-cell-input, -.estimate-sheet .estimate-group-title-row td:not(.estimate-cost-group-cell), -.estimate-comparison td:nth-child(2) { - text-align: left; -} - -.estimate-sheet td.estimate-total-label { - text-align: right; -} - -/* Druck im kbp-CD der Seitenansicht (src/frontend/print/print-document.css): - gleiche Farben, Schrift, Titelblock und Fußzeile. Die Fußzeile mit - Seitenzahlen setzt EstimatePrintHeader als @page-Regel mit Projektdaten. */ -.estimate-print-titleblock { - display: none; -} - @page estimate-balance { size: A4 landscape; - margin: 10mm 10mm 13mm; + margin: 10mm; } @media print { body:has(.estimate-print-root) .sidebar, .estimate-print-root .estimate-no-print, - .estimate-print-root .estimate-page-header, - .estimate-print-root .estimate-header-fields, .estimate-print-root .alert, .estimate-print-root input, .estimate-print-root select, @@ -2018,157 +1980,37 @@ button.structure-entry-main:focus-visible { display: none !important; } - html:has(.estimate-print-root), - body:has(.estimate-print-root), - body:has(.estimate-print-root) .app-shell, - body:has(.estimate-print-root) .app-shell-content { + body:has(.estimate-print-root) { background: #fff !important; } - /* Der Dunkelmodus setzt color-scheme: dark; Chrome färbt damit auch den - Seitenrand dunkel. Im Druck gilt immer das helle Schema. */ - html:has(.estimate-print-root) { - color-scheme: light !important; - } - - .estimate-print-root .estimate-group-editor.is-empty { - display: none; - } - - /* Verbraucher bekommt den Platz; Anmerkungen bleiben schmal. */ - .estimate-sheet .estimate-col-name { - width: 52mm; - } - - .estimate-sheet .estimate-col-remark { - width: 24mm; - } - - .estimate-sheet .estimate-col-supply { - width: 13mm; - } - body:has(.estimate-print-root) .app-shell { display: block; } .estimate-print-root { - --ep-blue: #476088; - --ep-lilac: #9198c4; - --ep-line: #d6dae5; - --ep-tint: #f5f6fa; - --ep-text: #2b2b2b; - --ep-muted: #6b7280; - --ep-accent: #c9782a; page: estimate-balance; padding: 0 !important; max-width: none; - color: var(--ep-text); - font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; - color-scheme: light; - } - - .estimate-print-root * { - -webkit-print-color-adjust: exact; - print-color-adjust: exact; + color: #000; } .estimate-print-root .estimate-print-value { display: inline; } - /* Titelblock wie der Plankopf der Seitenansicht */ - .estimate-print-titleblock { - display: grid; - grid-template-columns: 30mm 1fr auto auto; + .estimate-print-root .page-header { margin-bottom: 4mm; - border: 1.5px solid var(--ep-blue); - background: #fff; - font-size: 8.5pt; - break-inside: avoid; } - .estimate-print-titleblock > * { - padding: 2.5mm 3.5mm; - border-left: 1px solid var(--ep-line); + .estimate-print-root .page-header h1 { + font-size: 14pt; + color: #000; } - .estimate-print-titleblock > :first-child { - border-left: none; - } - - .estimate-print-logo { - display: flex; - align-items: center; - justify-content: center; - min-height: 16mm; - color: var(--ep-blue); - font-weight: bold; - text-align: center; - } - - .estimate-print-logo img { - max-width: 100%; - max-height: 10mm; - } - - .estimate-print-kicker { - color: var(--ep-muted); - font-size: 7pt; - letter-spacing: 0.05em; - text-transform: uppercase; - } - - .estimate-print-heading { - display: flex; - align-items: baseline; - flex-wrap: wrap; - gap: 1mm 4mm; - margin-top: 1mm; - } - - .estimate-print-stand { - color: var(--ep-blue); - font-size: 16pt; - font-weight: bold; - } - - .estimate-print-project { - font-size: 12pt; - font-weight: bold; - } - - .estimate-print-meta { - display: grid; - grid-template-columns: auto auto; - gap: 0.8mm 4mm; - align-content: center; - margin: 0; - font-size: 7.5pt; - } - - .estimate-print-meta div { - display: contents; - } - - .estimate-print-meta dt { - color: var(--ep-muted); - font-weight: normal; - } - - .estimate-print-meta dd { - margin: 0; - font-weight: bold; - } - - .estimate-print-meta.is-tinted { - background: var(--ep-tint); - } - - /* Karten und Ränder der Bildschirmansicht entfallen */ .estimate-print-root .card { border: 0; - box-shadow: none !important; + box-shadow: none; background: transparent; } @@ -2184,117 +2026,54 @@ button.structure-entry-main:focus-visible { break-inside: avoid; } - .estimate-print-root .estimate-comparison-card .card-header { - margin-bottom: 2mm; - color: var(--ep-blue); - font-size: 10pt; - font-weight: bold; - } - .estimate-sheet-wrap { overflow: visible; } - /* Tabelle */ .estimate-sheet { min-width: 0; table-layout: auto; - font-size: 7.4pt; - line-height: 1.2; + font-size: 7.5pt; background: #fff; - color: var(--ep-text); + color: #000; } .estimate-sheet th, .estimate-sheet td { - border: 1px solid var(--ep-line) !important; - padding: 0.7mm 1.5mm; - background: #fff; - color: var(--ep-text); + border-color: #999; + padding: 1px 3px; } .estimate-sheet thead { display: table-header-group; } - .estimate-sheet thead th { - background: var(--ep-blue) !important; - border-color: var(--ep-blue) !important; - color: #fff !important; - font-weight: bold; - vertical-align: bottom; - } - - .estimate-sheet thead .estimate-th-hint { - color: #dfe3f0 !important; + .estimate-sheet thead th, + .estimate-total-row td { + background: #e8e8e8 !important; } .estimate-group-title-row td { - background: var(--ep-tint) !important; - border-top: 1.5px solid var(--ep-lilac) !important; - font-weight: bold; - } - - .estimate-group-title-row .estimate-cost-group-cell { - color: var(--ep-blue) !important; - } - - .estimate-group-title-row .estimate-group-editor { - color: var(--ep-muted); - font-weight: normal; + background: #d9d9d9 !important; } .estimate-subtotal-row td { - border-bottom: 1.5px solid var(--ep-line) !important; - font-weight: bold; - } - - .estimate-subtotal-row .estimate-total-label { - color: var(--ep-muted); - } - - .estimate-total-row td { - background: var(--ep-tint) !important; - font-weight: bold; - } - - .estimate-total-row.is-strong td { - background: var(--ep-blue) !important; - border-color: var(--ep-blue) !important; - color: #fff !important; - } - - .estimate-total-row .estimate-total-hint { - color: var(--ep-muted); - font-weight: normal; - } - - .estimate-total-row.is-strong .estimate-total-hint { - color: #dfe3f0; - } - - .estimate-comparison .is-over { - color: var(--ep-accent) !important; - font-weight: bold; - } - - .estimate-comparison .is-under { - color: var(--ep-blue) !important; - font-weight: bold; + background: #f2f2f2 !important; } .estimate-group-title-row, .estimate-subtotal-row, - .estimate-total-row, - .estimate-row { + .estimate-total-row { break-inside: avoid; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; } .estimate-group-title-row { break-after: avoid; } - /* Summen einmal am Ende, nicht auf jeder Seite */ + /* Totals print once at the end, not repeated on every page. */ .estimate-sheet tfoot { display: table-row-group; break-inside: avoid; @@ -2306,6 +2085,10 @@ button.structure-entry-main:focus-visible { .estimate-unit-input, .estimate-gzf-input { - justify-content: center; + justify-content: flex-end; + } + + .estimate-header-fields { + margin-bottom: 3mm; } } diff --git a/src/app/projects/[projectId]/grobbilanz/[balanceId]/page.tsx b/src/app/projects/[projectId]/grobbilanz/[balanceId]/page.tsx index 584f5cf..2aa7f5e 100644 --- a/src/app/projects/[projectId]/grobbilanz/[balanceId]/page.tsx +++ b/src/app/projects/[projectId]/grobbilanz/[balanceId]/page.tsx @@ -48,7 +48,6 @@ import { } from "../../../../../frontend/estimate-balance/estimate-balance-sheet"; import { EstimateComparisonTable } from "../../../../../frontend/estimate-balance/estimate-comparison-table"; import { DraftInput } from "../../../../../frontend/estimate-balance/estimate-draft-input"; -import { EstimatePrintHeader } from "../../../../../frontend/estimate-balance/estimate-print-header"; import { EstimateGroupModal, type EstimateGroupFormValues, @@ -462,7 +461,6 @@ export default function EstimateBalanceEditorPage() { return (
- {project && draft ? : null}
estimateGroups.id, { onDelete: "cascade" }), sortOrder: integer("sort_order").notNull().default(0), supply: text("supply"), + circuitRef: text("circuit_ref"), name: text("name").notNull().default(""), quantity: real("quantity").notNull().default(0), unit: text("unit", { enum: ["piece", "area"] }).notNull().default("piece"), diff --git a/src/estimate-balance/domain/estimate-balance-command.model.ts b/src/estimate-balance/domain/estimate-balance-command.model.ts index f1b0da8..44263e4 100644 --- a/src/estimate-balance/domain/estimate-balance-command.model.ts +++ b/src/estimate-balance/domain/estimate-balance-command.model.ts @@ -28,6 +28,7 @@ export const estimateRowSnapshotSchema = z id: idSchema, sortOrder: z.number().int().min(0), supply: nullableText(50), + circuitRef: nullableText(50), name: trimmed(200), quantity: finite.min(0), unit: z.enum(["piece", "area"]), @@ -157,6 +158,7 @@ export function canonicalEstimateBalance(balance: EstimateBalanceSnapshot): stri id: row.id, sortOrder: row.sortOrder, supply: row.supply, + circuitRef: row.circuitRef, name: row.name, quantity: row.quantity, unit: row.unit, diff --git a/src/frontend/estimate-balance/estimate-balance-document.ts b/src/frontend/estimate-balance/estimate-balance-document.ts index e2726d1..0049dc2 100644 --- a/src/frontend/estimate-balance/estimate-balance-document.ts +++ b/src/frontend/estimate-balance/estimate-balance-document.ts @@ -18,6 +18,7 @@ export const estimateTextLimits = { groupTitle: 200, groupEditor: 100, supply: 50, + circuitRef: 50, rowName: 200, remark: 500, } as const; @@ -33,6 +34,7 @@ export function createEstimateRow( id: createId(), sortOrder, supply: null, + circuitRef: null, name: "", quantity: 1, unit: "piece", @@ -255,6 +257,7 @@ export function toSaveableEstimateBalance( id: row.id, sortOrder: row.sortOrder, supply: optionalText(row.supply), + circuitRef: optionalText(row.circuitRef), name: requiredText(row.name), quantity: row.quantity, unit: row.unit, diff --git a/src/frontend/estimate-balance/estimate-balance-sheet.tsx b/src/frontend/estimate-balance/estimate-balance-sheet.tsx index 07dc19c..debba7f 100644 --- a/src/frontend/estimate-balance/estimate-balance-sheet.tsx +++ b/src/frontend/estimate-balance/estimate-balance-sheet.tsx @@ -31,7 +31,6 @@ import { formatKw, parseGermanDecimal, type DecimalRange, - formatGermanNumber, } from "./estimate-balance-format"; import { DraftInput } from "./estimate-draft-input"; @@ -51,12 +50,8 @@ interface EstimateBalanceSheetProps { } // 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"; +const LEADING_COLUMN_COUNT = 10; +const COLUMN_COUNT = 17; function NumberInput({ ariaLabel, @@ -82,7 +77,6 @@ function NumberInput({ 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)} @@ -91,17 +85,9 @@ function NumberInput({ ); } -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, @@ -109,7 +95,6 @@ function TextInput({ }: { ariaLabel: string; disabled: boolean; - list?: string; maxLength: number; onChange: (value: string) => void; placeholder?: string; @@ -121,7 +106,6 @@ function TextInput({ aria-label={ariaLabel} className="estimate-cell-input" disabled={disabled} - list={list} maxLength={maxLength} onChange={(event) => onChange(event.target.value)} placeholder={placeholder} @@ -205,14 +189,22 @@ function EstimateRowLine({ patch({ supply })} placeholder="ELT" value={row.supply} /> - + + patch({ circuitRef })} + value={row.circuitRef} + /> + + - - {supplyOptions(balance).map((supply) => ( - + @@ -421,8 +409,9 @@ export function EstimateBalanceSheet({ +
KG Versorgung - ELT/MSR … + ELT/ISP … Stromkreis-Nr. Verbraucher / Flächen Anzahl / Fläche @@ -463,13 +452,7 @@ export function EstimateBalanceSheet({
{group.title} -