From b903de384185fd77c133702e625d7197b0d1c76a Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Fri, 2 Oct 2026 15:09:53 +0200 Subject: [PATCH] Masse einer Kategorie: nur zeigen, was etwas bedeutet Jedes angelegte Mass bekam eine Zeile, und jede Zeile drei gestapelte Reihen (Name, Menge, Art) plus Muelleimer. Bei sechs Massen sah das aus wie ein Formular voller Pflichtangaben - und keine davon ist eine. Jetzt drei Sorten: - Hier eingetragen: bearbeitbare Zeile mit Muelleimer. - Von oben geerbt: eine Zeile zum Lesen ("Essloeffel (EL) 10 g geerbt") mit Knopf "abweichen", der sie bei Bedarf zur Eingabezeile macht. - Alles andere: steckt in der Auswahl "Mass hinzufuegen" und stoert nicht. Hinzufuegen klappt eine leere Zeile auf; das Kreuz daneben klappt sie wieder zu, ohne etwas zu speichern. Dazu das Zeilenlayout auf EINE Zeile gebracht: Name, Menge, Art, Aktion nebeneinander, Spalten duerfen schrumpfen, Einheiten als Kurzform (g/ml/Stk) - in der schmalen Seitenkarte ist das lesbarer, der Name steht ja daneben. Von Hand geprueft: Eine Kategorie mit zwei eingetragenen Massen zeigt zwei Zeilen statt sechs, die Unterkategorie drei geerbte Lesezeilen und kein einziges Eingabefeld. "Tasse" ueber die Auswahl hinzugefuegt, 200 ml eingetragen, gespeichert. Zeilenhoehe 29 bzw. 40 Pixel, kein Umbruch bei 270 Pixel Kartenbreite. Co-Authored-By: Claude Opus 5 --- web/src/pages/Categories.jsx | 148 +++++++++++++++++++++++++++-------- web/src/styles.css | 20 +++++ 2 files changed, 135 insertions(+), 33 deletions(-) diff --git a/web/src/pages/Categories.jsx b/web/src/pages/Categories.jsx index cb2a80b..e971b50 100644 --- a/web/src/pages/Categories.jsx +++ b/web/src/pages/Categories.jsx @@ -421,7 +421,6 @@ function CategoryFields({ category, isAdmin, onClose }) { ); } -const BASIS_LABEL = { gram: "Gramm", milliliter: "Milliliter", piece: "Stück" }; const BASIS_KURZ = { gram: "g", milliliter: "ml", piece: "Stk" }; /** @@ -443,6 +442,10 @@ function CategoryMasse({ category, isAdmin, onClose, onFrist }) { const [frist, setFrist] = useState(category.open_days ?? ""); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); + // Masse, die als leere Eingabezeile offen stehen, weil jemand sie gerade + // hinzugefuegt hat. Alles andere ohne Wert bleibt unsichtbar: Ein Dutzend + // leere Felder sehen aus wie Pflichtangaben, und keines davon ist eine. + const [offen, setOffen] = useState(() => new Set()); async function load() { setLoading(true); @@ -475,10 +478,19 @@ function CategoryMasse({ category, isAdmin, onClose, onFrist }) { setError(null); try { await api.deleteCategoryMeasure(category.id, mass.id); + setOffen((alt) => { const neu = new Set(alt); neu.delete(mass.id); return neu; }); load(); } catch (err) { setError(err.message); } } + function aufklappen(id) { + setOffen((alt) => new Set(alt).add(Number(id))); + } + + function zuklappen(id) { + setOffen((alt) => { const neu = new Set(alt); neu.delete(id); return neu; }); + } + async function fristSpeichern() { const roh = String(frist).trim(); const zahl = roh === "" ? null : Number(roh); @@ -525,22 +537,20 @@ function CategoryMasse({ category, isAdmin, onClose, onFrist }) { Es gibt noch keine Maße. Angelegt werden sie unter Stammdaten ▸ Maße.

)} - {!loading && masse.map((m) => { - const w = wertFuer(m.id); - return ( - speichern(m, menge, basis)} - onEntfernen={async () => { - const ok = await confirm({ - title: `„${m.name}" hier entfernen?`, - message: "Danach erbt die Kategorie den Wert von oben – oder das Maß " - + "gilt hier gar nicht mehr.", - confirmLabel: "Entfernen", - }); - if (ok) entfernen(m); - }} /> - ); - })} + {!loading && masse.length > 0 && ( + { + const ok = await confirm({ + title: `„${m.name}" hier entfernen?`, + message: "Danach erbt die Kategorie den Wert von oben – oder das Maß " + + "gilt hier gar nicht mehr.", + confirmLabel: "Entfernen", + }); + if (ok) entfernen(m); + }} /> + )}

„1 EL Mehl ≈ 10 g", „1 Tasse Milch ≈ 200 ml". Damit lässt sich beim Verbrauchen „2 EL" buchen, statt Gramm zu schätzen. Die Art (Gramm, @@ -551,6 +561,80 @@ function CategoryMasse({ category, isAdmin, onClose, onFrist }) { ); } +/** + * Die Maße einer Kategorie – aber nur die, die etwas bedeuten. + * + * Es gibt beliebig viele Maß-Namen; für eine einzelne Kategorie sind fast immer + * nur zwei oder drei davon sinnvoll. Jedes als leeres Feld hinzustellen sah aus + * wie ein Formular voller Pflichtangaben, und keine davon ist eine. Deshalb drei + * Sorten: + * + * - **hier eingetragen** – bearbeitbare Zeile mit Mülleimer, + * - **von oben geerbt** – eine Lesezeile mit Herkunft und „abweichen", + * - **alles andere** – steckt in der Auswahl „Maß hinzufügen" und stört nicht. + */ +function MassListe({ + masse, wertFuer, istEigen, offen, isAdmin, + onAufklappen, onZuklappen, onSpeichern, onEntfernen, +}) { + const eigene = masse.filter((m) => istEigen(wertFuer(m.id)) || offen.has(m.id)); + const geerbte = masse.filter((m) => !eigene.includes(m) && wertFuer(m.id) !== null); + const uebrige = masse.filter((m) => !eigene.includes(m) && !geerbte.includes(m)); + + return ( + <> + {eigene.map((m) => { + const w = wertFuer(m.id); + return ( + onSpeichern(m, menge, basis)} + onEntfernen={() => (istEigen(w) ? onEntfernen(m) : onZuklappen(m.id))} /> + ); + })} + + {geerbte.map((m) => { + const w = wertFuer(m.id); + return ( +

+ + {m.short ? `${m.name} (${m.short})` : m.name} + + {w.amount} {BASIS_KURZ[w.base] || w.base} + geerbt + {isAdmin ? ( + + ) : } +
+ ); + })} + + {eigene.length === 0 && geerbte.length === 0 && ( +

+ Für diese Kategorie ist noch kein Maß hinterlegt – „2 EL" lässt sich + hier deshalb nicht buchen. +

+ )} + + {isAdmin && uebrige.length > 0 && ( + + )} + + ); +} + /** Eine Maß-Zeile: eigene Menge eintragen, geerbte als Platzhalter zeigen. */ function MassZeile({ mass, wert, eigen, isAdmin, onSpeichern, onEntfernen }) { const [menge, setMenge] = useState(eigen && wert ? String(wert.amount) : ""); @@ -565,28 +649,26 @@ function MassZeile({ mass, wert, eigen, isAdmin, onSpeichern, onEntfernen }) { const geerbt = wert && !eigen; return ( -
- - setMenge(e.target.value)} + onBlur={() => { if (menge !== "") onSpeichern(menge, basis); }} /> + - {geerbt && ( - geerbt - )} - {eigen && isAdmin && ( - )}
diff --git a/web/src/styles.css b/web/src/styles.css index a2eb6f7..5338c92 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -1038,6 +1038,26 @@ select.zeitraum:hover { color: var(--text); } Den fehlenden Abstand setzt deshalb die Geschwisterregel. */ .kv + .field-inline { margin-top: var(--sp-4); } +/* Hausmass-Zeile: Name, Menge, Art, Aktion - in EINER Zeile. Vorher trug jedes + Mass drei gestapelte Reihen, und ein Dutzend davon sah aus wie ein Formular + voller Pflichtfelder. Die Spalten duerfen schrumpfen (minmax mit 0), damit + die Zeile auch in der schmalen Seitenkarte nicht umbricht. */ +.mass-zeile { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.9fr) minmax(0, 0.9fr) auto; + gap: var(--sp-2); + align-items: center; + margin-bottom: var(--sp-2); +} +.mass-name { + font-size: 0.85rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +/* Geerbte Zeilen sind zum Lesen da, nicht zum Tippen. */ +.mass-zeile.geerbt { color: var(--muted); } + /* ---------- Utilities ---------- */ .muted { color: var(--muted); } .small { font-size: 0.8rem; }