Offer supply suggestions, drop the circuit column, center cells

The Versorgung field suggests ELT and MSR (plus values already used in
the balance) through a datalist and still accepts free text. The circuit
number column is not needed in early design phases; it is removed from
the data model, the regenerated migration 0010, the editor and the
print. All cells are centered except "Verbraucher / Flächen", group
titles and total labels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Grovy311 2026-09-26 17:56:53 +02:00
parent e06c7d91e2
commit 72189d23a5
12 changed files with 60 additions and 42 deletions

View file

@ -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, 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, 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,

View file

@ -1731,7 +1731,6 @@ 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; }
@ -1965,6 +1964,36 @@ 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): /* Druck im kbp-CD der Seitenansicht (src/frontend/print/print-document.css):
gleiche Farben, Schrift, Titelblock und Fußzeile. Die Fußzeile mit gleiche Farben, Schrift, Titelblock und Fußzeile. Die Fußzeile mit
Seitenzahlen setzt EstimatePrintHeader als @page-Regel mit Projektdaten. */ Seitenzahlen setzt EstimatePrintHeader als @page-Regel mit Projektdaten. */
@ -2251,6 +2280,6 @@ button.structure-entry-main:focus-visible {
.estimate-unit-input, .estimate-unit-input,
.estimate-gzf-input { .estimate-gzf-input {
justify-content: flex-end; justify-content: center;
} }
} }

View file

@ -27,7 +27,6 @@ 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,

View file

@ -1,7 +1,7 @@
{ {
"version": "6", "version": "6",
"dialect": "sqlite", "dialect": "sqlite",
"id": "6df688bd-a78a-46a1-ba7a-4068119bf8bd", "id": "5376ae9f-67c7-4603-b3d2-8527136436f9",
"prevId": "2cd9adef-2f08-4635-b3d9-ecf71f624c7f", "prevId": "2cd9adef-2f08-4635-b3d9-ecf71f624c7f",
"tables": { "tables": {
"buildings": { "buildings": {
@ -1322,13 +1322,6 @@
"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",

View file

@ -75,7 +75,7 @@
{ {
"idx": 10, "idx": 10,
"version": "6", "version": "6",
"when": 1790425784626, "when": 1790438092269,
"tag": "0010_estimate_balances", "tag": "0010_estimate_balances",
"breakpoints": true "breakpoints": true
} }

View file

@ -44,7 +44,6 @@ 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"),

View file

@ -28,7 +28,6 @@ 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"]),
@ -158,7 +157,6 @@ 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,

View file

@ -18,7 +18,6 @@ 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;
@ -34,7 +33,6 @@ 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",
@ -257,7 +255,6 @@ 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,

View file

@ -50,8 +50,12 @@ interface EstimateBalanceSheetProps {
} }
// Leading columns up to cos φ; the power columns follow. // Leading columns up to cos φ; the power columns follow.
const LEADING_COLUMN_COUNT = 10; const LEADING_COLUMN_COUNT = 9;
const COLUMN_COUNT = 17; 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({ function NumberInput({
ariaLabel, ariaLabel,
@ -85,9 +89,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({ function TextInput({
ariaLabel, ariaLabel,
disabled, disabled,
list,
maxLength, maxLength,
onChange, onChange,
placeholder, placeholder,
@ -95,6 +107,7 @@ 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;
@ -106,6 +119,7 @@ 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}
@ -189,22 +203,14 @@ 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> <td className="estimate-name-cell">
<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}
@ -384,11 +390,15 @@ 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" />
@ -409,9 +419,8 @@ 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/ISP …</span> <span className="estimate-th-hint">ELT/MSR …</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

View file

@ -12,10 +12,8 @@ export type EstimateRowConnection = "fixed" | "socket";
export interface EstimateRowSnapshot { export interface EstimateRowSnapshot {
id: string; id: string;
sortOrder: number; sortOrder: number;
/** Versorgung, e.g. "ELT" or "ISP". */ /** Versorgung, e.g. "ELT" or "MSR" (free text). */
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"). */

View file

@ -165,7 +165,6 @@ 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",
@ -273,7 +272,6 @@ 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);
@ -286,10 +284,9 @@ 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", circuitRef: null, remark: null } { name: "Hebeanlage", supply: "ELT", remark: null }
); );
}); });

View file

@ -52,7 +52,6 @@ function row(
id: id(), id: id(),
sortOrder: 0, sortOrder: 0,
supply: "ELT", supply: "ELT",
circuitRef: null,
name, name,
quantity, quantity,
unit: "piece", unit: "piece",