Ablauf-Warnungen + Packungen-Spalte + Farbcodierung
- Backend: ProductOut.expired_count (Anzahl abgelaufener Chargen je Produkt). - Einlagern: Inline-Warnung pro Charge, wenn MHD in der Vergangenheit liegt. - Produkte-Liste: Badge "N abgelaufen"; neue Spalte "Packungen" (Bestand/Packungsgroesse, bei Stueck-Produkten = Bestand). - Produktdetail: abgelaufene Chargen rot, Warnfrist gelb (Warnfrist aus Settings), Warnbanner bei abgelaufenem Bestand. - Uebersicht: alle Ablaufzeilen gelb, abgelaufene rot; deutlicher Warnbanner. - units.js: daysUntil/isExpired/expiryRowClass/amountText. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { api } from "../api";
|
||||
import { useAuth } from "../auth";
|
||||
import Icon from "../components/Icon";
|
||||
import { guessGroup } from "../offUtils";
|
||||
import { BASE_UNITS, fmt, unitShort } from "../units";
|
||||
import { BASE_UNITS, daysUntil, expiryRowClass, fmt, isExpired, unitShort } from "../units";
|
||||
|
||||
const EMPTY = {
|
||||
barcode: "", name: "", brand: "", image_url: "",
|
||||
@@ -27,6 +27,7 @@ export default function ProductForm() {
|
||||
const [busy, setBusy] = useState(false);
|
||||
// Einheit, in der der Mindestbestand eingegeben wird: "base" oder "package".
|
||||
const [minUnit, setMinUnit] = useState("base");
|
||||
const [warnDays, setWarnDays] = useState(7);
|
||||
|
||||
function set(k, v) {
|
||||
setForm((f) => ({ ...f, [k]: v }));
|
||||
@@ -86,6 +87,11 @@ export default function ProductForm() {
|
||||
try {
|
||||
const gs = await api.listGroups();
|
||||
setGroups(gs);
|
||||
try {
|
||||
const s = await api.listSettings();
|
||||
const row = s.find((x) => x.key === "expiry_warning_days");
|
||||
if (row) setWarnDays(parseInt(row.value, 10) || 7);
|
||||
} catch { /* Einstellungen optional */ }
|
||||
if (!isNew) {
|
||||
const p = await api.getProduct(id);
|
||||
setProduct(p);
|
||||
@@ -259,18 +265,31 @@ export default function ProductForm() {
|
||||
{!isNew && (
|
||||
<section className="card">
|
||||
<div className="card-head"><Icon name="box" /><h2>Chargen im Bestand</h2></div>
|
||||
{lots.some((l) => isExpired(l.best_before)) && (
|
||||
<div className="alert error"><Icon name="alert" size={16} />
|
||||
Dieses Produkt hat abgelaufene Chargen im Bestand.
|
||||
</div>
|
||||
)}
|
||||
{form.image_url && <img className="product-img" src={form.image_url} alt="" />}
|
||||
<div className="table-wrap">
|
||||
<table className="table">
|
||||
<thead><tr><th className="num">Menge</th><th>MHD</th><th>Eingelagert</th></tr></thead>
|
||||
<tbody>
|
||||
{lots.map((l) => (
|
||||
<tr key={l.id}>
|
||||
<td className="num">{fmt(l.quantity)} {unitLabel}</td>
|
||||
<td>{l.best_before || "–"}</td>
|
||||
<td className="muted">{new Date(l.created_at).toLocaleDateString("de-DE")}</td>
|
||||
</tr>
|
||||
))}
|
||||
{lots.map((l) => {
|
||||
const cls = expiryRowClass(l.best_before, warnDays);
|
||||
const dLeft = daysUntil(l.best_before);
|
||||
return (
|
||||
<tr key={l.id} className={cls}>
|
||||
<td className="num">{fmt(l.quantity)} {unitLabel}</td>
|
||||
<td>
|
||||
{l.best_before || "–"}
|
||||
{cls === "row-danger" && <span className="badge danger" style={{ marginLeft: 6 }}>abgelaufen</span>}
|
||||
{cls === "row-warn" && <span className="badge warn" style={{ marginLeft: 6 }}>{dLeft}d</span>}
|
||||
</td>
|
||||
<td className="muted">{new Date(l.created_at).toLocaleDateString("de-DE")}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{lots.length === 0 && <tr><td colSpan={3} className="empty">Keine Chargen im Bestand.</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user