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; }