Web-Artikelformular: Umschalter Lebensmittel/Gegenstand

Statt die Verwaltungsart nur implizit ueber die Kategorie zu waehlen (und dabei
Werkzeug & Co. zwischen 50 Lebensmittel-Kategorien zu suchen), gibt es jetzt oben
einen klaren Umschalter. Die Kategorieauswahl zeigt nur Kategorien des gewaehlten
Typs; neu angelegte Kategorien erben den Typ automatisch. Neuanlage startet als
Gegenstand, beim Bearbeiten kommt der Modus aus dem Artikel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-25 23:37:16 +02:00
parent 2a7f98d61c
commit 8d75ad63ce
2 changed files with 54 additions and 23 deletions

View File

@@ -79,8 +79,12 @@ export default function ProductForm() {
// Gegenstände: effektive (vererbte) Felder der Kategorie + deren Werte am Artikel. // Gegenstände: effektive (vererbte) Felder der Kategorie + deren Werte am Artikel.
const [effFields, setEffFields] = useState([]); const [effFields, setEffFields] = useState([]);
const [fieldValues, setFieldValues] = useState({}); const [fieldValues, setFieldValues] = useState({});
// Umschalter Lebensmittel/Gegenstand: filtert die Kategorieauswahl auf den Typ.
// Neuanlage startet als Gegenstand (der häufigere Non-Food-Fall); beim Bearbeiten
// wird er aus dem Artikel gesetzt.
const [modus, setModus] = useState("object");
// Direkt im Formular neue Kategorien/Felder anlegen. // Direkt im Formular neue Kategorien/Felder anlegen.
const [newCat, setNewCat] = useState(null); // { name, parent_id, tracking } | null const [newCat, setNewCat] = useState(null); // { name, parent_id } | null
const [newField, setNewField] = useState(null); // { label, field_type, unit, options, required } | null const [newField, setNewField] = useState(null); // { label, field_type, unit, options, required } | null
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
@@ -96,11 +100,9 @@ export default function ProductForm() {
const unitFactor = selectedUnit ? selectedUnit.factor : 1; const unitFactor = selectedUnit ? selectedUnit.factor : 1;
const unitName = selectedUnit ? selectedUnit.name : ""; const unitName = selectedUnit ? selectedUnit.name : "";
// Die gewählte Kategorie bestimmt die Verwaltungsart. Ohne Kategorie: Lebensmittel. // Der Umschalter bestimmt die Verwaltungsart; die Kategorieliste ist darauf
const currentCategory = // gefiltert, also passt die gewählte Kategorie immer zum Modus.
categories.find((c) => String(c.id) === String(form.category_id)) || null; const isObject = modus === "object";
const mode = currentCategory ? currentCategory.tracking : "food";
const isObject = mode === "object";
// Gegenstände gleicht das Backend gegen Open Products Facts ab, Lebensmittel // Gegenstände gleicht das Backend gegen Open Products Facts ab, Lebensmittel
// gegen Open Food Facts die Beschriftung folgt dem Modus. // gegen Open Food Facts die Beschriftung folgt dem Modus.
const datenquelle = isObject ? "Open Products Facts" : "Open Food Facts"; const datenquelle = isObject ? "Open Products Facts" : "Open Food Facts";
@@ -109,6 +111,16 @@ export default function ProductForm() {
setForm((f) => ({ ...f, [k]: v })); setForm((f) => ({ ...f, [k]: v }));
} }
// Typ umschalten: passt die gewählte Kategorie nicht mehr, wird sie geleert.
function changeModus(neu) {
setModus(neu);
const cat = categories.find((c) => String(c.id) === String(form.category_id));
if (cat && cat.tracking !== neu) {
set("category_id", "");
setEffFields([]);
}
}
function setField(fieldId, value) { function setField(fieldId, value) {
setFieldValues((v) => ({ ...v, [fieldId]: value })); setFieldValues((v) => ({ ...v, [fieldId]: value }));
} }
@@ -116,11 +128,8 @@ export default function ProductForm() {
// Öffnet den Inline-Dialog „Neue Kategorie" als Unterkategorie der gerade // Öffnet den Inline-Dialog „Neue Kategorie" als Unterkategorie der gerade
// gewählten (falls eine gewählt ist), im selben Modus. // gewählten (falls eine gewählt ist), im selben Modus.
function openNewCat() { function openNewCat() {
setNewCat({ // Neue Kategorie erbt den aktuell gewählten Typ (Umschalter).
name: "", setNewCat({ name: "", parent_id: form.category_id || "", tracking: modus });
parent_id: form.category_id || "",
tracking: currentCategory ? currentCategory.tracking : "object",
});
} }
async function createCategoryInline() { async function createCategoryInline() {
@@ -230,6 +239,10 @@ export default function ProductForm() {
group_id: f.group_id || (res.group_id ? String(res.group_id) : ""), group_id: f.group_id || (res.group_id ? String(res.group_id) : ""),
})); }));
if (s.package_size != null) setMinUnit("package"); if (s.package_size != null) setMinUnit("package");
if (res.category_id) {
const cat = categories.find((c) => String(c.id) === String(res.category_id));
if (cat) setModus(cat.tracking);
}
toast( toast(
`Daten von ${datenquelle} übernommen.` + `Daten von ${datenquelle} übernommen.` +
(s.package_size != null ? "" : " (Füllmenge nicht hinterlegt bitte Packungsgröße prüfen.)") + (s.package_size != null ? "" : " (Füllmenge nicht hinterlegt bitte Packungsgröße prüfen.)") +
@@ -373,6 +386,7 @@ export default function ProductForm() {
product_url: p.product_url || "", product_url: p.product_url || "",
}); });
setFieldValues(p.field_values || {}); setFieldValues(p.field_values || {});
setModus(p.tracking === "object" ? "object" : "food");
setLots(await api.listLots(id)); setLots(await api.listLots(id));
} else { } else {
const bc = searchParams.get("barcode"); const bc = searchParams.get("barcode");
@@ -656,15 +670,31 @@ export default function ProductForm() {
</>)} </>)}
<div className="row"> <div className="row">
<label className="grow"> <label className="grow">
<span className="tip" title="Bestimmt zugleich die Verwaltungsart: Gegenstands-Kategorien blenden MHD/Chargen aus und zeigen Menge je Lagerort."> Typ
<div className="segmented">
<button type="button" className={modus === "food" ? "active" : ""}
onClick={() => changeModus("food")} disabled={readOnly}>
Lebensmittel
</button>
<button type="button" className={modus === "object" ? "active" : ""}
onClick={() => changeModus("object")} disabled={readOnly}>
Gegenstand
</button>
</div>
</label>
<div className="grow" />
</div>
<div className="row">
<label className="grow">
<span className="tip" title="Nur Kategorien des gewählten Typs. Gegenstands-Kategorien zeigen Menge je Lagerort, Lebensmittel MHD/Chargen.">
Kategorie Kategorie
</span> </span>
<select value={form.category_id} onChange={(e) => set("category_id", e.target.value)} <select value={form.category_id} onChange={(e) => set("category_id", e.target.value)}
disabled={readOnly}> disabled={readOnly}>
<option value=""> keine </option> <option value=""> keine </option>
{asTree(categories).map((c) => ( {asTree(categories).filter((c) => c.tracking === modus).map((c) => (
<option key={c.id} value={c.id}> <option key={c.id} value={c.id}>
{"— ".repeat(c.depth)}{c.name}{c.tracking === "object" ? " · Gegenstand" : ""} {"— ".repeat(c.depth)}{c.name}
</option> </option>
))} ))}
</select> </select>
@@ -681,7 +711,8 @@ export default function ProductForm() {
{isAdmin && newCat && ( {isAdmin && newCat && (
<div className="card-sub" style={{ marginBottom: "var(--sp-3)" }}> <div className="card-sub" style={{ marginBottom: "var(--sp-3)" }}>
<div className="card-head"><Icon name="tag" /><h3>Neue Kategorie / Unterkategorie</h3> <div className="card-head"><Icon name="tag" />
<h3>Neue {modus === "object" ? "Gegenstands" : "Lebensmittel"}-Kategorie</h3>
<button type="button" className="btn-icon" style={{ marginLeft: "auto" }} <button type="button" className="btn-icon" style={{ marginLeft: "auto" }}
onClick={() => setNewCat(null)}><Icon name="close" size={16} /></button> onClick={() => setNewCat(null)}><Icon name="close" size={16} /></button>
</div> </div>
@@ -694,18 +725,11 @@ export default function ProductForm() {
<select value={newCat.parent_id} <select value={newCat.parent_id}
onChange={(e) => setNewCat({ ...newCat, parent_id: e.target.value })}> onChange={(e) => setNewCat({ ...newCat, parent_id: e.target.value })}>
<option value=""> oberste Ebene </option> <option value=""> oberste Ebene </option>
{asTree(categories).map((c) => ( {asTree(categories).filter((c) => c.tracking === modus).map((c) => (
<option key={c.id} value={c.id}>{"— ".repeat(c.depth)}{c.name}</option> <option key={c.id} value={c.id}>{"— ".repeat(c.depth)}{c.name}</option>
))} ))}
</select> </select>
</label> </label>
<label style={{ width: 150 }}>Art
<select value={newCat.tracking}
onChange={(e) => setNewCat({ ...newCat, tracking: e.target.value })}>
<option value="object">Gegenstand</option>
<option value="food">Lebensmittel</option>
</select>
</label>
</div> </div>
<button type="button" className="btn primary" onClick={createCategoryInline}> <button type="button" className="btn primary" onClick={createCategoryInline}>
Anlegen &amp; auswählen Anlegen &amp; auswählen

View File

@@ -859,6 +859,13 @@ tr.row-active { background: var(--surface-2); }
.card-sub .card-head { margin-top: 0; } .card-sub .card-head { margin-top: 0; }
.card-sub h3 { margin: 0; font-size: 15px; } .card-sub h3 { margin: 0; font-size: 15px; }
/* Umschalter Lebensmittel/Gegenstand */
.segmented { display: inline-flex; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; margin-top: 4px; }
.segmented button { border: 0; background: var(--surface); color: var(--text); padding: 8px 16px; cursor: pointer; font: inherit; }
.segmented button + button { border-left: 1px solid var(--border); }
.segmented button.active { background: var(--accent); color: #fff; }
.segmented button:disabled { cursor: default; opacity: 0.6; }
/* Artikelfoto mit Aufnahme-/Galerie-Knöpfen */ /* Artikelfoto mit Aufnahme-/Galerie-Knöpfen */
.produkt-foto { display: flex; flex-direction: column; gap: var(--sp-2); align-items: center; } .produkt-foto { display: flex; flex-direction: column; gap: var(--sp-2); align-items: center; }
.foto-knoepfe { display: flex; flex-wrap: wrap; gap: var(--sp-1); justify-content: center; } .foto-knoepfe { display: flex; flex-wrap: wrap; gap: var(--sp-1); justify-content: center; }