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 SplitLotDialog from "../components/SplitLotDialog"; import { ProduktThumb } from "../components/ProduktBild"; import { useSettings } from "../settings"; import { locationOptions, locationPathById } from "../locationPath"; import { expiryRowClass, fmt, gebinde, unitShort } from "../units"; // Artikeleinheit einer Charge (Gebinde, sonst Produkteinheit). const artFactor = (l) => (l.package_size && l.package_size > 0 ? l.package_size : (l.unit_factor || 1)); function mengeText(l) { const menge = l.quantity / artFactor(l); const label = l.package_size && l.package_size > 0 ? gebinde(menge, l.package_label) : (l.unit_name || unitShort(l.base_unit)); return `${fmt(menge)} ${label}`; } /** * Übergreifende Chargenliste (wie die Einzelstückliste, aber für Chargen). Zeigt * je Charge Artikel, Menge, MHD und Lagerort; der Lagerort ist je Zeile direkt * änderbar, und alle Chargen ohne Ort lassen sich in einem Klick zuordnen. */ export default function Charges() { const { isAdmin } = useAuth(); const { formatBestBefore } = useSettings(); const [lots, setLots] = useState([]); const [locations, setLocations] = useState([]); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const [bulkLoc, setBulkLoc] = useState(""); const [selected, setSelected] = useState(() => new Set()); const [visible, setVisible] = useState([]); // aktuell gefiltert sichtbare Chargen const [splitting, setSplitting] = useState(null); // Charge, die gerade aufgeteilt wird const [warnDays, setWarnDays] = useState(7); // Warnfrist für „bald ablaufend" async function load() { setLoading(true); try { const [ls, locs] = await Promise.all([api.listAllLots(), api.listLocations()]); setLots(ls); setLocations(locs); } catch (err) { setError(err.message); } finally { setLoading(false); } } useEffect(() => { load(); }, []); // Warnfrist (Tage) für die gelbe „bald ablaufend"-Markierung – wie auf der Artikelseite. useEffect(() => { api.listSettings() .then((s) => { const r = s.find((x) => x.key === "expiry_warning_days"); if (r) setWarnDays(parseInt(r.value, 10) || 7); }) .catch(() => {}); }, []); const ohneOrt = useMemo(() => lots.filter((l) => !l.location_id), [lots]); function toggle(id) { setSelected((alt) => { const neu = new Set(alt); if (neu.has(id)) neu.delete(id); else neu.add(id); return neu; }); } // Menge von IDs zur Auswahl hinzufügen (für „alle sichtbaren/ohne Ort"). function selectAll(ids) { setSelected(new Set(ids)); } function clearSelection() { setSelected(new Set()); } async function setLocation(lotId, locId) { setError(null); try { await api.updateLot(lotId, { location_id: locId || null }); setLots((ls) => ls.map((l) => (l.id === lotId ? { ...l, location_id: locId || null } : l))); } catch (err) { setError(err.message); } } // Lagerort für die aktuell ausgewählten Chargen setzen (leer = Ort entfernen). async function applyToSelection() { if (selected.size === 0) return; setError(null); try { await api.bulkLotLocation([...selected], bulkLoc); setBulkLoc(""); clearSelection(); await load(); } catch (err) { setError(err.message); } } const columns = [ { key: "select", header: "", label: "Auswahl", fixed: true, width: 44, render: (l) => ( toggle(l.id)} aria-label={`${l.product_name} auswählen`} /> ) }, { key: "thumb", header: "", label: "Bild", fixed: true, width: 56, render: (l) => }, { key: "product", header: "Artikel", grow: true, min: 180, filterText: (l) => `${l.product_name} ${l.product_brand || ""}`, sortValue: (l) => l.product_name, render: (l) => ( {l.product_name} {l.product_brand && {l.product_brand}} ) }, { key: "art", header: "Art", width: 130, filterText: (l) => (l.tracking === "object" ? "Gegenstand" : "Lebensmittel"), filterValues: (l) => [l.tracking === "object" ? "Gegenstand" : "Lebensmittel"], render: (l) => ( {l.tracking === "object" ? "Gegenstand" : "Lebensmittel"} ) }, { key: "menge", header: "Menge", width: 150, align: "num", sortValue: (l) => l.quantity / artFactor(l), render: mengeText }, { key: "mhd", header: "MHD", width: 150, filterText: (l) => (l.best_before ? formatBestBefore(l.best_before, l.best_before_precision) : ""), sortValue: (l) => l.best_before || "9999-99-99", render: (l) => (l.best_before ? formatBestBefore(l.best_before, l.best_before_precision) : ) }, { key: "location", header: "Lagerort", width: 240, filterText: (l) => (l.location_id ? (locationPathById(l.location_id, locations) || "") : "(ohne)"), filterValues: (l) => [l.location_id ? (locationPathById(l.location_id, locations) || "?") : "(ohne)"], render: (l) => (isAdmin ? ( ) : ( l.location_id ? (locationPathById(l.location_id, locations) || "?") : – ohne – )) }, { key: "created", header: "Eingelagert", width: 130, sortValue: (l) => new Date(l.created_at).getTime(), render: (l) => {new Date(l.created_at).toLocaleDateString("de-DE")} }, { key: "split", header: "Aufteilen", label: "Aufteilen", width: 118, align: "num", render: (l) => (isAdmin ? ( ) : null) }, { key: "details", header: "Artikel", label: "Artikel", width: 90, align: "num", render: (l) => Artikel }, ]; return (

Chargen

{loading ? "Wird geladen…" : `${lots.length} Chargen · ${ohneOrt.length} ohne Lagerort`}
{error &&
{error}
} {isAdmin && (

Lagerort für ausgewählte Chargen setzen

{selected.size > 0 ? `${selected.size} Charge(n) ausgewählt – diese bekommen den gewählten Lagerort (leer = Ort entfernen).` : "Chargen in der Tabelle anhaken, dann hier den Lagerort setzen. Oder unten schnell alle sichtbaren / alle ohne Ort auswählen."}

{selected.size > 0 && ( )}
)}
[expiryRowClass(l.best_before, warnDays), selected.has(l.id) ? "row-active" : ""].filter(Boolean).join(" ")} onRowClick={(l, e) => { // Klicks auf Bedienelemente (Checkbox, Lagerort-Dropdown, Link) nicht // als Zeilenauswahl werten – überall sonst die Zeile umschalten. if (!e.target.closest("input, select, a, button, label")) toggle(l.id); }} getRowKey={(l) => l.id} empty="Keine Chargen im Bestand." />
{splitting && ( (splitting.package_size && splitting.package_size > 0 ? gebinde(q, splitting.package_label) : (splitting.unit_name || unitShort(splitting.base_unit)))} locations={locations} onClose={() => setSplitting(null)} onDone={load} onError={setError} /> )}
); }