Files
Vorrania/web/src/pages/Dashboard.jsx
Scarriffle 0310cdfbd6 Verwaltbare Einheiten mit Umrechnung + Chargen bearbeiten + gezieltes Auslagern
Einheiten (neu):
- Tabelle units (name, kind=count|weight|volume, factor, is_builtin); eingebaut
  Stueck/Gramm/Kilogramm/Milliliter/Liter, Admin kann eigene anlegen (z.B. Pfund=500g).
- Bestaende bleiben intern in kanonischer Basis (Stueck/Gramm/Milliliter);
  Product.display_unit_id und Group.min_stock_unit_id als nullable FKs.
- Neuer Umrechnungs-Service (services/conversion.py) ersetzt die feste Einheitenlogik;
  Ein-/Auslagern und Gruppen-Mindestbestand rechnen ueber den Faktor.
- Gruppen-Mindestbestand mit Einheit; Gruppenbestand summiert nur Produkte
  passender Art. Neue Verwaltungsseite "Einheiten" (Admin).
- Schonende Migration beim Start: ADD COLUMN IF NOT EXISTS (Postgres), damit
  bestehende Installationen ihre Daten behalten.

Chargen:
- PATCH /lots/{id} und DELETE /lots/{id}: Menge/MHD korrigieren, Charge loeschen
  (wird als Korrektur-Bewegung protokolliert). Bearbeitung im Produktdetail.

Auslagern:
- Optionales lot_id: gezielt aus einer bestimmten Charge/MHD abbuchen statt FEFO;
  Auswahl-Dropdown in der Auslagern-Seite.

Sonstiges: Roadmap aktualisiert, Tests fuer die Umrechnung.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 12:33:14 +02:00

137 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { api } from "../api";
import Icon from "../components/Icon";
import { amountText, fmt, unitShort } from "../units";
export default function Dashboard() {
const [expiring, setExpiring] = useState([]);
const [shopping, setShopping] = useState([]);
const [groupShopping, setGroupShopping] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
async function load() {
try {
const [e, s, g] = await Promise.all([
api.expiring(),
api.shoppingList(),
api.groupShoppingList(),
]);
setExpiring(e);
setShopping(s);
setGroupShopping(g);
} catch (err) {
setError(err.message);
}
}
load();
}, []);
const overdue = expiring.filter((e) => e.days_left < 0).length;
return (
<div>
<div className="page-head">
<div>
<h1>Übersicht</h1>
<div className="sub">Bestand, Ablauf und Bedarf auf einen Blick</div>
</div>
<div className="actions">
<Link className="btn primary" to="/checkin"><Icon name="checkin" size={16} />Einlagern</Link>
<Link className="btn" to="/checkout"><Icon name="checkout" size={16} />Auslagern</Link>
</div>
</div>
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
{overdue > 0 && (
<div className="alert error"><Icon name="alert" size={16} />
{overdue} Charge(n) im Lager sind bereits abgelaufen bitte aussortieren.
</div>
)}
<div className="stat-row">
<div className="stat">
<div className="stat-label"><Icon name="clock" size={14} />Bald ablaufend</div>
<div className="stat-value">{expiring.length}</div>
</div>
<div className="stat">
<div className="stat-label"><Icon name="alert" size={14} />Überfällig</div>
<div className="stat-value">{overdue}</div>
</div>
<div className="stat">
<div className="stat-label"><Icon name="cart" size={14} />Nachzukaufen</div>
<div className="stat-value">{shopping.length + groupShopping.length}</div>
</div>
</div>
<div className="grid-2">
<section className="card">
<div className="card-head"><Icon name="clock" />
<h2>Bald ablaufend</h2>
</div>
{expiring.length === 0 ? (
<div className="empty">Nichts läuft demnächst ab.</div>
) : (
<div className="table-wrap">
<table className="table">
<thead>
<tr><th>Produkt</th><th className="num">Menge</th><th>MHD</th><th className="num">Tage</th></tr>
</thead>
<tbody>
{expiring.map((it) => (
<tr key={it.lot_id} className={it.days_left < 0 ? "row-danger" : "row-warn"}>
<td>{it.product_name}</td>
<td className="num">{fmt(it.quantity)} {unitShort(it.base_unit)}</td>
<td>{it.best_before}</td>
<td className="num">
{it.days_left < 0
? <span className="badge danger">{-it.days_left}d überf.</span>
: it.days_left}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
<section className="card">
<div className="card-head"><Icon name="cart" />
<h2>Einkaufsliste</h2>
</div>
{shopping.length === 0 && groupShopping.length === 0 ? (
<div className="empty">Alle Mindestbestände erreicht.</div>
) : (
<div className="table-wrap">
<table className="table">
<thead>
<tr><th>Bedarf</th><th className="num">Bestand</th><th className="num">Fehlt</th></tr>
</thead>
<tbody>
{groupShopping.map((it) => (
<tr key={`g${it.group_id}`}>
<td><span className="badge accent">Gruppe</span> {it.name}</td>
<td className="num">{fmt(it.stock)} {it.unit_name}</td>
<td className="num strong">{fmt(it.deficit)} {it.unit_name}</td>
</tr>
))}
{shopping.map((it) => (
<tr key={`p${it.product_id}`}>
<td>{it.name}</td>
<td className="num">{amountText(it.stock, it.package_size, it.base_unit)}</td>
<td className="num strong">{amountText(it.deficit, it.package_size, it.base_unit)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
</div>
</div>
);
}