diff --git a/docs/estimate-balance.md b/docs/estimate-balance.md index 4e43cec..afe4780 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, 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 | +| `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 | 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 b3f526a..dbf49fb 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1731,7 +1731,6 @@ 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; } @@ -1965,14 +1964,53 @@ 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; + margin: 10mm 10mm 13mm; } @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, @@ -1980,37 +2018,157 @@ button.structure-entry-main:focus-visible { display: none !important; } - body:has(.estimate-print-root) { + 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 { 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: #000; + 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; } .estimate-print-root .estimate-print-value { display: inline; } - .estimate-print-root .page-header { + /* Titelblock wie der Plankopf der Seitenansicht */ + .estimate-print-titleblock { + display: grid; + grid-template-columns: 30mm 1fr auto auto; margin-bottom: 4mm; + border: 1.5px solid var(--ep-blue); + background: #fff; + font-size: 8.5pt; + break-inside: avoid; } - .estimate-print-root .page-header h1 { - font-size: 14pt; - color: #000; + .estimate-print-titleblock > * { + padding: 2.5mm 3.5mm; + border-left: 1px solid var(--ep-line); } + .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; + box-shadow: none !important; background: transparent; } @@ -2026,54 +2184,117 @@ 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.5pt; + font-size: 7.4pt; + line-height: 1.2; background: #fff; - color: #000; + color: var(--ep-text); } .estimate-sheet th, .estimate-sheet td { - border-color: #999; - padding: 1px 3px; + border: 1px solid var(--ep-line) !important; + padding: 0.7mm 1.5mm; + background: #fff; + color: var(--ep-text); } .estimate-sheet thead { display: table-header-group; } - .estimate-sheet thead th, - .estimate-total-row td { - background: #e8e8e8 !important; + .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-group-title-row td { - background: #d9d9d9 !important; + 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; } .estimate-subtotal-row td { - background: #f2f2f2 !important; + 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; } .estimate-group-title-row, .estimate-subtotal-row, - .estimate-total-row { + .estimate-total-row, + .estimate-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. */ + /* Summen einmal am Ende, nicht auf jeder Seite */ .estimate-sheet tfoot { display: table-row-group; break-inside: avoid; @@ -2085,10 +2306,6 @@ button.structure-entry-main:focus-visible { .estimate-unit-input, .estimate-gzf-input { - justify-content: flex-end; - } - - .estimate-header-fields { - margin-bottom: 3mm; + justify-content: center; } } diff --git a/src/app/projects/[projectId]/grobbilanz/[balanceId]/page.tsx b/src/app/projects/[projectId]/grobbilanz/[balanceId]/page.tsx index 2aa7f5e..584f5cf 100644 --- a/src/app/projects/[projectId]/grobbilanz/[balanceId]/page.tsx +++ b/src/app/projects/[projectId]/grobbilanz/[balanceId]/page.tsx @@ -48,6 +48,7 @@ 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, @@ -461,6 +462,7 @@ 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 44263e4..f1b0da8 100644 --- a/src/estimate-balance/domain/estimate-balance-command.model.ts +++ b/src/estimate-balance/domain/estimate-balance-command.model.ts @@ -28,7 +28,6 @@ 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"]), @@ -158,7 +157,6 @@ 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 0049dc2..e2726d1 100644 --- a/src/frontend/estimate-balance/estimate-balance-document.ts +++ b/src/frontend/estimate-balance/estimate-balance-document.ts @@ -18,7 +18,6 @@ export const estimateTextLimits = { groupTitle: 200, groupEditor: 100, supply: 50, - circuitRef: 50, rowName: 200, remark: 500, } as const; @@ -34,7 +33,6 @@ export function createEstimateRow( id: createId(), sortOrder, supply: null, - circuitRef: null, name: "", quantity: 1, unit: "piece", @@ -257,7 +255,6 @@ 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 debba7f..07dc19c 100644 --- a/src/frontend/estimate-balance/estimate-balance-sheet.tsx +++ b/src/frontend/estimate-balance/estimate-balance-sheet.tsx @@ -31,6 +31,7 @@ import { formatKw, parseGermanDecimal, type DecimalRange, + formatGermanNumber, } from "./estimate-balance-format"; import { DraftInput } from "./estimate-draft-input"; @@ -50,8 +51,12 @@ interface EstimateBalanceSheetProps { } // Leading columns up to cos φ; the power columns follow. -const LEADING_COLUMN_COUNT = 10; -const COLUMN_COUNT = 17; +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, @@ -77,6 +82,7 @@ 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)} @@ -85,9 +91,17 @@ 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, @@ -95,6 +109,7 @@ function TextInput({ }: { ariaLabel: string; disabled: boolean; + list?: string; maxLength: number; onChange: (value: string) => void; placeholder?: string; @@ -106,6 +121,7 @@ function TextInput({ aria-label={ariaLabel} className="estimate-cell-input" disabled={disabled} + list={list} maxLength={maxLength} onChange={(event) => onChange(event.target.value)} placeholder={placeholder} @@ -189,22 +205,14 @@ function EstimateRowLine({ patch({ supply })} placeholder="ELT" value={row.supply} /> - - patch({ circuitRef })} - value={row.circuitRef} - /> - - + + + {supplyOptions(balance).map((supply) => ( + - @@ -409,9 +421,8 @@ export function EstimateBalanceSheet({ -
KG Versorgung - ELT/ISP … + ELT/MSR … Stromkreis-Nr. Verbraucher / Flächen Anzahl / Fläche @@ -452,7 +463,13 @@ export function EstimateBalanceSheet({
{group.title} -