iOS-App angefangen; Import-Modi; MHD-Formatierung; Gebinde in Ablauf-Ansichten

iOS (neu, ios/):
- SwiftUI-App: Login (Server + Keychain-Token), Kamera-Scanner (EAN/UPC/Code128),
  Einlagern mit mehreren Chargen und eigenem MHD, Auslagern mit Chargenauswahl
  oder FEFO, Artikelsuche, Anlegen mit Open-Food-Facts-Vorbefuellung.
- Home-Screen-Shortcuts: Schnellaktionen (langer Druck) und URL-Schema
  projectgood://checkin | ://checkout fuer eigene Symbole via Kurzbefehle.
- project.yml (XcodeGen) + README mit Build-Anleitung. NICHT kompiliert - auf
  diesem Rechner ist kein Xcode vorhanden.

Import-Modi (wie besprochen sinnvoll):
- add (Standard, nichts loeschen), replace_listed (Bestaende der in der Datei
  genannten Produkte ersetzen - fuer Inventur), replace_all (alles ersetzen).
  Geleerte Bestaende werden als Korrektur-Bewegung protokolliert; die Oberflaeche
  fragt bei den zerstoerenden Modi nach.

Anzeige:
- "Bald ablaufend"/"Abgelaufen" zeigen jetzt das Gebinde ("1 Glas") mit der
  Basiseinheit klein darunter (ExpiringItem liefert die Einheiten mit).
- MHD als Zeitspanne mit korrektem Numerus: "in 3 Tagen", "in 1 Woche",
  "vor 2 Wochen", dazu heute/morgen/gestern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-22 14:59:04 +02:00
parent de6c8632a1
commit 4bb9a10366
23 changed files with 1770 additions and 41 deletions

View File

@@ -53,6 +53,30 @@ export function isExpired(dateStr) {
return d != null && d < 0;
}
// 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);
@@ -85,6 +109,23 @@ export function buildUnitOptions(product, units) {
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) {
return `${fmt(item.quantity / item.package_size)} ${item.package_label || "Packung"}`;
}
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;