import { useEffect, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { api } from "../api"; import { useAuth } from "../auth"; import Icon from "../components/Icon"; import DataTable from "../components/DataTable"; import { ProduktThumb } from "../components/ProduktBild"; import CategoryPathLabel, { pathParts } from "../components/CategoryPathLabel"; import { categoryInfoMap } from "../categoryPath"; import { gruppenInfoMap } from "../groupGraph"; import { fmt, gebinde, unitShort } from "../units"; // Wie viele Basiseinheiten eine "Artikeleinheit" umfasst (Gebinde oder Produkteinheit). function unitCount(p) { return p.package_size && p.package_size > 0 ? p.package_size : (p.unit_factor || 1); } // Mengenabhaengig, damit "3 Gläser" dasteht und nicht "3 Glas". function countLabel(p, menge) { return p.package_size && p.package_size > 0 ? gebinde(menge, p.package_label) : (p.unit_name || unitShort(p.base_unit)); } function einheitText(p) { const basis = p.unit_name || unitShort(p.base_unit); return p.package_size ? `${basis} · ${p.package_label || "Packung"} ${fmt(p.package_size)} ${unitShort(p.base_unit)}` : basis; } // „Zuletzt geändert" als Datum + Uhrzeit (zum Sortieren/Filtern). function fmtDateTime(iso) { if (!iso) return "–"; return new Date(iso).toLocaleString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", }); } // Verwaltungsart eines Gegenstands (nur für die Gegenstände-Liste sinnvoll). function verwaltungLabel(p) { if (p.individual) return "Einzelstücke"; if (p.bulk) return "Verbrauchsgegenstand"; return "Menge je Lagerort"; } export default function Products({ fixedType = null }) { const { isAdmin } = useAuth(); const [products, setProducts] = 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. const [typ, setTyp] = useState(""); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { // Alle Produkte einmal laden; das Filtern erledigt die Tabelle clientseitig. setLoading(true); api.listProducts("", "") .then(setProducts) .catch((err) => setError(err.message)) .finally(() => setLoading(false)); api.listCategories().then(setCategories).catch(() => {}); api.listGroups().then(setGroups).catch(() => {}); }, []); // Kategorie als Pfad + Tokens (jede Ebene), damit "Klamotten" auch die // Unterkategorien findet und die Oberkategorie im Filter wählbar ist. const catInfo = useMemo(() => categoryInfoMap(categories), [categories]); const grpInfo = useMemo(() => gruppenInfoMap(groups), [groups]); // Zwei getrennte Seiten (Lebensmittel/Gegenstände) fixieren den Typ; sonst der // Umschalter oben. const activeTyp = fixedType || typ; const sichtbar = products.filter((p) => !activeTyp || p.tracking === activeTyp); const titel = fixedType === "food" ? "Lebensmittel" : fixedType === "object" ? "Gegenstände" : "Produkte"; const columns = [ { key: "thumb", header: "", label: "Bild", fixed: true, width: 56, render: (p) => }, { key: "name", header: "Name", grow: true, min: 200, filterText: (p) => p.name, sortValue: (p) => p.name, render: (p) => ( {p.name} {p.expired_count > 0 && ( {p.expired_count} abgelaufen )} ) }, { key: "brand", header: "Marke", width: 150, filterText: (p) => p.brand || "", render: (p) => {p.brand || "–"} }, { key: "category", header: "Kategorie", width: 210, filterText: (p) => catInfo.get(p.category_id)?.path || p.category_name || "", filterValues: (p) => catInfo.get(p.category_id)?.tokens || (p.category_name ? [p.category_name] : [""]), 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. Mit ihrem Weg von oben // dargestellt, wie die Kategorie – ein Etikett verschwiege, dass „Grillwurst" // unter „Wurst" hängt. Gefiltert wird über ALLE Wege, damit „Wurst" auch die // Artikel aus den Untergruppen zeigt. { key: "gruppe", header: "Gruppe", width: 200, filterText: (p) => grpInfo.get(p.group_id)?.path || p.group_name || "", filterValues: (p) => grpInfo.get(p.group_id)?.tokens || (p.group_name ? [p.group_name] : [""]), filterOptionLabel: (v) => (v === "" ? "(Ohne Gruppe)" : ), sortValue: (p) => grpInfo.get(p.group_id)?.path || p.group_name || "", render: (p) => (p.group_id != null ? : ) }, // 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), filterValues: (p) => [verwaltungLabel(p)], sortValue: (p) => verwaltungLabel(p), render: (p) => {verwaltungLabel(p)} }, { key: "unit", header: "Einheit", width: 200, filterText: einheitText, render: einheitText }, { key: "stock", header: "Bestand", width: 130, align: "num", sortValue: (p) => p.stock / (p.unit_factor || 1), render: (p) => `${fmt(p.stock / (p.unit_factor || 1))} ${p.unit_name || unitShort(p.base_unit)}` }, { key: "units", header: "Einheiten (ist / soll)", width: 190, align: "num", sortValue: (p) => p.stock / unitCount(p), render: (p) => { const low = p.min_stock != null && p.stock < p.min_stock; return ( {fmt(p.stock / unitCount(p))} {p.min_stock != null ? ` / ${fmt(p.min_stock / unitCount(p))}` : ""}{" "} {countLabel(p, p.stock / unitCount(p))} {low && niedrig} ); } }, { key: "updated", header: "Zuletzt geändert", label: "Zuletzt geändert", width: 170, sortValue: (p) => (p.updated_at ? new Date(p.updated_at).getTime() : 0), filterText: (p) => fmtDateTime(p.updated_at), render: (p) => {fmtDateTime(p.updated_at)} }, // Kein fixed: die Spalte soll sich verschieben lassen. Dafuer braucht sie // einen sichtbaren Titel – an einem leeren Kopf gibt es nichts zu greifen. { key: "details", header: "Details", label: "Details", width: 90, align: "num", render: (p) => Details }, ].filter(Boolean); return (

{titel}

{loading ? "Wird geladen…" : `${sichtbar.length} ${fixedType ? titel : `Produkte${activeTyp ? (activeTyp === "food" ? " · Lebensmittel" : " · Gegenstände") : " im Katalog"}`}`}
{isAdmin && ( Neues Produkt )}
{error &&
{error}
} {!fixedType && (
)}
p.id} loading={loading} empty="Keine Produkte gefunden." />
); }