- g.id} empty="Noch keine Gruppen." />
+ g._pfad}
+ tree={{ column: "name", idOf: (g) => g._pfad, parentIdOf: (g) => g._elternPfad }}
+ empty="Noch keine Gruppen." />
diff --git a/web/src/pages/MinStock.jsx b/web/src/pages/MinStock.jsx
index 9412fb9..04fc94f 100644
--- a/web/src/pages/MinStock.jsx
+++ b/web/src/pages/MinStock.jsx
@@ -4,7 +4,7 @@ import { useAuth } from "../auth";
import { useConfirm } from "../confirm";
import Icon from "../components/Icon";
import { locationOptions, locationPathById } from "../locationPath";
-import { fmt, gebinde, kindShort } from "../units";
+import { anzahlWort, fmt, gebinde, grpFactor, grpPkgMode, grpUnit, kindShort } from "../units";
// Anzeigeeinheit eines Produkts: die im Formular gewählte Einheit (Gramm,
// Milliliter, Stück) – NICHT die Packung. „1 Packung" ist nichtssagend, weil eine
@@ -24,13 +24,9 @@ const effFactor = (p) => (pkgMode(p) ? p.package_size : dispFactor(p));
// Einheitslabel der gewählten Erfassungseinheit, passend zur Menge (1 Glas / 3 Gläser).
const effUnit = (p, menge = 1) => (pkgMode(p) ? gebinde(menge, p.package_label || "Packung") : dispLabel(p));
-// Gruppen: analog, mit dem GRUPPEN-Gebinde (Richtwert der Gruppe, nicht der
-// einzelnen Produkte).
-const grpPkgMode = (g) => !!g.min_stock_in_packages && !!(g.package_size && g.package_size > 0);
-const grpFactor = (g) => (grpPkgMode(g) ? g.package_size : (g.min_stock_unit_factor || 1));
-const grpUnit = (g, menge = 1) => (grpPkgMode(g)
- ? gebinde(menge, g.package_label || "Packung")
- : (g.min_stock_unit_name || kindShort(g.kind) || ""));
+// Gruppen rechnen in ihrer verwalteten Einheit ODER im Gruppen-Gebinde –
+// grpPkgMode/grpFactor/grpUnit stehen in units.js, weil Groups.jsx sie genauso
+// braucht (und ohne sie den Bestand falsch beschriftet hat).
// „Überall" ist der Ort NULL. Im
diff --git a/web/src/pages/Products.jsx b/web/src/pages/Products.jsx
index 5e375a4..1c329b0 100644
--- a/web/src/pages/Products.jsx
+++ b/web/src/pages/Products.jsx
@@ -91,6 +91,16 @@ export default function Products({ fixedType = null }) {
filterOptionLabel: (v) => (v === "" ? "(Leere)" : ),
sortValue: (p) => catInfo.get(p.category_id)?.path || p.category_name || "",
render: (p) => },
+ // Gruppe: zählt Bestände mehrerer Marken zusammen. Als eigene Spalte, damit
+ // sich die Liste danach filtern lässt („zeig mir alles aus Pesto Rosso").
+ { key: "gruppe", header: "Gruppe", width: 170,
+ filterText: (p) => p.group_name || "",
+ filterValues: (p) => [p.group_name || ""],
+ filterOptionLabel: (v) => (v === "" ? "(Ohne Gruppe)" : v),
+ sortValue: (p) => p.group_name || "",
+ render: (p) => (p.group_name
+ ? {p.group_name}
+ : –) },
// Nur in der Gegenstände-Liste: Menge je Lagerort / Einzelstücke / Verbrauchsgegenstand.
activeTyp === "object" && { key: "verwaltung", header: "Verwaltung", label: "Verwaltung", width: 190,
filterText: (p) => verwaltungLabel(p),
diff --git a/web/src/pages/ShoppingList.jsx b/web/src/pages/ShoppingList.jsx
index acb9bad..0680ed8 100644
--- a/web/src/pages/ShoppingList.jsx
+++ b/web/src/pages/ShoppingList.jsx
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { api } from "../api";
import Icon from "../components/Icon";
import ShoppingNeedText from "../components/ShoppingNeedText";
+import { anzahlWort } from "../units";
export default function ShoppingList() {
const [items, setItems] = useState([]);
@@ -50,7 +51,7 @@ export default function ShoppingList() {
Gruppe
{it.subgroup_count > 0 && (
- inkl. {it.subgroup_count} Untergruppen
+ inkl. {anzahlWort(it.subgroup_count, "Untergruppe", "Untergruppen")}
)}
{it.name}
@@ -95,7 +96,7 @@ export default function ShoppingList() {
Gruppe
{it.subgroup_count > 0 && (
- inkl. {it.subgroup_count} Untergruppen
+ inkl. {anzahlWort(it.subgroup_count, "Untergruppe", "Untergruppen")}
)}
{it.name}
diff --git a/web/src/styles.css b/web/src/styles.css
index 629ee37..3361295 100644
--- a/web/src/styles.css
+++ b/web/src/styles.css
@@ -387,6 +387,8 @@ td select { width: auto; min-width: 0; max-width: 100%; }
/* Zelle mit Eingabe plus Kennzeichen (z.B. "niedrig"): eine Zeile, damit das
Kennzeichen immer an derselben Stelle sitzt statt mal darunter zu rutschen. */
.cell-row { display: flex; align-items: center; gap: 6px; }
+/* Zwei Zeilen in einer Zelle: Steuerelement oben, Erläuterung darunter. */
+.cell-col { display: flex; flex-direction: column; gap: 2px; align-items: stretch; }
/* Beschriftung mit Erklaerung im Tooltip: die gepunktete Linie zeigt an, dass
dort etwas steht - so bleibt das Formular kurz, ohne Wissen zu verstecken. */
.tip { border-bottom: 1px dotted var(--border-strong); }
diff --git a/web/src/units.js b/web/src/units.js
index 0bc828f..f51f22b 100644
--- a/web/src/units.js
+++ b/web/src/units.js
@@ -54,6 +54,35 @@ export function kindShort(kind) {
return KIND_SHORT[kind] || "";
}
+/** „1 Untergruppe" / „2 Untergruppen" – Ein-/Mehrzahl an einer Stelle. */
+export function anzahlWort(n, einzahl, mehrzahl) {
+ return `${n} ${Number(n) === 1 ? einzahl : mehrzahl}`;
+}
+
+// ---- Gruppen: in welcher Einheit sie zählen ----
+// Eine Gruppe rechnet entweder in ihrer verwalteten Einheit (Gramm, Kilogramm)
+// ODER – wenn ein Gruppen-Gebinde definiert und aktiv ist – in ganzen Packungen.
+// Beides zusammen an EINER Stelle, weil genau diese Unterscheidung vorher an
+// zwei Orten stand und in Groups.jsx fehlte: dort klebte immer „Gramm" hinter
+// einem Bestand, der längst in Gläsern gerechnet war.
+
+/** Zählt die Gruppe in ihrem Gebinde (statt in der verwalteten Einheit)? */
+export function grpPkgMode(g) {
+ return !!g?.min_stock_in_packages && !!(g.package_size && g.package_size > 0);
+}
+
+/** Basiseinheiten je Erfassungseinheit der Gruppe. */
+export function grpFactor(g) {
+ return grpPkgMode(g) ? g.package_size : (g?.min_stock_unit_factor || 1);
+}
+
+/** Beschriftung der Erfassungseinheit, passend zur Menge (1 Glas / 3 Gläser). */
+export function grpUnit(g, menge = 1) {
+ return grpPkgMode(g)
+ ? gebinde(menge, g.package_label || "Packung")
+ : (g?.min_stock_unit_name || kindShort(g?.kind) || "");
+}
+
// Einheiten-ART je Basiseinheit – Spiegelbild von conversion.py::KIND_OF_BASE.
export const KIND_OF_BASE = { piece: "count", gram: "weight", milliliter: "volume" };