Gruppen hierarchisch: in allen Auswahlfeldern und in der Artikelliste
Die Gruppen-Dropdowns waren flache, alphabetische Listen - man sah nicht, dass "Grillwurst" unter "Wurst" haengt. Ein gemeinsamer Helfer (gruppenOptionen) rollt den Graphen zum Baum aus und rueckt ein; eine Gruppe mit mehreren Obergruppen erscheint unter jeder, denn genau das ist ihr Sinn, und ausgewaehlt wird ohnehin dieselbe ID. Eingerueckt wird mit GESCHUETZTEN Leerzeichen - normale fasst der Browser in einem <option> zusammen, die Einrueckung waere sonst wirkungslos. Betroffen: Gruppen-Auswahl im Artikelformular (beide Zweige), beim Einlagern, die Ziel-Auswahl der Mindestbestaende und die Obergruppen-Mehrfachauswahl (die beim Suchen bewusst abflacht, weil eine Einrueckung ohne die Eltern in die Irre fuehrt). In der App dasselbe in beiden Gruppen-Pickern. Die Gruppen-Spalte der Artikelliste zeigt jetzt den Weg von oben statt eines Etiketts - "Wurst → Grillwurst", mit gedaempften Vorfahren wie bei der Kategorie. Gefiltert wird ueber ALLE Wege: wer auf "Wurst" filtert, sieht auch die Artikel aus den Untergruppen. Dafuer gibt es gruppenInfoMap als Gegenstueck zu categoryInfoMap, das mit mehreren Wegen je Gruppe umgehen kann. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
302e7625e5
commit
e00eef275c
@@ -10,6 +10,7 @@ import { asTree } from "../categoryTree";
|
||||
import CategorySelect from "../components/CategorySelect";
|
||||
import { locationOptions } from "../locationPath";
|
||||
import { buildUnitOptions, fmt, fromMonthInput, isExpired, monthInputToLastDay } from "../units";
|
||||
import { gruppenOptionen } from "../groupGraph";
|
||||
|
||||
const emptyLine = () => ({ quantity: "", best_before: "" });
|
||||
|
||||
@@ -159,7 +160,9 @@ export default function CheckIn() {
|
||||
Gruppe
|
||||
<select value={newGroupId} onChange={(e) => setNewGroupId(e.target.value)}>
|
||||
<option value="">– keine –</option>
|
||||
{groups.map((g) => <option key={g.id} value={g.id}>{g.name}</option>)}
|
||||
{gruppenOptionen(groups).map((o) => (
|
||||
<option key={o.key} value={o.id}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="muted small">
|
||||
Zählt Bestände mehrerer Marken zusammen. Der EAN-Code landet dann auch dort.
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAuth } from "../auth";
|
||||
import { useConfirm } from "../confirm";
|
||||
import Icon from "../components/Icon";
|
||||
import { locationOptions, locationPathById } from "../locationPath";
|
||||
import { gruppenOptionen } from "../groupGraph";
|
||||
import { anzahlWort, fmt, gebinde, grpFactor, grpPkgMode, grpUnit, kindShort } from "../units";
|
||||
|
||||
// Anzeigeeinheit eines Produkts: die im Formular gewählte Einheit (Gramm,
|
||||
@@ -366,14 +367,20 @@ export default function MinStock() {
|
||||
{draft.kind === "product" ? "Lebensmittel / Verbrauchsgegenstand" : "Gruppe"}
|
||||
<select value={draft.targetId} onChange={(e) => setDraft({ ...draft, targetId: e.target.value })}>
|
||||
<option value="">– wählen –</option>
|
||||
{zielListe.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
{t.name}
|
||||
{draft.kind === "product" && t.brand ? ` · ${t.brand}` : ""}
|
||||
{draft.kind === "group" && t.child_ids?.length
|
||||
? ` (inkl. ${anzahlWort(t.child_ids.length, "Untergruppe", "Untergruppen")})` : ""}
|
||||
</option>
|
||||
))}
|
||||
{/* Gruppen als Baum, damit sichtbar ist, was unter was hängt. */}
|
||||
{draft.kind === "group"
|
||||
? gruppenOptionen(groups).map((o) => (
|
||||
<option key={o.key} value={o.id}>
|
||||
{o.label}
|
||||
{o.gruppe.child_ids?.length
|
||||
? ` (inkl. ${anzahlWort(o.gruppe.child_ids.length, "Untergruppe", "Untergruppen")})` : ""}
|
||||
</option>
|
||||
))
|
||||
: zielListe.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
{t.name}{t.brand ? ` · ${t.brand}` : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ import ObjektBestand from "../components/ObjektBestand";
|
||||
import Einzelstuecke from "../components/Einzelstuecke";
|
||||
import SplitLotDialog from "../components/SplitLotDialog";
|
||||
import { locationOptions, locationPathById } from "../locationPath";
|
||||
import { gruppenOptionen } from "../groupGraph";
|
||||
import {
|
||||
BASE_UNITS, daysUntil, expiryRowClass, fmt, fromMonthInput, gebinde, isExpired,
|
||||
relativeExpiry, toMonthInput, unitShort, zweitFaktor,
|
||||
@@ -968,7 +969,9 @@ export default function ProductForm() {
|
||||
</span>
|
||||
<select value={form.group_id} onChange={(e) => set("group_id", e.target.value)} disabled={readOnly}>
|
||||
<option value="">– keine –</option>
|
||||
{groups.map((g) => <option key={g.id} value={g.id}>{g.name}</option>)}
|
||||
{gruppenOptionen(groups).map((o) => (
|
||||
<option key={o.key} value={o.id}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
@@ -1043,7 +1046,9 @@ export default function ProductForm() {
|
||||
<select value={form.group_id} onChange={(e) => set("group_id", e.target.value)}
|
||||
disabled={readOnly}>
|
||||
<option value="">– keine –</option>
|
||||
{groups.map((g) => <option key={g.id} value={g.id}>{g.name}</option>)}
|
||||
{gruppenOptionen(groups).map((o) => (
|
||||
<option key={o.key} value={o.id}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import DataTable from "../components/DataTable";
|
||||
import { ProduktThumb } from "../components/ProduktBild";
|
||||
import CategoryPathLabel, { pathParts } from "../components/CategoryPathLabel";
|
||||
import { categoryInfoMap } from "../categoryPath";
|
||||
import { gruppenInfoMap } from "../groupGraph";
|
||||
import { fmt, gebinde, unitShort } from "../units";
|
||||
|
||||
// Wie viele Basiseinheiten eine "Artikeleinheit" umfasst (Gebinde oder Produkteinheit).
|
||||
@@ -43,6 +44,8 @@ export default function Products({ fixedType = null }) {
|
||||
const { isAdmin } = useAuth();
|
||||
const [products, setProducts] = useState([]);
|
||||
const [categories, setCategories] = useState([]);
|
||||
// Gruppen nur fuer die Pfad-Anzeige der Gruppen-Spalte.
|
||||
const [groups, setGroups] = useState([]);
|
||||
// "" = alle, sonst "food"/"object": trennt Lebensmittel und Gegenstände.
|
||||
const [typ, setTyp] = useState("");
|
||||
const [error, setError] = useState(null);
|
||||
@@ -56,11 +59,13 @@ export default function Products({ fixedType = null }) {
|
||||
.catch((err) => setError(err.message))
|
||||
.finally(() => setLoading(false));
|
||||
api.listCategories().then(setCategories).catch(() => {});
|
||||
api.listGroups().then(setGroups).catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Kategorie als Pfad + Tokens (jede Ebene), damit "Klamotten" auch die
|
||||
// Unterkategorien findet und die Oberkategorie im Filter wählbar ist.
|
||||
const catInfo = useMemo(() => categoryInfoMap(categories), [categories]);
|
||||
const grpInfo = useMemo(() => gruppenInfoMap(groups), [groups]);
|
||||
// Zwei getrennte Seiten (Lebensmittel/Gegenstände) fixieren den Typ; sonst der
|
||||
// Umschalter oben.
|
||||
const activeTyp = fixedType || typ;
|
||||
@@ -91,15 +96,20 @@ export default function Products({ fixedType = null }) {
|
||||
filterOptionLabel: (v) => (v === "" ? "(Leere)" : <CategoryPathLabel parts={pathParts(v)} />),
|
||||
sortValue: (p) => catInfo.get(p.category_id)?.path || p.category_name || "",
|
||||
render: (p) => <CategoryPathLabel parts={catInfo.get(p.category_id)?.parts} fallback={p.category_name || "–"} /> },
|
||||
// Gruppe: zählt Bestände mehrerer Marken zusammen. Als eigene Spalte, damit
|
||||
// sich die Liste danach filtern lässt („zeig mir alles aus Pesto Rosso").
|
||||
{ key: "gruppe", header: "Gruppe", width: 170,
|
||||
filterText: (p) => p.group_name || "",
|
||||
filterValues: (p) => [p.group_name || ""],
|
||||
filterOptionLabel: (v) => (v === "" ? "(Ohne Gruppe)" : v),
|
||||
sortValue: (p) => p.group_name || "",
|
||||
render: (p) => (p.group_name
|
||||
? <span className="badge accent">{p.group_name}</span>
|
||||
// Gruppe: zählt Bestände mehrerer Marken zusammen. Mit ihrem Weg von oben
|
||||
// dargestellt, wie die Kategorie – ein Etikett verschwiege, dass „Grillwurst"
|
||||
// unter „Wurst" hängt. Gefiltert wird über ALLE Wege, damit „Wurst" auch die
|
||||
// Artikel aus den Untergruppen zeigt.
|
||||
{ key: "gruppe", header: "Gruppe", width: 200,
|
||||
filterText: (p) => grpInfo.get(p.group_id)?.path || p.group_name || "",
|
||||
filterValues: (p) => grpInfo.get(p.group_id)?.tokens
|
||||
|| (p.group_name ? [p.group_name] : [""]),
|
||||
filterOptionLabel: (v) => (v === "" ? "(Ohne Gruppe)" : <CategoryPathLabel parts={pathParts(v)} />),
|
||||
sortValue: (p) => grpInfo.get(p.group_id)?.path || p.group_name || "",
|
||||
render: (p) => (p.group_id != null
|
||||
? <span title={(grpInfo.get(p.group_id)?.alle || []).join(" · ")}>
|
||||
<CategoryPathLabel parts={grpInfo.get(p.group_id)?.parts} fallback={p.group_name || "–"} />
|
||||
</span>
|
||||
: <span className="muted">–</span>) },
|
||||
// Nur in der Gegenstände-Liste: Menge je Lagerort / Einzelstücke / Verbrauchsgegenstand.
|
||||
activeTyp === "object" && { key: "verwaltung", header: "Verwaltung", label: "Verwaltung", width: 190,
|
||||
|
||||
Reference in New Issue
Block a user