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:
@@ -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 & auswählen
|
Anlegen & auswählen
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user