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

@@ -75,3 +75,74 @@ export function elternNamen(groups, group) {
const map = byId(groups);
return (group?.parent_ids || []).map((id) => map.get(id)?.name).filter(Boolean);
}
/**
* Gruppen für ein Auswahlfeld: als Baum ausgerollt und eingerückt.
*
* Eine Gruppe mit mehreren Obergruppen erscheint unter JEDER genau das ist
* der Sinn mehrerer Obergruppen. Der Wert ist ohnehin dieselbe ID, egal welche
* Zeile man trifft.
*
* Eingerückt wird mit GESCHÜTZTEN Leerzeichen: normale fasst der Browser in
* einem <option> zusammen, die Einrückung wäre dann wirkungslos.
*/
export function gruppenOptionen(groups) {
const liste = groups || [];
const vorhanden = new Set(liste.map((g) => g.id));
const nachName = (a, b) => a.name.localeCompare(b.name, "de");
const kinderVon = new Map();
const wurzeln = [];
for (const g of liste) {
const eltern = (g.parent_ids || []).filter((id) => vorhanden.has(id));
if (eltern.length === 0) wurzeln.push(g);
for (const e of eltern) {
if (!kinderVon.has(e)) kinderVon.set(e, []);
kinderVon.get(e).push(g);
}
}
const out = [];
const walk = (g, tiefe, pfad, gesehen) => {
const eigen = `${pfad}/${g.id}`;
out.push({
id: g.id,
key: eigen,
gruppe: g,
tiefe,
label: `${"\u00A0\u00A0\u00A0".repeat(tiefe)}${tiefe > 0 ? "\u21B3 " : ""}${g.name}`,
});
if (gesehen.has(g.id)) return; // Ringschutz
const weiter = new Set(gesehen).add(g.id);
for (const k of (kinderVon.get(g.id) || []).slice().sort(nachName)) {
walk(k, tiefe + 1, eigen, weiter);
}
};
for (const w of wurzeln.slice().sort(nachName)) walk(w, 0, "", new Set());
return out;
}
/**
* Gegenstück zu categoryInfoMap für Gruppen: je Gruppe der Weg von oben.
*
* Anders als bei Kategorien kann es MEHRERE Wege geben („Grillwurst" unter
* „Wurst" und unter „Grillgut"). Angezeigt wird der erste, gefiltert wird über
* alle wer auf „Wurst" filtert, will „Grillwurst" mitsehen.
*/
export function gruppenInfoMap(groups) {
const out = new Map();
for (const g of groups || []) {
const wege = pfade(groups, g.id);
const erster = wege[0]?.length ? wege[0] : [g.name];
const tokens = new Set();
for (const weg of wege) {
weg.forEach((_, i) => tokens.add(weg.slice(0, i + 1).join(" → ")));
}
out.set(g.id, {
parts: erster,
path: erster.join(" → "),
alle: wege.map((w) => w.join(" → ")),
tokens: [...tokens],
});
}
return out;
}