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

@@ -1,7 +1,7 @@
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import Icon from "./Icon";
import { nachfahrenIds, pfadText } from "../groupGraph";
import { gruppenOptionen, nachfahrenIds, pfadText } from "../groupGraph";
/**
* Mehrfachauswahl der Obergruppen einer Gruppe.
@@ -35,10 +35,15 @@ export default function GroupParentSelect({
const gesperrt = selfId == null ? new Set() : new Set([selfId, ...nachfahrenIds(groups, selfId)]);
const nameById = Object.fromEntries(groups.map((g) => [g.id, g.name]));
const sichtbar = groups
.filter((g) => !filter || g.name.toLowerCase().includes(filter.toLowerCase()))
.slice()
.sort((a, b) => a.name.localeCompare(b.name, "de"));
// Als Baum, damit die Struktur beim Zuordnen sichtbar ist. Beim Suchen
// flacht die Liste ab eine Einrückung ohne ihre Eltern wäre irreführend.
const sichtbar = filter
? groups
.filter((g) => g.name.toLowerCase().includes(filter.toLowerCase()))
.slice()
.sort((a, b) => a.name.localeCompare(b.name, "de"))
.map((g) => ({ id: g.id, key: `f${g.id}`, gruppe: g, tiefe: 0, label: g.name }))
: gruppenOptionen(groups);
const label = value?.length
? value.map((id) => nameById[id]).filter(Boolean).join(", ")
@@ -115,22 +120,23 @@ export default function GroupParentSelect({
/>
</div>
)}
{sichtbar.map((g) => {
const aus = gesperrt.has(g.id);
const an = gewaehlt.has(g.id);
{sichtbar.map((o) => {
const aus = gesperrt.has(o.id);
const an = gewaehlt.has(o.id);
return (
<div key={g.id} className="tree-pop-line">
<div key={o.key} className="tree-pop-line" style={{ paddingLeft: o.tiefe * 14 }}>
<button
type="button"
className={`tree-pop-opt ${an ? "sel" : ""}`}
disabled={aus}
title={aus
? "Würde einen Ring erzeugen (die Gruppe selbst oder eine ihrer Untergruppen)"
: pfadText(groups, g.id)}
onClick={() => umschalten(g.id)}
: pfadText(groups, o.id)}
onClick={() => umschalten(o.id)}
>
<span style={{ display: "inline-block", width: 18 }}>{an ? "✓" : ""}</span>
{g.name}
{o.tiefe > 0 && <span className="muted"> </span>}
{o.gruppe.name}
</button>
</div>
);