Einkaufsliste: Fehlmenge als Gebinde (Glaeser/Dosen), Basiseinheit klein

Statt 'fehlt 500 Gramm' jetzt die Produktgroesse als Leitangabe:
'fehlt 3 Glaeser (500 g)'. Auf ganze Gebinde aufgerundet, weil man nur
ganze Glaeser/Dosen kauft; Basiseinheit als kleiner Hinweis.

Backend: neues ShoppingNeed-Objekt an jeder Einkaufslisten-Zeile (Produkte
+ Gruppen, gesamt + je Ort) - damit die Angabe auch direkt ueber die API
kommt ('x Glaeser (y g)'), inkl. Pluralisierung aus der Gebinde-Tabelle.
Gruppen leiten das Gebinde ab, wenn alle passenden Produkte dasselbe haben,
sonst bleibt es bei der Basiseinheit. Bestehende Felder unveraendert.

Frontend: gemeinsame Komponente ShoppingNeedText fuer Karte und volle Seite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-08-07 18:03:37 +02:00
parent e214bb76a9
commit f2c657ff31
6 changed files with 294 additions and 29 deletions

View File

@@ -3,9 +3,10 @@ import { Link } from "react-router-dom";
import { api } from "../api";
import Icon from "../components/Icon";
import { CardSkeleton } from "../components/Skeleton";
import ShoppingNeedText from "../components/ShoppingNeedText";
import { useSettings } from "../settings";
import {
amountText, articlePrimary, articleSecondary, fmt, relativeExpiry,
articlePrimary, articleSecondary, fmt, relativeExpiry,
} from "../units";
import Bars from "./charts/Bars";
import Donut from "./charts/Donut";
@@ -332,10 +333,10 @@ function KarteEinkaufsliste({ props: karteProps }) {
const abhaken = (key, val) => setErledigt((e) => ({ ...e, [key]: val }));
// Ein Abschnitt (Gesamt oder ein Ort) als abhakbare Liste wie die volle Seite.
// `prefix` macht die Schlüssel über die Abschnitte hinweg eindeutig, `prodInfo`
// formatiert die Produktzeile (Gesamt trägt Gebinde/Basiseinheit, je-Ort schon
// eine fertige Einheit).
const abschnitt = (gruppen, produkte, prefix, prodInfo) => (
// `prefix` macht die Schlüssel über die Abschnitte hinweg eindeutig. Die
// Bedarfszeile kommt vom Server fertig aufbereitet (`need`) und ist für
// Produkte wie Gruppen, gesamt wie je Ort identisch aufgebaut.
const abschnitt = (gruppen, produkte, prefix) => (
<ul className="checklist">
{gruppen.map((it) => {
const key = `${prefix}g${it.group_id}`;
@@ -347,8 +348,7 @@ function KarteEinkaufsliste({ props: karteProps }) {
<span className="item-name">{it.name}</span>
</label>
<span className="muted small">
fehlt <strong>{fmt(it.deficit)} {it.unit_name}</strong>{" "}
(Bestand {fmt(it.stock)} / min {fmt(it.min_stock)} {it.unit_name})
<ShoppingNeedText need={it.need} stock={it.stock} minStock={it.min_stock} unitName={it.unit_name} />
</span>
</li>
);
@@ -361,27 +361,21 @@ function KarteEinkaufsliste({ props: karteProps }) {
<input type="checkbox" checked={!!erledigt[key]} onChange={(e) => abhaken(key, e.target.checked)} />
<span className="item-name">{it.name}</span>
</label>
<span className="muted small">{prodInfo(it)}</span>
<span className="muted small">
<ShoppingNeedText need={it.need} stock={it.stock} minStock={it.min_stock} unitName={it.unit_label} />
</span>
</li>
);
})}
</ul>
);
const gesamtInfo = (it) => (
<>fehlt <strong>{amountText(it.deficit, it.package_size, it.base_unit)}</strong>{" "}
(Bestand {amountText(it.stock, it.package_size, it.base_unit)})</>
);
const ortInfo = (it) => (
<>fehlt <strong>{fmt(it.deficit)} {it.unit_label}</strong>{" "}
(Bestand {fmt(it.stock)} / min {fmt(it.min_stock)})</>
);
return (
<div className="card-stack">
{!gesamtLeer && (
<div>
<div className="muted small" style={{ fontWeight: 600 }}>Gesamt</div>
{abschnitt(daten.gruppen, daten.produkte, "", gesamtInfo)}
{abschnitt(daten.gruppen, daten.produkte, "")}
</div>
)}
{orte.map((o) => ((o.products.length || o.groups.length) ? (
@@ -389,7 +383,7 @@ function KarteEinkaufsliste({ props: karteProps }) {
<div className="muted small" style={{ fontWeight: 600 }}>
<Icon name="location" size={13} /> {o.location_name}
</div>
{abschnitt(o.groups, o.products, `l${o.location_id}`, ortInfo)}
{abschnitt(o.groups, o.products, `l${o.location_id}`)}
</div>
) : null))}
</div>