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 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-02 15:09:53 +02:00
co-authored by Claude Opus 5
parent 7ce07daa97
commit b903de3841
2 changed files with 135 additions and 33 deletions
+115 -33
View File
@@ -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.
</p>
)}
{!loading && masse.map((m) => {
const w = wertFuer(m.id);
return (
<MassZeile key={m.id} mass={m} wert={w} eigen={istEigen(w)} isAdmin={isAdmin}
onSpeichern={(menge, basis) => 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 && (
<MassListe masse={masse} wertFuer={wertFuer} istEigen={istEigen} offen={offen}
isAdmin={isAdmin} onAufklappen={aufklappen} onZuklappen={zuklappen}
onSpeichern={speichern}
onEntfernen={async (m) => {
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);
}} />
)}
<p className="muted small">
„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 (
<MassZeile key={m.id} mass={m} wert={w} eigen={istEigen(w)} isAdmin={isAdmin}
onSpeichern={(menge, basis) => onSpeichern(m, menge, basis)}
onEntfernen={() => (istEigen(w) ? onEntfernen(m) : onZuklappen(m.id))} />
);
})}
{geerbte.map((m) => {
const w = wertFuer(m.id);
return (
<div className="mass-zeile geerbt" key={m.id}>
<span className="mass-name" title={m.name}>
{m.short ? `${m.name} (${m.short})` : m.name}
</span>
<span className="nowrap">{w.amount} {BASIS_KURZ[w.base] || w.base}</span>
<span className="badge nowrap" title={`Geerbt von ${w.herkunft}`}>geerbt</span>
{isAdmin ? (
<button type="button" className="btn sm ghost nowrap"
onClick={() => onAufklappen(m.id)}>
abweichen
</button>
) : <span />}
</div>
);
})}
{eigene.length === 0 && geerbte.length === 0 && (
<p className="muted small mt-0">
Für diese Kategorie ist noch kein Maß hinterlegt – „2 EL" lässt sich
hier deshalb nicht buchen.
</p>
)}
{isAdmin && uebrige.length > 0 && (
<label style={{ marginTop: "var(--sp-3)" }}>
Maß hinzufügen
<select value="" onChange={(e) => e.target.value && onAufklappen(e.target.value)}>
<option value="">– wählen –</option>
{uebrige.map((m) => (
<option key={m.id} value={m.id}>
{m.short ? `${m.name} (${m.short})` : m.name}
</option>
))}
</select>
</label>
)}
</>
);
}
/** 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 (
<div className="field-inline" style={{ flexWrap: "wrap", alignItems: "flex-end" }}>
<label style={{ margin: 0, minWidth: 110, flex: "0 0 auto" }}>
<span className="muted small">{mass.short ? `${mass.name} (${mass.short})` : mass.name}</span>
<input type="number" step="any" min="0" value={menge} disabled={!isAdmin}
placeholder={geerbt ? `${wert.amount} ${BASIS_KURZ[wert.base] || wert.base}` : "–"}
onChange={(e) => setMenge(e.target.value)}
onBlur={() => { if (menge !== "") onSpeichern(menge, basis); }} />
</label>
<select value={basis} disabled={!isAdmin} style={{ maxWidth: 130, marginTop: 0 }}
<div className="mass-zeile">
<span className="mass-name" title={mass.name}>
{mass.short ? `${mass.name} (${mass.short})` : mass.name}
</span>
<input type="number" step="any" min="0" value={menge} disabled={!isAdmin}
style={{ marginTop: 0 }} placeholder={geerbt ? String(wert.amount) : "Menge"}
onChange={(e) => setMenge(e.target.value)}
onBlur={() => { if (menge !== "") onSpeichern(menge, basis); }} />
<select value={basis} disabled={!isAdmin} style={{ marginTop: 0 }}
onChange={(e) => {
setBasis(e.target.value);
if (menge !== "") onSpeichern(menge, e.target.value);
}}>
{Object.entries(BASIS_LABEL).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
{Object.entries(BASIS_KURZ).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
</select>
{geerbt && (
<span className="badge nowrap" title={`Geerbt von ${wert.herkunft}`}>geerbt</span>
)}
{eigen && isAdmin && (
<button type="button" className="btn-icon danger" title="Hier entfernen"
{isAdmin && (
<button type="button" className="btn-icon danger"
title={eigen ? "Hier entfernen" : "Zeile wieder zuklappen"}
onClick={onEntfernen}>
<Icon name="trash" size={16} />
<Icon name={eigen ? "trash" : "close"} size={16} />
</button>
)}
</div>
+20
View File
@@ -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; }