Compare commits
No commits in common. "7b2419b347470807a6a5a9a6bc395fe9ba51009b" and "37a70b6b363959d3018c48d5c40b94e1d25a3163" have entirely different histories.
7b2419b347
...
37a70b6b36
15 changed files with 69 additions and 394 deletions
|
|
@ -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_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_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,
|
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,
|
434 Kälte, 440 Starkstrom, 450 Schwachstrom, 460 Förder, 470 Nutzungsspezifisch,
|
||||||
|
|
|
||||||
|
|
@ -1731,6 +1731,7 @@ button.structure-entry-main:focus-visible {
|
||||||
|
|
||||||
.estimate-col-kg { width: 3rem; }
|
.estimate-col-kg { width: 3rem; }
|
||||||
.estimate-col-supply { width: 4rem; }
|
.estimate-col-supply { width: 4rem; }
|
||||||
|
.estimate-col-circuit { width: 4.6rem; }
|
||||||
.estimate-col-name { width: auto; }
|
.estimate-col-name { width: auto; }
|
||||||
.estimate-col-quantity { width: 4.8rem; }
|
.estimate-col-quantity { width: 4.8rem; }
|
||||||
.estimate-col-unit { width: 3.8rem; }
|
.estimate-col-unit { width: 3.8rem; }
|
||||||
|
|
@ -1964,53 +1965,14 @@ button.structure-entry-main:focus-visible {
|
||||||
display: none;
|
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 {
|
@page estimate-balance {
|
||||||
size: A4 landscape;
|
size: A4 landscape;
|
||||||
margin: 10mm 10mm 13mm;
|
margin: 10mm;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
body:has(.estimate-print-root) .sidebar,
|
body:has(.estimate-print-root) .sidebar,
|
||||||
.estimate-print-root .estimate-no-print,
|
.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 .alert,
|
||||||
.estimate-print-root input,
|
.estimate-print-root input,
|
||||||
.estimate-print-root select,
|
.estimate-print-root select,
|
||||||
|
|
@ -2018,157 +1980,37 @@ button.structure-entry-main:focus-visible {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
html:has(.estimate-print-root),
|
body: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;
|
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 {
|
body:has(.estimate-print-root) .app-shell {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.estimate-print-root {
|
.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;
|
page: estimate-balance;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
color: var(--ep-text);
|
color: #000;
|
||||||
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 {
|
.estimate-print-root .estimate-print-value {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Titelblock wie der Plankopf der Seitenansicht */
|
.estimate-print-root .page-header {
|
||||||
.estimate-print-titleblock {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 30mm 1fr auto auto;
|
|
||||||
margin-bottom: 4mm;
|
margin-bottom: 4mm;
|
||||||
border: 1.5px solid var(--ep-blue);
|
|
||||||
background: #fff;
|
|
||||||
font-size: 8.5pt;
|
|
||||||
break-inside: avoid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.estimate-print-titleblock > * {
|
.estimate-print-root .page-header h1 {
|
||||||
padding: 2.5mm 3.5mm;
|
font-size: 14pt;
|
||||||
border-left: 1px solid var(--ep-line);
|
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 {
|
.estimate-print-root .card {
|
||||||
border: 0;
|
border: 0;
|
||||||
box-shadow: none !important;
|
box-shadow: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2184,117 +2026,54 @@ button.structure-entry-main:focus-visible {
|
||||||
break-inside: avoid;
|
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 {
|
.estimate-sheet-wrap {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tabelle */
|
|
||||||
.estimate-sheet {
|
.estimate-sheet {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
table-layout: auto;
|
table-layout: auto;
|
||||||
font-size: 7.4pt;
|
font-size: 7.5pt;
|
||||||
line-height: 1.2;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: var(--ep-text);
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.estimate-sheet th,
|
.estimate-sheet th,
|
||||||
.estimate-sheet td {
|
.estimate-sheet td {
|
||||||
border: 1px solid var(--ep-line) !important;
|
border-color: #999;
|
||||||
padding: 0.7mm 1.5mm;
|
padding: 1px 3px;
|
||||||
background: #fff;
|
|
||||||
color: var(--ep-text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.estimate-sheet thead {
|
.estimate-sheet thead {
|
||||||
display: table-header-group;
|
display: table-header-group;
|
||||||
}
|
}
|
||||||
|
|
||||||
.estimate-sheet thead th {
|
.estimate-sheet thead th,
|
||||||
background: var(--ep-blue) !important;
|
.estimate-total-row td {
|
||||||
border-color: var(--ep-blue) !important;
|
background: #e8e8e8 !important;
|
||||||
color: #fff !important;
|
|
||||||
font-weight: bold;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
.estimate-sheet thead .estimate-th-hint {
|
|
||||||
color: #dfe3f0 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.estimate-group-title-row td {
|
.estimate-group-title-row td {
|
||||||
background: var(--ep-tint) !important;
|
background: #d9d9d9 !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 {
|
.estimate-subtotal-row td {
|
||||||
border-bottom: 1.5px solid var(--ep-line) !important;
|
background: #f2f2f2 !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-group-title-row,
|
||||||
.estimate-subtotal-row,
|
.estimate-subtotal-row,
|
||||||
.estimate-total-row,
|
.estimate-total-row {
|
||||||
.estimate-row {
|
|
||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
}
|
}
|
||||||
|
|
||||||
.estimate-group-title-row {
|
.estimate-group-title-row {
|
||||||
break-after: avoid;
|
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 {
|
.estimate-sheet tfoot {
|
||||||
display: table-row-group;
|
display: table-row-group;
|
||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
|
|
@ -2306,6 +2085,10 @@ button.structure-entry-main:focus-visible {
|
||||||
|
|
||||||
.estimate-unit-input,
|
.estimate-unit-input,
|
||||||
.estimate-gzf-input {
|
.estimate-gzf-input {
|
||||||
justify-content: center;
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.estimate-header-fields {
|
||||||
|
margin-bottom: 3mm;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,6 @@ import {
|
||||||
} from "../../../../../frontend/estimate-balance/estimate-balance-sheet";
|
} from "../../../../../frontend/estimate-balance/estimate-balance-sheet";
|
||||||
import { EstimateComparisonTable } from "../../../../../frontend/estimate-balance/estimate-comparison-table";
|
import { EstimateComparisonTable } from "../../../../../frontend/estimate-balance/estimate-comparison-table";
|
||||||
import { DraftInput } from "../../../../../frontend/estimate-balance/estimate-draft-input";
|
import { DraftInput } from "../../../../../frontend/estimate-balance/estimate-draft-input";
|
||||||
import { EstimatePrintHeader } from "../../../../../frontend/estimate-balance/estimate-print-header";
|
|
||||||
import {
|
import {
|
||||||
EstimateGroupModal,
|
EstimateGroupModal,
|
||||||
type EstimateGroupFormValues,
|
type EstimateGroupFormValues,
|
||||||
|
|
@ -462,7 +461,6 @@ export default function EstimateBalanceEditorPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="container-fluid py-4 estimate-print-root">
|
<main className="container-fluid py-4 estimate-print-root">
|
||||||
{project && draft ? <EstimatePrintHeader balance={draft} project={project} /> : null}
|
|
||||||
<div className="page-header estimate-page-header">
|
<div className="page-header estimate-page-header">
|
||||||
<div className="d-flex align-items-start gap-3">
|
<div className="d-flex align-items-start gap-3">
|
||||||
<Link
|
<Link
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ CREATE TABLE `estimate_rows` (
|
||||||
`group_id` text NOT NULL,
|
`group_id` text NOT NULL,
|
||||||
`sort_order` integer DEFAULT 0 NOT NULL,
|
`sort_order` integer DEFAULT 0 NOT NULL,
|
||||||
`supply` text,
|
`supply` text,
|
||||||
|
`circuit_ref` text,
|
||||||
`name` text DEFAULT '' NOT NULL,
|
`name` text DEFAULT '' NOT NULL,
|
||||||
`quantity` real DEFAULT 0 NOT NULL,
|
`quantity` real DEFAULT 0 NOT NULL,
|
||||||
`unit` text DEFAULT 'piece' NOT NULL,
|
`unit` text DEFAULT 'piece' NOT NULL,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"id": "5376ae9f-67c7-4603-b3d2-8527136436f9",
|
"id": "6df688bd-a78a-46a1-ba7a-4068119bf8bd",
|
||||||
"prevId": "2cd9adef-2f08-4635-b3d9-ecf71f624c7f",
|
"prevId": "2cd9adef-2f08-4635-b3d9-ecf71f624c7f",
|
||||||
"tables": {
|
"tables": {
|
||||||
"buildings": {
|
"buildings": {
|
||||||
|
|
@ -1322,6 +1322,13 @@
|
||||||
"notNull": false,
|
"notNull": false,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
|
"circuit_ref": {
|
||||||
|
"name": "circuit_ref",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"name": "name",
|
"name": "name",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@
|
||||||
{
|
{
|
||||||
"idx": 10,
|
"idx": 10,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1790438092269,
|
"when": 1790425784626,
|
||||||
"tag": "0010_estimate_balances",
|
"tag": "0010_estimate_balances",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ export const estimateRows = sqliteTable(
|
||||||
.references(() => estimateGroups.id, { onDelete: "cascade" }),
|
.references(() => estimateGroups.id, { onDelete: "cascade" }),
|
||||||
sortOrder: integer("sort_order").notNull().default(0),
|
sortOrder: integer("sort_order").notNull().default(0),
|
||||||
supply: text("supply"),
|
supply: text("supply"),
|
||||||
|
circuitRef: text("circuit_ref"),
|
||||||
name: text("name").notNull().default(""),
|
name: text("name").notNull().default(""),
|
||||||
quantity: real("quantity").notNull().default(0),
|
quantity: real("quantity").notNull().default(0),
|
||||||
unit: text("unit", { enum: ["piece", "area"] }).notNull().default("piece"),
|
unit: text("unit", { enum: ["piece", "area"] }).notNull().default("piece"),
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ export const estimateRowSnapshotSchema = z
|
||||||
id: idSchema,
|
id: idSchema,
|
||||||
sortOrder: z.number().int().min(0),
|
sortOrder: z.number().int().min(0),
|
||||||
supply: nullableText(50),
|
supply: nullableText(50),
|
||||||
|
circuitRef: nullableText(50),
|
||||||
name: trimmed(200),
|
name: trimmed(200),
|
||||||
quantity: finite.min(0),
|
quantity: finite.min(0),
|
||||||
unit: z.enum(["piece", "area"]),
|
unit: z.enum(["piece", "area"]),
|
||||||
|
|
@ -157,6 +158,7 @@ export function canonicalEstimateBalance(balance: EstimateBalanceSnapshot): stri
|
||||||
id: row.id,
|
id: row.id,
|
||||||
sortOrder: row.sortOrder,
|
sortOrder: row.sortOrder,
|
||||||
supply: row.supply,
|
supply: row.supply,
|
||||||
|
circuitRef: row.circuitRef,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
quantity: row.quantity,
|
quantity: row.quantity,
|
||||||
unit: row.unit,
|
unit: row.unit,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export const estimateTextLimits = {
|
||||||
groupTitle: 200,
|
groupTitle: 200,
|
||||||
groupEditor: 100,
|
groupEditor: 100,
|
||||||
supply: 50,
|
supply: 50,
|
||||||
|
circuitRef: 50,
|
||||||
rowName: 200,
|
rowName: 200,
|
||||||
remark: 500,
|
remark: 500,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
@ -33,6 +34,7 @@ export function createEstimateRow(
|
||||||
id: createId(),
|
id: createId(),
|
||||||
sortOrder,
|
sortOrder,
|
||||||
supply: null,
|
supply: null,
|
||||||
|
circuitRef: null,
|
||||||
name: "",
|
name: "",
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
unit: "piece",
|
unit: "piece",
|
||||||
|
|
@ -255,6 +257,7 @@ export function toSaveableEstimateBalance(
|
||||||
id: row.id,
|
id: row.id,
|
||||||
sortOrder: row.sortOrder,
|
sortOrder: row.sortOrder,
|
||||||
supply: optionalText(row.supply),
|
supply: optionalText(row.supply),
|
||||||
|
circuitRef: optionalText(row.circuitRef),
|
||||||
name: requiredText(row.name),
|
name: requiredText(row.name),
|
||||||
quantity: row.quantity,
|
quantity: row.quantity,
|
||||||
unit: row.unit,
|
unit: row.unit,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ import {
|
||||||
formatKw,
|
formatKw,
|
||||||
parseGermanDecimal,
|
parseGermanDecimal,
|
||||||
type DecimalRange,
|
type DecimalRange,
|
||||||
formatGermanNumber,
|
|
||||||
} from "./estimate-balance-format";
|
} from "./estimate-balance-format";
|
||||||
import { DraftInput } from "./estimate-draft-input";
|
import { DraftInput } from "./estimate-draft-input";
|
||||||
|
|
||||||
|
|
@ -51,12 +50,8 @@ interface EstimateBalanceSheetProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leading columns up to cos φ; the power columns follow.
|
// Leading columns up to cos φ; the power columns follow.
|
||||||
const LEADING_COLUMN_COUNT = 9;
|
const LEADING_COLUMN_COUNT = 10;
|
||||||
const COLUMN_COUNT = 16;
|
const COLUMN_COUNT = 17;
|
||||||
|
|
||||||
/** Suggested supply values; free text stays possible. */
|
|
||||||
const SUPPLY_SUGGESTIONS = ["ELT", "MSR"];
|
|
||||||
const SUPPLY_LIST_ID = "estimate-supply-options";
|
|
||||||
|
|
||||||
function NumberInput({
|
function NumberInput({
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
|
|
@ -82,7 +77,6 @@ function NumberInput({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
format={(current: number) => formatDecimalInput(current)}
|
format={(current: number) => formatDecimalInput(current)}
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
printFormat={(current: number) => formatGermanNumber(current, 0, 3)}
|
|
||||||
onCommit={onCommit}
|
onCommit={onCommit}
|
||||||
onValidityChange={(isValid) => onValidityChange(fieldKey, isValid)}
|
onValidityChange={(isValid) => onValidityChange(fieldKey, isValid)}
|
||||||
parse={(text) => parseGermanDecimal(text, range)}
|
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({
|
function TextInput({
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
disabled,
|
disabled,
|
||||||
list,
|
|
||||||
maxLength,
|
maxLength,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
|
@ -109,7 +95,6 @@ function TextInput({
|
||||||
}: {
|
}: {
|
||||||
ariaLabel: string;
|
ariaLabel: string;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
list?: string;
|
|
||||||
maxLength: number;
|
maxLength: number;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
|
@ -121,7 +106,6 @@ function TextInput({
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
className="estimate-cell-input"
|
className="estimate-cell-input"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
list={list}
|
|
||||||
maxLength={maxLength}
|
maxLength={maxLength}
|
||||||
onChange={(event) => onChange(event.target.value)}
|
onChange={(event) => onChange(event.target.value)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|
@ -205,14 +189,22 @@ function EstimateRowLine({
|
||||||
<TextInput
|
<TextInput
|
||||||
ariaLabel={`Versorgung (${rowLabel})`}
|
ariaLabel={`Versorgung (${rowLabel})`}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
list={SUPPLY_LIST_ID}
|
|
||||||
maxLength={estimateTextLimits.supply}
|
maxLength={estimateTextLimits.supply}
|
||||||
onChange={(supply) => patch({ supply })}
|
onChange={(supply) => patch({ supply })}
|
||||||
placeholder="ELT"
|
placeholder="ELT"
|
||||||
value={row.supply}
|
value={row.supply}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="estimate-name-cell">
|
<td>
|
||||||
|
<TextInput
|
||||||
|
ariaLabel={`Stromkreis-Nr. (${rowLabel})`}
|
||||||
|
disabled={disabled}
|
||||||
|
maxLength={estimateTextLimits.circuitRef}
|
||||||
|
onChange={(circuitRef) => patch({ circuitRef })}
|
||||||
|
value={row.circuitRef}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
<TextInput
|
<TextInput
|
||||||
ariaLabel={`Verbraucher / Fläche (${rowLabel})`}
|
ariaLabel={`Verbraucher / Fläche (${rowLabel})`}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
@ -392,15 +384,11 @@ export function EstimateBalanceSheet({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="estimate-sheet-wrap">
|
<div className="estimate-sheet-wrap">
|
||||||
<datalist id={SUPPLY_LIST_ID}>
|
|
||||||
{supplyOptions(balance).map((supply) => (
|
|
||||||
<option key={supply} value={supply} />
|
|
||||||
))}
|
|
||||||
</datalist>
|
|
||||||
<table className="estimate-sheet">
|
<table className="estimate-sheet">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col className="estimate-col-kg" />
|
<col className="estimate-col-kg" />
|
||||||
<col className="estimate-col-supply" />
|
<col className="estimate-col-supply" />
|
||||||
|
<col className="estimate-col-circuit" />
|
||||||
<col className="estimate-col-name" />
|
<col className="estimate-col-name" />
|
||||||
<col className="estimate-col-quantity" />
|
<col className="estimate-col-quantity" />
|
||||||
<col className="estimate-col-unit" />
|
<col className="estimate-col-unit" />
|
||||||
|
|
@ -421,8 +409,9 @@ export function EstimateBalanceSheet({
|
||||||
<th rowSpan={2}>KG</th>
|
<th rowSpan={2}>KG</th>
|
||||||
<th rowSpan={2}>
|
<th rowSpan={2}>
|
||||||
Versorgung
|
Versorgung
|
||||||
<span className="estimate-th-hint">ELT/MSR …</span>
|
<span className="estimate-th-hint">ELT/ISP …</span>
|
||||||
</th>
|
</th>
|
||||||
|
<th rowSpan={2}>Stromkreis-Nr.</th>
|
||||||
<th rowSpan={2}>Verbraucher / Flächen</th>
|
<th rowSpan={2}>Verbraucher / Flächen</th>
|
||||||
<th colSpan={2}>
|
<th colSpan={2}>
|
||||||
Anzahl / Fläche
|
Anzahl / Fläche
|
||||||
|
|
@ -463,13 +452,7 @@ export function EstimateBalanceSheet({
|
||||||
<td colSpan={COLUMN_COUNT - 2}>
|
<td colSpan={COLUMN_COUNT - 2}>
|
||||||
<div className="estimate-group-title">
|
<div className="estimate-group-title">
|
||||||
<span className="estimate-group-name">{group.title}</span>
|
<span className="estimate-group-name">{group.title}</span>
|
||||||
<label
|
<label className="estimate-group-editor">
|
||||||
className={
|
|
||||||
group.editor?.trim()
|
|
||||||
? "estimate-group-editor"
|
|
||||||
: "estimate-group-editor is-empty"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span>bearbeitet von</span>
|
<span>bearbeitet von</span>
|
||||||
<input
|
<input
|
||||||
aria-label={`Bearbeitet von (${label})`}
|
aria-label={`Bearbeitet von (${label})`}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ interface DraftInputProps<T> {
|
||||||
className?: string;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
format: (value: T) => string;
|
format: (value: T) => string;
|
||||||
/** Printed text; defaults to the input text. */
|
|
||||||
printFormat?: (value: T) => string;
|
|
||||||
inputMode?: "decimal" | "text";
|
inputMode?: "decimal" | "text";
|
||||||
maxLength?: number;
|
maxLength?: number;
|
||||||
onCommit: (value: T) => void;
|
onCommit: (value: T) => void;
|
||||||
|
|
@ -32,7 +30,6 @@ export function DraftInput<T>({
|
||||||
className = "estimate-cell-input",
|
className = "estimate-cell-input",
|
||||||
disabled,
|
disabled,
|
||||||
format,
|
format,
|
||||||
printFormat,
|
|
||||||
inputMode = "text",
|
inputMode = "text",
|
||||||
maxLength,
|
maxLength,
|
||||||
onCommit,
|
onCommit,
|
||||||
|
|
@ -104,9 +101,7 @@ export function DraftInput<T>({
|
||||||
type="text"
|
type="text"
|
||||||
value={text}
|
value={text}
|
||||||
/>
|
/>
|
||||||
<span className="estimate-print-value">
|
<span className="estimate-print-value">{error ? format(value) : text}</span>
|
||||||
{printFormat ? printFormat(value) : error ? format(value) : text}
|
|
||||||
</span>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { documentLogoUrl, getDocumentBranding } from "../utils/api";
|
|
||||||
import type { EstimateBalanceDto, OfficeProfileDto, ProjectDto } from "../types";
|
|
||||||
import { formatIsoDateGerman } from "./estimate-balance-format";
|
|
||||||
|
|
||||||
interface EstimatePrintHeaderProps {
|
|
||||||
project: ProjectDto;
|
|
||||||
balance: EstimateBalanceDto;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cssString(value: string) {
|
|
||||||
return JSON.stringify(value.replace(/[<>\r\n]/g, " "));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Print-only title block and page footer of the rough power balance in the
|
|
||||||
* kbp corporate design of the print view (Seitenansicht): logo, blue title,
|
|
||||||
* meta data, "Seite x von y". Hidden on screen.
|
|
||||||
*/
|
|
||||||
export function EstimatePrintHeader({ project, balance }: EstimatePrintHeaderProps) {
|
|
||||||
const [office, setOffice] = useState<OfficeProfileDto | null>(null);
|
|
||||||
const [hasLogo, setHasLogo] = useState(false);
|
|
||||||
const [logoVersion, setLogoVersion] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getDocumentBranding()
|
|
||||||
.then((branding) => {
|
|
||||||
setOffice(branding.office);
|
|
||||||
setHasLogo(Boolean(branding.logo));
|
|
||||||
setLogoVersion(branding.logo ? branding.logo.updatedAtIso : "");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setOffice(null);
|
|
||||||
setHasLogo(false);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const stand = [balance.name, balance.workStage].filter(Boolean).join(" · ");
|
|
||||||
const date = balance.dateIso ? formatIsoDateGerman(balance.dateIso) : null;
|
|
||||||
const footer = `
|
|
||||||
@page estimate-balance {
|
|
||||||
size: A4 landscape;
|
|
||||||
margin: 10mm 10mm 13mm;
|
|
||||||
@bottom-left { content: ${cssString(`${project.name} · Grobe Leistungsbilanz`)}; font: 7.5pt Arial, sans-serif; color: #6b7280; }
|
|
||||||
@bottom-center { content: ${cssString(`Stand ${stand}${date ? ` · ${date}` : ""}`)}; font: 7.5pt Arial, sans-serif; color: #6b7280; }
|
|
||||||
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 7.5pt Arial, sans-serif; color: #6b7280; }
|
|
||||||
}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<style>{footer}</style>
|
|
||||||
<header className="estimate-print-titleblock" aria-hidden="true">
|
|
||||||
<div className="estimate-print-logo">
|
|
||||||
{hasLogo ? (
|
|
||||||
<img alt="" src={`${documentLogoUrl}?v=${encodeURIComponent(logoVersion)}`} />
|
|
||||||
) : (
|
|
||||||
<span>{office?.officeName ?? ""}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="estimate-print-title">
|
|
||||||
<div className="estimate-print-kicker">Grobe Leistungsbilanz · KG 400</div>
|
|
||||||
<div className="estimate-print-heading">
|
|
||||||
<span className="estimate-print-stand">{balance.name}</span>
|
|
||||||
<span className="estimate-print-project">{project.name}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<dl className="estimate-print-meta">
|
|
||||||
<div>
|
|
||||||
<dt>Projekt-Nr.</dt>
|
|
||||||
<dd>{project.internalProjectNumber ?? "–"}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Bauherr</dt>
|
|
||||||
<dd>{project.buildingOwner ?? "–"}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Anschrift</dt>
|
|
||||||
<dd>{project.siteAddress?.split("\n").join(", ") ?? "–"}</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
<dl className="estimate-print-meta is-tinted">
|
|
||||||
<div>
|
|
||||||
<dt>Leistungsphase</dt>
|
|
||||||
<dd>{balance.workStage ?? "–"}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Datum</dt>
|
|
||||||
<dd>{date ?? "–"}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Bearbeitung</dt>
|
|
||||||
<dd>{project.projectEditor ?? "–"}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Büro</dt>
|
|
||||||
<dd>{office?.officeName ?? "–"}</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</header>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -12,8 +12,10 @@ export type EstimateRowConnection = "fixed" | "socket";
|
||||||
export interface EstimateRowSnapshot {
|
export interface EstimateRowSnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
/** Versorgung, e.g. "ELT" or "MSR" (free text). */
|
/** Versorgung, e.g. "ELT" or "ISP". */
|
||||||
supply: string | null;
|
supply: string | null;
|
||||||
|
/** Optional circuit reference, free text. */
|
||||||
|
circuitRef: string | null;
|
||||||
/** Verbraucher / Fläche. May be empty while a row is being typed. */
|
/** Verbraucher / Fläche. May be empty while a row is being typed. */
|
||||||
name: string;
|
name: string;
|
||||||
/** Anzahl (unit "piece") or area in m² (unit "area"). */
|
/** Anzahl (unit "piece") or area in m² (unit "area"). */
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,7 @@ describe("estimate balance document editing", () => {
|
||||||
id: "row-x",
|
id: "row-x",
|
||||||
sortOrder: 3,
|
sortOrder: 3,
|
||||||
supply: null,
|
supply: null,
|
||||||
|
circuitRef: null,
|
||||||
name: "",
|
name: "",
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
unit: "piece",
|
unit: "piece",
|
||||||
|
|
@ -272,6 +273,7 @@ describe("estimate balance saving", () => {
|
||||||
balance = updateEstimateRow(balance, "group-410", "row-a", {
|
balance = updateEstimateRow(balance, "group-410", "row-a", {
|
||||||
name: " Hebeanlage ",
|
name: " Hebeanlage ",
|
||||||
supply: " ELT ",
|
supply: " ELT ",
|
||||||
|
circuitRef: "",
|
||||||
remark: " ",
|
remark: " ",
|
||||||
});
|
});
|
||||||
const saveable = toSaveableEstimateBalance(balance);
|
const saveable = toSaveableEstimateBalance(balance);
|
||||||
|
|
@ -284,9 +286,10 @@ describe("estimate balance saving", () => {
|
||||||
{
|
{
|
||||||
name: saveable.groups[0]!.rows[0]!.name,
|
name: saveable.groups[0]!.rows[0]!.name,
|
||||||
supply: saveable.groups[0]!.rows[0]!.supply,
|
supply: saveable.groups[0]!.rows[0]!.supply,
|
||||||
|
circuitRef: saveable.groups[0]!.rows[0]!.circuitRef,
|
||||||
remark: saveable.groups[0]!.rows[0]!.remark,
|
remark: saveable.groups[0]!.rows[0]!.remark,
|
||||||
},
|
},
|
||||||
{ name: "Hebeanlage", supply: "ELT", remark: null }
|
{ name: "Hebeanlage", supply: "ELT", circuitRef: null, remark: null }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ function row(
|
||||||
id: id(),
|
id: id(),
|
||||||
sortOrder: 0,
|
sortOrder: 0,
|
||||||
supply: "ELT",
|
supply: "ELT",
|
||||||
|
circuitRef: null,
|
||||||
name,
|
name,
|
||||||
quantity,
|
quantity,
|
||||||
unit: "piece",
|
unit: "piece",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue