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:
co-authored by
Claude Opus 5
parent
7ce07daa97
commit
b903de3841
+115
-33
@@ -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" };
|
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 [frist, setFrist] = useState(category.open_days ?? "");
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
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() {
|
async function load() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -475,10 +478,19 @@ function CategoryMasse({ category, isAdmin, onClose, onFrist }) {
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
await api.deleteCategoryMeasure(category.id, mass.id);
|
await api.deleteCategoryMeasure(category.id, mass.id);
|
||||||
|
setOffen((alt) => { const neu = new Set(alt); neu.delete(mass.id); return neu; });
|
||||||
load();
|
load();
|
||||||
} catch (err) { setError(err.message); }
|
} 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() {
|
async function fristSpeichern() {
|
||||||
const roh = String(frist).trim();
|
const roh = String(frist).trim();
|
||||||
const zahl = roh === "" ? null : Number(roh);
|
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.
|
Es gibt noch keine Maße. Angelegt werden sie unter Stammdaten ▸ Maße.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{!loading && masse.map((m) => {
|
{!loading && masse.length > 0 && (
|
||||||
const w = wertFuer(m.id);
|
<MassListe masse={masse} wertFuer={wertFuer} istEigen={istEigen} offen={offen}
|
||||||
return (
|
isAdmin={isAdmin} onAufklappen={aufklappen} onZuklappen={zuklappen}
|
||||||
<MassZeile key={m.id} mass={m} wert={w} eigen={istEigen(w)} isAdmin={isAdmin}
|
onSpeichern={speichern}
|
||||||
onSpeichern={(menge, basis) => speichern(m, menge, basis)}
|
onEntfernen={async (m) => {
|
||||||
onEntfernen={async () => {
|
const ok = await confirm({
|
||||||
const ok = await confirm({
|
title: `„${m.name}" hier entfernen?`,
|
||||||
title: `„${m.name}" hier entfernen?`,
|
message: "Danach erbt die Kategorie den Wert von oben – oder das Maß "
|
||||||
message: "Danach erbt die Kategorie den Wert von oben – oder das Maß "
|
+ "gilt hier gar nicht mehr.",
|
||||||
+ "gilt hier gar nicht mehr.",
|
confirmLabel: "Entfernen",
|
||||||
confirmLabel: "Entfernen",
|
});
|
||||||
});
|
if (ok) entfernen(m);
|
||||||
if (ok) entfernen(m);
|
}} />
|
||||||
}} />
|
)}
|
||||||
);
|
|
||||||
})}
|
|
||||||
<p className="muted small">
|
<p className="muted small">
|
||||||
„1 EL Mehl ≈ 10 g", „1 Tasse Milch ≈ 200 ml". Damit lässt sich beim
|
„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,
|
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. */
|
/** Eine Maß-Zeile: eigene Menge eintragen, geerbte als Platzhalter zeigen. */
|
||||||
function MassZeile({ mass, wert, eigen, isAdmin, onSpeichern, onEntfernen }) {
|
function MassZeile({ mass, wert, eigen, isAdmin, onSpeichern, onEntfernen }) {
|
||||||
const [menge, setMenge] = useState(eigen && wert ? String(wert.amount) : "");
|
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;
|
const geerbt = wert && !eigen;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="field-inline" style={{ flexWrap: "wrap", alignItems: "flex-end" }}>
|
<div className="mass-zeile">
|
||||||
<label style={{ margin: 0, minWidth: 110, flex: "0 0 auto" }}>
|
<span className="mass-name" title={mass.name}>
|
||||||
<span className="muted small">{mass.short ? `${mass.name} (${mass.short})` : mass.name}</span>
|
{mass.short ? `${mass.name} (${mass.short})` : mass.name}
|
||||||
<input type="number" step="any" min="0" value={menge} disabled={!isAdmin}
|
</span>
|
||||||
placeholder={geerbt ? `${wert.amount} ${BASIS_KURZ[wert.base] || wert.base}` : "–"}
|
<input type="number" step="any" min="0" value={menge} disabled={!isAdmin}
|
||||||
onChange={(e) => setMenge(e.target.value)}
|
style={{ marginTop: 0 }} placeholder={geerbt ? String(wert.amount) : "Menge"}
|
||||||
onBlur={() => { if (menge !== "") onSpeichern(menge, basis); }} />
|
onChange={(e) => setMenge(e.target.value)}
|
||||||
</label>
|
onBlur={() => { if (menge !== "") onSpeichern(menge, basis); }} />
|
||||||
<select value={basis} disabled={!isAdmin} style={{ maxWidth: 130, marginTop: 0 }}
|
<select value={basis} disabled={!isAdmin} style={{ marginTop: 0 }}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setBasis(e.target.value);
|
setBasis(e.target.value);
|
||||||
if (menge !== "") onSpeichern(menge, 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>
|
</select>
|
||||||
{geerbt && (
|
{isAdmin && (
|
||||||
<span className="badge nowrap" title={`Geerbt von ${wert.herkunft}`}>geerbt</span>
|
<button type="button" className="btn-icon danger"
|
||||||
)}
|
title={eigen ? "Hier entfernen" : "Zeile wieder zuklappen"}
|
||||||
{eigen && isAdmin && (
|
|
||||||
<button type="button" className="btn-icon danger" title="Hier entfernen"
|
|
||||||
onClick={onEntfernen}>
|
onClick={onEntfernen}>
|
||||||
<Icon name="trash" size={16} />
|
<Icon name={eigen ? "trash" : "close"} size={16} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1038,6 +1038,26 @@ select.zeitraum:hover { color: var(--text); }
|
|||||||
Den fehlenden Abstand setzt deshalb die Geschwisterregel. */
|
Den fehlenden Abstand setzt deshalb die Geschwisterregel. */
|
||||||
.kv + .field-inline { margin-top: var(--sp-4); }
|
.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 ---------- */
|
/* ---------- Utilities ---------- */
|
||||||
.muted { color: var(--muted); }
|
.muted { color: var(--muted); }
|
||||||
.small { font-size: 0.8rem; }
|
.small { font-size: 0.8rem; }
|
||||||
|
|||||||
Reference in New Issue
Block a user