Web: Obergruppen zuordnen, Mindestbestaende nach Lagerort

Mindestbestaende-Seite ist jetzt nach Lagerort gegliedert statt nach Artikel -
"Ueberall (egal wo)" zuerst, danach die Lagerorte. Es gibt keine automatische
Leerzeile je Artikel mehr; angelegt wird ueber "+ hinzufuegen" im jeweiligen
Abschnitt, dann steht der Ort schon fest und es fehlen nur Ziel und Menge.
Das Zeilenmodell faellt damit von vier Faellen auf zwei, und weil der Server
einheitlich Basiseinheiten liefert, bleibt genau eine Umrechnung uebrig statt
der drei gegenlaeufigen von vorher.

Gruppen: neue Spalte "Obergruppen" mit Mehrfachauswahl (GroupParentSelect).
Bewusst kein Baum - CategorySelect und die tree-Prop der DataTable koennen nur
einen Elternteil und wuerden eine Gruppe mit zwei Obergruppen doppelt anzeigen.
Die eigene Gruppe und ihre Untergruppen sind in der Auswahl gesperrt, damit die
Ringregel sichtbar wird, bevor der Server sie ablehnt. Produktzahl zeigt
transitiv und direkt ("12 (3 direkt)"), dazu Badges fuer Untergruppen und eine
Warnung, wenn eine Gruppe mit Untergruppen keine Einheit hat - sonst summiert
sie Gramm und Stueck zu einer sinnlosen Zahl.

Einkaufsliste: "Gesamt" heisst "Ueberall", Gruppen zeigen ihre mitgezaehlten
Untergruppen, und eine Fussnote benennt die eine Stelle, die sich nicht
verrechnen laesst (zwei Obergruppen mit gemeinsamer Untergruppe).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-08-15 23:56:18 +02:00
parent 54261b98c0
commit 0537a2b53f
9 changed files with 603 additions and 357 deletions

77
web/src/groupGraph.js Normal file
View File

@@ -0,0 +1,77 @@
// 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);
}