// Anzeige-Helfer für Einheiten (müssen zu backend/app/models.py::BaseUnit passen). // ---- Gebinde: Einzahl und Mehrzahl ---- // Der Artikel speichert nur die Einzahl ("Glas"); die Mehrzahl steht in der // Verwaltung (Tabelle package_types) und wird nach dem Login einmal geladen. // // Bewusst eine Modulvariable und kein React-Kontext: Die Formatierer hier sind // gewöhnliche Funktionen, die auch außerhalb von Komponenten aufgerufen werden. // Sie alle in Hooks zu verwandeln, hieße jede Aufrufstelle umzubauen, um eine // Liste durchzureichen, die sich praktisch nie ändert. let GEBINDE_MEHRZAHL = {}; export function setGebindeFormen(liste) { GEBINDE_MEHRZAHL = Object.fromEntries( (liste || []).map((t) => [t.singular, t.plural || t.singular]), ); } /** * Gebinde-Bezeichnung passend zur Menge: "1 Glas", "3 Gläser". * * Unbekannte Bezeichnungen bleiben unverändert – lieber eine fehlende Mehrzahl * als ein erfundenes "Glass". Nicht ganze Mengen zählen als Mehrzahl * ("0,5 Gläser"), so wie im Deutschen alles außer der Eins. */ export function gebinde(menge, einzahl) { const wort = einzahl || "Packung"; if (Math.abs(Number(menge)) === 1) return wort; return GEBINDE_MEHRZAHL[wort] || wort; } export const BASE_UNITS = [ { value: "piece", label: "Stück" }, { value: "gram", label: "Gramm (g)" }, { value: "milliliter", label: "Milliliter (ml)" }, ]; export const BASE_UNIT_SHORT = { piece: "Stk", gram: "g", milliliter: "ml", }; export function unitShort(baseUnit) { return BASE_UNIT_SHORT[baseUnit] || baseUnit; } // Auswahlmöglichkeiten für Menge beim Ein-/Auslagern eines Produkts. export function unitOptions(product) { const opts = [{ value: baseUnitToInput(product.base_unit), label: unitShort(product.base_unit) }]; if (product.package_size) { opts.push({ value: "package", label: `Packung (${product.package_size} ${unitShort(product.base_unit)})` }); } return opts; } // Das Backend akzeptiert 'g', 'ml', 'piece'/'st' etc. als Basiseinheit-Eingabe. export function baseUnitToInput(baseUnit) { if (baseUnit === "gram") return "g"; if (baseUnit === "milliliter") return "ml"; return "piece"; } export function fmt(n) { if (n == null) return "–"; return Number(n).toLocaleString("de-DE", { maximumFractionDigits: 2 }); } // Tage bis zum MHD (negativ = überfällig, null = kein MHD). export function daysUntil(dateStr) { if (!dateStr) return null; const today = new Date(); today.setHours(0, 0, 0, 0); const d = new Date(dateStr); d.setHours(0, 0, 0, 0); return Math.round((d - today) / 86400000); } // Ist ein MHD (YYYY-MM-DD) bereits abgelaufen (vor heute)? export function isExpired(dateStr) { const d = daysUntil(dateStr); return d != null && d < 0; } // Auswählbare Datumsformate (Einstellungen -> Darstellung). export const DATE_FORMATS = [ { value: "de", label: "23.12.2026" }, { value: "de-short", label: "23.12.26" }, { value: "long", label: "23. Dezember 2026" }, { value: "iso", label: "2026-12-23" }, { value: "us", label: "12/23/2026" }, ]; // Formatiert ein ISO-Datum (YYYY-MM-DD) nach dem gewählten Format. export function formatDate(iso, format = "de") { if (!iso) return "–"; const date = new Date(`${iso}T00:00:00`); if (Number.isNaN(date.getTime())) return iso; switch (format) { case "iso": return iso; case "de-short": return date.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "2-digit" }); case "long": return date.toLocaleDateString("de-DE", { day: "numeric", month: "long", year: "numeric" }); case "us": return date.toLocaleDateString("en-US"); default: return date.toLocaleDateString("de-DE"); } } // ---- MHD-Genauigkeit ---- // Intern steht immer ein volles Datum (bei Monatsangaben der Monatsletzte). // Nur die Anzeige und das Eingabefeld richten sich nach der Genauigkeit. /** MHD anzeigen: Monatsangaben ohne Tag, also "09/2026". */ export function formatBestBefore(iso, precision, format = "de") { if (!iso) return "–"; if (precision !== "month") return formatDate(iso, format); const date = new Date(`${iso}T00:00:00`); if (Number.isNaN(date.getTime())) return iso; return `${String(date.getMonth() + 1).padStart(2, "0")}/${date.getFullYear()}`; } /** "2026-09-30" -> "2026-09" für . */ export function toMonthInput(iso) { return iso ? iso.slice(0, 7) : ""; } /** "2026-09" -> "2026-09-01"; das Backend legt daraus den Monatsletzten. */ export function fromMonthInput(value) { return value ? `${value}-01` : ""; } /** "2026-09" -> "2026-09-30". Fuer Ablauf-Warnungen: ein Monats-MHD laeuft * erst am Monatsende ab, nicht am Monatsanfang. */ export function monthInputToLastDay(value) { if (!value) return ""; const [year, month] = value.split("-").map(Number); if (!year || !month) return ""; return new Date(Date.UTC(year, month, 0)).toISOString().slice(0, 10); } // MHD lesbar als Zeitspanne: "in 3 Tagen", "in 1 Woche", "vor 2 Wochen". // Deutsch nach "in"/"vor" steht im Dativ: 1 Tag / 3 Tagen, 1 Woche / 2 Wochen. export function relativeExpiry(daysLeft) { if (daysLeft == null) return ""; if (daysLeft === 0) return "heute"; if (daysLeft === 1) return "morgen"; if (daysLeft === -1) return "gestern"; const abs = Math.abs(daysLeft); let value; let unit; if (abs < 7) { value = abs; unit = value === 1 ? "Tag" : "Tagen"; } else if (abs < 60) { value = Math.round(abs / 7); unit = value === 1 ? "Woche" : "Wochen"; } else { value = Math.round(abs / 30); unit = value === 1 ? "Monat" : "Monaten"; } return daysLeft > 0 ? `in ${value} ${unit}` : `vor ${value} ${unit}`; } // CSS-Zeilenklasse je MHD: rot wenn abgelaufen, gelb wenn innerhalb der Warnfrist. export function expiryRowClass(dateStr, warnDays = 7) { const d = daysUntil(dateStr); if (d == null) return ""; if (d < 0) return "row-danger"; if (d <= warnDays) return "row-warn"; return ""; } // Menge lesbar darstellen: bei Packungsprodukten in Packungen (+ Basiseinheit in Klammern). export function amountText(qty, packageSize, baseUnit) { if (packageSize && packageSize > 0) { return `${fmt(qty / packageSize)} Pkg (${fmt(qty)} ${unitShort(baseUnit)})`; } return `${fmt(qty)} ${unitShort(baseUnit)}`; } // Einheiten-Optionen fürs Ein-/Auslagern: verwaltete Einheiten der Produkt-Art + Packung. export function buildUnitOptions(product, units) { const opts = (units || []) .filter((u) => u.kind === product.kind) .map((u) => ({ value: u.name, label: u.name })); if (product.package_size) { // Bezeichnung des Gebindes je Artikel (Glas, Tüte, …); Wert bleibt "package". opts.push({ value: "package", label: `${product.package_label || "Packung"} (${fmt(product.package_size)} ${unitShort(product.base_unit)})`, }); } return opts; } // Leitangabe einer Menge in Artikeleinheiten (Gebinde), sonst in der Produkteinheit. // Erwartet ein Objekt mit quantity, package_size, package_label, unit_name, unit_factor, base_unit. export function articlePrimary(item) { if (item.package_size > 0) { const anzahl = item.quantity / item.package_size; return `${fmt(anzahl)} ${gebinde(anzahl, item.package_label)}`; } return `${fmt(item.quantity / (item.unit_factor || 1))} ${item.unit_name || unitShort(item.base_unit)}`; } // Untermenge in der Basiseinheit – nur wenn sie sich von der Leitangabe unterscheidet. export function articleSecondary(item) { if (item.package_size > 0 || (item.unit_factor || 1) !== 1) { return `${fmt(item.quantity)} ${unitShort(item.base_unit)}`; } return ""; } // Bestand eines Produkts in seiner Anzeigeeinheit, z.B. "1,5 Kilogramm". export function stockLabel(product) { const factor = product.unit_factor || 1; return `${fmt((product.stock || 0) / factor)} ${product.unit_name || unitShort(product.base_unit)}`; }