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>
149 lines
4.8 KiB
JavaScript
149 lines
4.8 KiB
JavaScript
// Gruppen bilden einen gerichteten azyklischen Graphen, keinen Baum:
|
||
// „Grillwurst" hängt unter „Wurst" UND unter „Grillgut". Deshalb hier
|
||
// Mengen-Helfer statt der Baum-Helfer aus categoryTree.js – ein Baum-Walk
|
||
// würde eine über zwei Wege erreichbare Gruppe doppelt liefern.
|
||
//
|
||
// Alle Funktionen schützen sich mit einem gesehen-Set gegen Ringe. Die API
|
||
// lässt keinen zu, eine eingespielte Sicherung könnte aber einen mitbringen.
|
||
|
||
const byId = (groups) => new Map((groups || []).map((g) => [g.id, g]));
|
||
|
||
/** IDs aller Untergruppen (transitiv, ohne die Gruppe selbst). */
|
||
export function nachfahrenIds(groups, id) {
|
||
const map = byId(groups);
|
||
const gesehen = new Set();
|
||
const offen = [id];
|
||
while (offen.length) {
|
||
const cur = offen.pop();
|
||
for (const kind of map.get(cur)?.child_ids || []) {
|
||
if (!gesehen.has(kind)) {
|
||
gesehen.add(kind);
|
||
offen.push(kind);
|
||
}
|
||
}
|
||
}
|
||
gesehen.delete(id);
|
||
return gesehen;
|
||
}
|
||
|
||
/** IDs aller Obergruppen (transitiv, ohne die Gruppe selbst). */
|
||
export function vorfahrenIds(groups, id) {
|
||
const map = byId(groups);
|
||
const gesehen = new Set();
|
||
const offen = [...(map.get(id)?.parent_ids || [])];
|
||
while (offen.length) {
|
||
const cur = offen.pop();
|
||
if (gesehen.has(cur)) continue;
|
||
gesehen.add(cur);
|
||
offen.push(...(map.get(cur)?.parent_ids || []));
|
||
}
|
||
gesehen.delete(id);
|
||
return gesehen;
|
||
}
|
||
|
||
/**
|
||
* Alle Wege zu einer Gruppe, z.B. [["Wurst","Grillwurst"], ["Grillgut","Grillwurst"]].
|
||
* Anders als bei Kategorien gibt es nicht DEN einen Pfad – deshalb eine Liste.
|
||
*/
|
||
export function pfade(groups, id) {
|
||
const map = byId(groups);
|
||
const bauen = (cur, gesehen) => {
|
||
const g = map.get(cur);
|
||
if (!g) return [[]];
|
||
const eltern = (g.parent_ids || []).filter((p) => !gesehen.has(p));
|
||
if (eltern.length === 0) return [[g.name]];
|
||
const raus = [];
|
||
for (const p of eltern) {
|
||
for (const oben of bauen(p, new Set([...gesehen, cur]))) {
|
||
raus.push([...oben, g.name]);
|
||
}
|
||
}
|
||
return raus;
|
||
};
|
||
return bauen(id, new Set());
|
||
}
|
||
|
||
/** Die Wege als ein Text: „Wurst → Grillwurst · Grillgut → Grillwurst". */
|
||
export function pfadText(groups, id) {
|
||
return pfade(groups, id)
|
||
.map((teile) => teile.join(" → "))
|
||
.join(" · ");
|
||
}
|
||
|
||
/** Namen der Obergruppen, für Chips und Spaltentexte. */
|
||
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;
|
||
}
|