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:
@@ -327,3 +327,47 @@ struct GroupParentPicker: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Eine Zeile fuer ein Gruppen-Auswahlfeld: Gruppe plus ihre Tiefe im Baum.
|
||||||
|
struct GruppenOption: Identifiable {
|
||||||
|
let id: String // Pfad – eine Gruppe kann unter mehreren haengen
|
||||||
|
let gruppe: GroupItem
|
||||||
|
let tiefe: Int
|
||||||
|
/// Eingerueckte Beschriftung fuer den Picker.
|
||||||
|
var label: String {
|
||||||
|
String(repeating: " ", count: tiefe) + (tiefe > 0 ? "↳ " : "") + gruppe.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gruppen als Baum ausrollen – fuer Auswahlfelder.
|
||||||
|
///
|
||||||
|
/// Eine Gruppe mit mehreren Obergruppen erscheint unter JEDER; das ist der Sinn
|
||||||
|
/// mehrerer Obergruppen, und ausgewaehlt wird ohnehin dieselbe ID.
|
||||||
|
func gruppenOptionen(_ groups: [GroupItem]) -> [GruppenOption] {
|
||||||
|
let vorhanden = Set(groups.map(\.id))
|
||||||
|
var kinderVon: [Int: [GroupItem]] = [:]
|
||||||
|
var wurzeln: [GroupItem] = []
|
||||||
|
for g in groups {
|
||||||
|
let eltern = (g.parentIds ?? []).filter { vorhanden.contains($0) }
|
||||||
|
if eltern.isEmpty { wurzeln.append(g) }
|
||||||
|
for e in eltern { kinderVon[e, default: []].append(g) }
|
||||||
|
}
|
||||||
|
let nachName: (GroupItem, GroupItem) -> Bool = {
|
||||||
|
$0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending
|
||||||
|
}
|
||||||
|
|
||||||
|
var out: [GruppenOption] = []
|
||||||
|
func walk(_ g: GroupItem, _ tiefe: Int, _ pfad: String, _ gesehen: Set<Int>) {
|
||||||
|
let eigen = "\(pfad)/\(g.id)"
|
||||||
|
out.append(GruppenOption(id: eigen, gruppe: g, tiefe: tiefe))
|
||||||
|
if gesehen.contains(g.id) { return } // Ringschutz
|
||||||
|
var weiter = gesehen
|
||||||
|
weiter.insert(g.id)
|
||||||
|
for k in (kinderVon[g.id] ?? []).sorted(by: nachName) {
|
||||||
|
walk(k, tiefe + 1, eigen, weiter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for w in wurzeln.sorted(by: nachName) { walk(w, 0, "", []) }
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -217,8 +217,9 @@ struct ProductDetailView: View {
|
|||||||
Section {
|
Section {
|
||||||
Picker("Gruppe", selection: $groupId) {
|
Picker("Gruppe", selection: $groupId) {
|
||||||
Text("– keine –").tag(Int?.none)
|
Text("– keine –").tag(Int?.none)
|
||||||
ForEach(groups) { gruppe in
|
// Als Baum, damit sichtbar ist, was unter was haengt.
|
||||||
Text(gruppe.name).tag(Int?.some(gruppe.id))
|
ForEach(gruppenOptionen(groups)) { eintrag in
|
||||||
|
Text(eintrag.label).tag(Int?.some(eintrag.gruppe.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} footer: {
|
} footer: {
|
||||||
|
|||||||
@@ -166,8 +166,9 @@ struct ProductFormView: View {
|
|||||||
Section {
|
Section {
|
||||||
Picker("Gruppe", selection: $selectedGroupId) {
|
Picker("Gruppe", selection: $selectedGroupId) {
|
||||||
Text("– keine –").tag(Int?.none)
|
Text("– keine –").tag(Int?.none)
|
||||||
ForEach(groups) { gruppe in
|
// Als Baum, damit sichtbar ist, was unter was haengt.
|
||||||
Text(gruppe.name).tag(Int?.some(gruppe.id))
|
ForEach(gruppenOptionen(groups)) { eintrag in
|
||||||
|
Text(eintrag.label).tag(Int?.some(eintrag.gruppe.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if session.isAdmin {
|
if session.isAdmin {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import { nachfahrenIds, pfadText } from "../groupGraph";
|
import { gruppenOptionen, nachfahrenIds, pfadText } from "../groupGraph";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mehrfachauswahl der Obergruppen einer Gruppe.
|
* 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 gesperrt = selfId == null ? new Set() : new Set([selfId, ...nachfahrenIds(groups, selfId)]);
|
||||||
const nameById = Object.fromEntries(groups.map((g) => [g.id, g.name]));
|
const nameById = Object.fromEntries(groups.map((g) => [g.id, g.name]));
|
||||||
|
|
||||||
const sichtbar = groups
|
// Als Baum, damit die Struktur beim Zuordnen sichtbar ist. Beim Suchen
|
||||||
.filter((g) => !filter || g.name.toLowerCase().includes(filter.toLowerCase()))
|
// 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()
|
.slice()
|
||||||
.sort((a, b) => a.name.localeCompare(b.name, "de"));
|
.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
|
const label = value?.length
|
||||||
? value.map((id) => nameById[id]).filter(Boolean).join(", ")
|
? value.map((id) => nameById[id]).filter(Boolean).join(", ")
|
||||||
@@ -115,22 +120,23 @@ export default function GroupParentSelect({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{sichtbar.map((g) => {
|
{sichtbar.map((o) => {
|
||||||
const aus = gesperrt.has(g.id);
|
const aus = gesperrt.has(o.id);
|
||||||
const an = gewaehlt.has(g.id);
|
const an = gewaehlt.has(o.id);
|
||||||
return (
|
return (
|
||||||
<div key={g.id} className="tree-pop-line">
|
<div key={o.key} className="tree-pop-line" style={{ paddingLeft: o.tiefe * 14 }}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`tree-pop-opt ${an ? "sel" : ""}`}
|
className={`tree-pop-opt ${an ? "sel" : ""}`}
|
||||||
disabled={aus}
|
disabled={aus}
|
||||||
title={aus
|
title={aus
|
||||||
? "Würde einen Ring erzeugen (die Gruppe selbst oder eine ihrer Untergruppen)"
|
? "Würde einen Ring erzeugen (die Gruppe selbst oder eine ihrer Untergruppen)"
|
||||||
: pfadText(groups, g.id)}
|
: pfadText(groups, o.id)}
|
||||||
onClick={() => umschalten(g.id)}
|
onClick={() => umschalten(o.id)}
|
||||||
>
|
>
|
||||||
<span style={{ display: "inline-block", width: 18 }}>{an ? "✓" : ""}</span>
|
<span style={{ display: "inline-block", width: 18 }}>{an ? "✓" : ""}</span>
|
||||||
{g.name}
|
{o.tiefe > 0 && <span className="muted">↳ </span>}
|
||||||
|
{o.gruppe.name}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -75,3 +75,74 @@ export function elternNamen(groups, group) {
|
|||||||
const map = byId(groups);
|
const map = byId(groups);
|
||||||
return (group?.parent_ids || []).map((id) => map.get(id)?.name).filter(Boolean);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { asTree } from "../categoryTree";
|
|||||||
import CategorySelect from "../components/CategorySelect";
|
import CategorySelect from "../components/CategorySelect";
|
||||||
import { locationOptions } from "../locationPath";
|
import { locationOptions } from "../locationPath";
|
||||||
import { buildUnitOptions, fmt, fromMonthInput, isExpired, monthInputToLastDay } from "../units";
|
import { buildUnitOptions, fmt, fromMonthInput, isExpired, monthInputToLastDay } from "../units";
|
||||||
|
import { gruppenOptionen } from "../groupGraph";
|
||||||
|
|
||||||
const emptyLine = () => ({ quantity: "", best_before: "" });
|
const emptyLine = () => ({ quantity: "", best_before: "" });
|
||||||
|
|
||||||
@@ -159,7 +160,9 @@ export default function CheckIn() {
|
|||||||
Gruppe
|
Gruppe
|
||||||
<select value={newGroupId} onChange={(e) => setNewGroupId(e.target.value)}>
|
<select value={newGroupId} onChange={(e) => setNewGroupId(e.target.value)}>
|
||||||
<option value="">– keine –</option>
|
<option value="">– keine –</option>
|
||||||
{groups.map((g) => <option key={g.id} value={g.id}>{g.name}</option>)}
|
{gruppenOptionen(groups).map((o) => (
|
||||||
|
<option key={o.key} value={o.id}>{o.label}</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
<span className="muted small">
|
<span className="muted small">
|
||||||
Zählt Bestände mehrerer Marken zusammen. Der EAN-Code landet dann auch dort.
|
Zählt Bestände mehrerer Marken zusammen. Der EAN-Code landet dann auch dort.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useAuth } from "../auth";
|
|||||||
import { useConfirm } from "../confirm";
|
import { useConfirm } from "../confirm";
|
||||||
import Icon from "../components/Icon";
|
import Icon from "../components/Icon";
|
||||||
import { locationOptions, locationPathById } from "../locationPath";
|
import { locationOptions, locationPathById } from "../locationPath";
|
||||||
|
import { gruppenOptionen } from "../groupGraph";
|
||||||
import { anzahlWort, fmt, gebinde, grpFactor, grpPkgMode, grpUnit, kindShort } from "../units";
|
import { anzahlWort, fmt, gebinde, grpFactor, grpPkgMode, grpUnit, kindShort } from "../units";
|
||||||
|
|
||||||
// Anzeigeeinheit eines Produkts: die im Formular gewählte Einheit (Gramm,
|
// Anzeigeeinheit eines Produkts: die im Formular gewählte Einheit (Gramm,
|
||||||
@@ -366,12 +367,18 @@ export default function MinStock() {
|
|||||||
{draft.kind === "product" ? "Lebensmittel / Verbrauchsgegenstand" : "Gruppe"}
|
{draft.kind === "product" ? "Lebensmittel / Verbrauchsgegenstand" : "Gruppe"}
|
||||||
<select value={draft.targetId} onChange={(e) => setDraft({ ...draft, targetId: e.target.value })}>
|
<select value={draft.targetId} onChange={(e) => setDraft({ ...draft, targetId: e.target.value })}>
|
||||||
<option value="">– wählen –</option>
|
<option value="">– wählen –</option>
|
||||||
{zielListe.map((t) => (
|
{/* Gruppen als Baum, damit sichtbar ist, was unter was hängt. */}
|
||||||
|
{draft.kind === "group"
|
||||||
|
? gruppenOptionen(groups).map((o) => (
|
||||||
|
<option key={o.key} value={o.id}>
|
||||||
|
{o.label}
|
||||||
|
{o.gruppe.child_ids?.length
|
||||||
|
? ` (inkl. ${anzahlWort(o.gruppe.child_ids.length, "Untergruppe", "Untergruppen")})` : ""}
|
||||||
|
</option>
|
||||||
|
))
|
||||||
|
: zielListe.map((t) => (
|
||||||
<option key={t.id} value={t.id}>
|
<option key={t.id} value={t.id}>
|
||||||
{t.name}
|
{t.name}{t.brand ? ` · ${t.brand}` : ""}
|
||||||
{draft.kind === "product" && t.brand ? ` · ${t.brand}` : ""}
|
|
||||||
{draft.kind === "group" && t.child_ids?.length
|
|
||||||
? ` (inkl. ${anzahlWort(t.child_ids.length, "Untergruppe", "Untergruppen")})` : ""}
|
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import ObjektBestand from "../components/ObjektBestand";
|
|||||||
import Einzelstuecke from "../components/Einzelstuecke";
|
import Einzelstuecke from "../components/Einzelstuecke";
|
||||||
import SplitLotDialog from "../components/SplitLotDialog";
|
import SplitLotDialog from "../components/SplitLotDialog";
|
||||||
import { locationOptions, locationPathById } from "../locationPath";
|
import { locationOptions, locationPathById } from "../locationPath";
|
||||||
|
import { gruppenOptionen } from "../groupGraph";
|
||||||
import {
|
import {
|
||||||
BASE_UNITS, daysUntil, expiryRowClass, fmt, fromMonthInput, gebinde, isExpired,
|
BASE_UNITS, daysUntil, expiryRowClass, fmt, fromMonthInput, gebinde, isExpired,
|
||||||
relativeExpiry, toMonthInput, unitShort, zweitFaktor,
|
relativeExpiry, toMonthInput, unitShort, zweitFaktor,
|
||||||
@@ -968,7 +969,9 @@ export default function ProductForm() {
|
|||||||
</span>
|
</span>
|
||||||
<select value={form.group_id} onChange={(e) => set("group_id", e.target.value)} disabled={readOnly}>
|
<select value={form.group_id} onChange={(e) => set("group_id", e.target.value)} disabled={readOnly}>
|
||||||
<option value="">– keine –</option>
|
<option value="">– keine –</option>
|
||||||
{groups.map((g) => <option key={g.id} value={g.id}>{g.name}</option>)}
|
{gruppenOptionen(groups).map((o) => (
|
||||||
|
<option key={o.key} value={o.id}>{o.label}</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -1043,7 +1046,9 @@ export default function ProductForm() {
|
|||||||
<select value={form.group_id} onChange={(e) => set("group_id", e.target.value)}
|
<select value={form.group_id} onChange={(e) => set("group_id", e.target.value)}
|
||||||
disabled={readOnly}>
|
disabled={readOnly}>
|
||||||
<option value="">– keine –</option>
|
<option value="">– keine –</option>
|
||||||
{groups.map((g) => <option key={g.id} value={g.id}>{g.name}</option>)}
|
{gruppenOptionen(groups).map((o) => (
|
||||||
|
<option key={o.key} value={o.id}>{o.label}</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import DataTable from "../components/DataTable";
|
|||||||
import { ProduktThumb } from "../components/ProduktBild";
|
import { ProduktThumb } from "../components/ProduktBild";
|
||||||
import CategoryPathLabel, { pathParts } from "../components/CategoryPathLabel";
|
import CategoryPathLabel, { pathParts } from "../components/CategoryPathLabel";
|
||||||
import { categoryInfoMap } from "../categoryPath";
|
import { categoryInfoMap } from "../categoryPath";
|
||||||
|
import { gruppenInfoMap } from "../groupGraph";
|
||||||
import { fmt, gebinde, unitShort } from "../units";
|
import { fmt, gebinde, unitShort } from "../units";
|
||||||
|
|
||||||
// Wie viele Basiseinheiten eine "Artikeleinheit" umfasst (Gebinde oder Produkteinheit).
|
// Wie viele Basiseinheiten eine "Artikeleinheit" umfasst (Gebinde oder Produkteinheit).
|
||||||
@@ -43,6 +44,8 @@ export default function Products({ fixedType = null }) {
|
|||||||
const { isAdmin } = useAuth();
|
const { isAdmin } = useAuth();
|
||||||
const [products, setProducts] = useState([]);
|
const [products, setProducts] = useState([]);
|
||||||
const [categories, setCategories] = 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.
|
// "" = alle, sonst "food"/"object": trennt Lebensmittel und Gegenstände.
|
||||||
const [typ, setTyp] = useState("");
|
const [typ, setTyp] = useState("");
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
@@ -56,11 +59,13 @@ export default function Products({ fixedType = null }) {
|
|||||||
.catch((err) => setError(err.message))
|
.catch((err) => setError(err.message))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
api.listCategories().then(setCategories).catch(() => {});
|
api.listCategories().then(setCategories).catch(() => {});
|
||||||
|
api.listGroups().then(setGroups).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Kategorie als Pfad + Tokens (jede Ebene), damit "Klamotten" auch die
|
// Kategorie als Pfad + Tokens (jede Ebene), damit "Klamotten" auch die
|
||||||
// Unterkategorien findet und die Oberkategorie im Filter wählbar ist.
|
// Unterkategorien findet und die Oberkategorie im Filter wählbar ist.
|
||||||
const catInfo = useMemo(() => categoryInfoMap(categories), [categories]);
|
const catInfo = useMemo(() => categoryInfoMap(categories), [categories]);
|
||||||
|
const grpInfo = useMemo(() => gruppenInfoMap(groups), [groups]);
|
||||||
// Zwei getrennte Seiten (Lebensmittel/Gegenstände) fixieren den Typ; sonst der
|
// Zwei getrennte Seiten (Lebensmittel/Gegenstände) fixieren den Typ; sonst der
|
||||||
// Umschalter oben.
|
// Umschalter oben.
|
||||||
const activeTyp = fixedType || typ;
|
const activeTyp = fixedType || typ;
|
||||||
@@ -91,15 +96,20 @@ export default function Products({ fixedType = null }) {
|
|||||||
filterOptionLabel: (v) => (v === "" ? "(Leere)" : <CategoryPathLabel parts={pathParts(v)} />),
|
filterOptionLabel: (v) => (v === "" ? "(Leere)" : <CategoryPathLabel parts={pathParts(v)} />),
|
||||||
sortValue: (p) => catInfo.get(p.category_id)?.path || p.category_name || "",
|
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 || "–"} /> },
|
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
|
// Gruppe: zählt Bestände mehrerer Marken zusammen. Mit ihrem Weg von oben
|
||||||
// sich die Liste danach filtern lässt („zeig mir alles aus Pesto Rosso").
|
// dargestellt, wie die Kategorie – ein Etikett verschwiege, dass „Grillwurst"
|
||||||
{ key: "gruppe", header: "Gruppe", width: 170,
|
// unter „Wurst" hängt. Gefiltert wird über ALLE Wege, damit „Wurst" auch die
|
||||||
filterText: (p) => p.group_name || "",
|
// Artikel aus den Untergruppen zeigt.
|
||||||
filterValues: (p) => [p.group_name || ""],
|
{ key: "gruppe", header: "Gruppe", width: 200,
|
||||||
filterOptionLabel: (v) => (v === "" ? "(Ohne Gruppe)" : v),
|
filterText: (p) => grpInfo.get(p.group_id)?.path || p.group_name || "",
|
||||||
sortValue: (p) => p.group_name || "",
|
filterValues: (p) => grpInfo.get(p.group_id)?.tokens
|
||||||
render: (p) => (p.group_name
|
|| (p.group_name ? [p.group_name] : [""]),
|
||||||
? <span className="badge accent">{p.group_name}</span>
|
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>) },
|
: <span className="muted">–</span>) },
|
||||||
// Nur in der Gegenstände-Liste: Menge je Lagerort / Einzelstücke / Verbrauchsgegenstand.
|
// Nur in der Gegenstände-Liste: Menge je Lagerort / Einzelstücke / Verbrauchsgegenstand.
|
||||||
activeTyp === "object" && { key: "verwaltung", header: "Verwaltung", label: "Verwaltung", width: 190,
|
activeTyp === "object" && { key: "verwaltung", header: "Verwaltung", label: "Verwaltung", width: 190,
|
||||||
|
|||||||
Reference in New Issue
Block a user