Gebinde verwalten - mit Einzahl und Mehrzahl

Bisher war die Auswahl eine fest im Frontend verdrahtete Liste und es gab nur
eine Form: ueberall stand "3 Glas".

Neu unter Verwaltung > Gebinde: anlegen, umbenennen, loeschen, jeweils mit
Einzahl und Mehrzahl. Die eingebauten Gebinde lassen sich in der Schreibweise
aendern, aber nicht loeschen; ein Gebinde, das ein Artikel verwendet, ebenfalls
nicht.

Der Artikel speichert weiterhin nur die Einzahl als Text - so bleiben
vorhandene Artikel, Sicherungen und CSV-Dateien gueltig, und eine unbekannte
Bezeichnung faellt schlicht auf die Einzahl zurueck. Deshalb zieht ein
Umbenennen die Artikel mit; sonst zeigten sie auf eine Bezeichnung, die es
nicht mehr gibt.

Die Mehrzahl greift jetzt in Artikelliste, Artikelseite (Chargen und Gebinde-
Auswahl), Auslagern und in allen Ablauf- und Einkaufslisten samt Startseite.
Einheiten wie Gramm oder Liter bleiben unveraendert - die haben im Deutschen
keine Mehrzahl.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-23 14:24:42 +02:00
parent 0ba968985f
commit fd9045226b
14 changed files with 511 additions and 22 deletions

View File

@@ -11,7 +11,8 @@ import { useToast } from "../toast";
import { useSettings } from "../settings";
import { asTree } from "../categoryTree";
import {
daysUntil, expiryRowClass, fmt, fromMonthInput, isExpired, relativeExpiry, toMonthInput, unitShort,
daysUntil, expiryRowClass, fmt, fromMonthInput, gebinde, isExpired, relativeExpiry,
toMonthInput, unitShort,
} from "../units";
const EMPTY = {
@@ -20,10 +21,7 @@ const EMPTY = {
group_id: "", category_id: "",
};
// Auswahl für die Gebinde-Bezeichnung ("Packung" ist der leere Standardwert).
const PACKAGE_LABELS = [
"Glas", "Tüte", "Flasche", "Dose", "Tube", "Becher", "Beutel", "Karton", "Riegel", "Rolle",
];
// (Gebinde kommen jetzt aus der Verwaltung, siehe api.listPackageTypes)
// Kanonische Einheit je Basiseinheit (für OFF-Vorschläge und Altbestand).
const CANONICAL_NAME = { piece: "Stück", gram: "Gramm", milliliter: "Milliliter" };
@@ -46,6 +44,7 @@ export default function ProductForm() {
const [groups, setGroups] = useState([]);
const [categories, setCategories] = useState([]);
const [units, setUnits] = useState([]);
const [gebindearten, setGebindearten] = useState([]);
const [error, setError] = useState(null);
const [busy, setBusy] = useState(false);
// Einheit für die Mindestbestand-Eingabe: "package" oder die ID einer Einheit.
@@ -204,12 +203,13 @@ export default function ProductForm() {
useEffect(() => {
async function load() {
try {
const [gs, us, cs] = await Promise.all([
api.listGroups(), api.listUnits(), api.listCategories(),
const [gs, us, cs, pts] = await Promise.all([
api.listGroups(), api.listUnits(), api.listCategories(), api.listPackageTypes(),
]);
setGroups(gs);
setUnits(us);
setCategories(cs);
setGebindearten(pts);
try {
const s = await api.listSettings();
const row = s.find((x) => x.key === "expiry_warning_days");
@@ -415,12 +415,18 @@ export default function ProductForm() {
</div>
<div className="row">
<label className="grow">
Bezeichnung der Einheit
<span className="tip" title="Verwaltet unter Verwaltung → Gebinde, dort auch mit Mehrzahl.">
Gebinde
</span>
<select value={form.package_label} onChange={(e) => set("package_label", e.target.value)}
disabled={readOnly}>
<option value="">Packung (Standard)</option>
{PACKAGE_LABELS.map((l) => <option key={l} value={l}>{l}</option>)}
{form.package_label && !PACKAGE_LABELS.includes(form.package_label) && (
{gebindearten.map((g) => (
<option key={g.id} value={g.singular}>{g.singular} / {g.plural}</option>
))}
{/* Ein Artikel kann eine Bezeichnung tragen, die es in der
Verwaltung (noch) nicht gibt etwa aus einem Import. */}
{form.package_label && !gebindearten.some((g) => g.singular === form.package_label) && (
<option value={form.package_label}>{form.package_label}</option>
)}
</select>
@@ -555,7 +561,9 @@ function LotsCard({ product, lots, baseShort, warnDays, isAdmin, onChanged, onEr
// Leitangabe: Packungen, sobald eine Packungsgröße hinterlegt ist
// sonst die Produkteinheit. Darunter steht die jeweils andere Angabe klein.
const primaryFactor = pkgSize > 0 ? pkgSize : unitFactor;
const primaryLabel = pkgSize > 0 ? pkgLabel : unitName;
// Mengenabhaengig: "1 Glas", aber "3 Gläser". Einheiten (Gramm, Liter)
// bleiben unveraendert - die haben im Deutschen keine Mehrzahl.
const primaryLabel = (menge) => (pkgSize > 0 ? gebinde(menge, pkgLabel) : unitName);
function secondary(qty) {
if (pkgSize > 0) return `${fmt(qty / unitFactor)} ${unitName}`;
@@ -667,7 +675,7 @@ function LotsCard({ product, lots, baseShort, warnDays, isAdmin, onChanged, onEr
</div>
) : (
<>
{fmt(l.quantity / primaryFactor)} {primaryLabel}
{fmt(l.quantity / primaryFactor)} {primaryLabel(l.quantity / primaryFactor)}
{secondary(l.quantity) && (
<div className="muted small">{secondary(l.quantity)}</div>
)}