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:
24
web/src/components/ShoppingNeedText.jsx
Normal file
24
web/src/components/ShoppingNeedText.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { fmt, gebinde } from "../units";
|
||||
|
||||
/**
|
||||
* Einheitliche Bedarfszeile der Einkaufsliste: die Gebinde-Leitangabe groß
|
||||
* ("3 Gläser"), die Basiseinheit klein daneben ("(500 g)"), dann Bestand/min in
|
||||
* derselben Leitangabe. Die Rechnung (Aufrunden auf ganze Packungen,
|
||||
* Pluralisierung des Leitworts) kommt fertig vom Server als `need`.
|
||||
*/
|
||||
export default function ShoppingNeedText({ need, stock, minStock, unitName }) {
|
||||
// Fallback für den (Übergangs-)Fall ohne `need` vom Server: altes Format.
|
||||
if (!need) {
|
||||
return (
|
||||
<>fehlt <strong>{fmt(stock == null ? 0 : minStock - stock)} {unitName}</strong></>
|
||||
);
|
||||
}
|
||||
const einheit = need.singular ? ` ${gebinde(need.min_stock, need.singular)}` : "";
|
||||
return (
|
||||
<>
|
||||
fehlt <strong>{need.text}</strong>
|
||||
{need.hint ? <> <span className="muted">({need.hint})</span></> : null}{" "}
|
||||
(Bestand {fmt(need.stock)} / min {fmt(need.min_stock)}{einheit})
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "../api";
|
||||
import Icon from "../components/Icon";
|
||||
import { amountText, fmt, unitShort } from "../units";
|
||||
import ShoppingNeedText from "../components/ShoppingNeedText";
|
||||
|
||||
export default function ShoppingList() {
|
||||
const [items, setItems] = useState([]);
|
||||
@@ -51,8 +51,7 @@ export default function ShoppingList() {
|
||||
<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>
|
||||
);
|
||||
@@ -66,8 +65,7 @@ export default function ShoppingList() {
|
||||
<span className="item-name">{it.name}</span>
|
||||
</label>
|
||||
<span className="muted small">
|
||||
fehlt <strong>{amountText(it.deficit, it.package_size, it.base_unit)}</strong>{" "}
|
||||
(Bestand {amountText(it.stock, it.package_size, it.base_unit)})
|
||||
<ShoppingNeedText need={it.need} stock={it.stock} minStock={it.min_stock} unitName={it.base_unit} />
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
@@ -93,8 +91,7 @@ export default function ShoppingList() {
|
||||
<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)})
|
||||
<ShoppingNeedText need={it.need} stock={it.stock} minStock={it.min_stock} unitName={it.unit_name} />
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
@@ -108,8 +105,7 @@ export default function ShoppingList() {
|
||||
<span className="item-name">{it.name}</span>
|
||||
</label>
|
||||
<span className="muted small">
|
||||
fehlt <strong>{fmt(it.deficit)} {it.unit_label}</strong>{" "}
|
||||
(Bestand {fmt(it.stock)} / min {fmt(it.min_stock)})
|
||||
<ShoppingNeedText need={it.need} stock={it.stock} minStock={it.min_stock} unitName={it.unit_label} />
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user