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:
Scarriffle
2026-08-16 09:59:32 +02:00
parent 302e7625e5
commit e00eef275c
9 changed files with 184 additions and 36 deletions

View File

@@ -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,