Web-UI: professionelles Redesign (Icons statt Emojis) + neue Features
Design: - Neues Icon-Set (Inline-SVG, Feather-Stil), keine Emojis mehr - Kohaerentes Design-System (Sidebar-Layout, Palette, Spacing, Light/Dark) - Alle Seiten ueberarbeitet (Login, Dashboard, Produkte, Formular, Ein-/Auslagern, Lagerorte, Benutzer, Einkaufsliste) Neue Features: - Gruppen-Verwaltung (anlegen/bearbeiten/loeschen, Produktzahl + Bestand, Gruppen-Mindestbestand) inkl. Einkaufsliste - Einfache Gruppen-Auto-Zuordnung aus OFF-Kategorie im Produktformular - Verlauf-Seite (Bewegungen: wer/was/wann) via neuem /movements-Endpoint - Einstellungen-Seite (Ablauf-Warnfrist) - Lagerorte mit optional uebergeordnetem Ort (verschachtelbar) Backend: - groups: PATCH + Anreicherung (product_count, stock) - views: /shopping-list/groups, /movements - schemas: GroupUpdate, GroupShoppingItem, MovementOut Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,19 +1,26 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { api } from "../api";
|
||||
import Icon from "../components/Icon";
|
||||
import { 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] = await Promise.all([api.expiring(), api.shoppingList()]);
|
||||
const [e, s, g] = await Promise.all([
|
||||
api.expiring(),
|
||||
api.shoppingList(),
|
||||
api.groupShoppingList(),
|
||||
]);
|
||||
setExpiring(e);
|
||||
setShopping(s);
|
||||
setGroupShopping(g);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
@@ -21,61 +28,100 @@ export default function Dashboard() {
|
||||
load();
|
||||
}, []);
|
||||
|
||||
const overdue = expiring.filter((e) => e.days_left < 0).length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="page-head">
|
||||
<h1>Übersicht</h1>
|
||||
<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">Einlagern</Link>
|
||||
<Link className="btn" to="/checkout">Auslagern</Link>
|
||||
<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>}
|
||||
|
||||
<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>
|
||||
{error && <div className="alert error">{error}</div>}
|
||||
|
||||
<div className="grid-2">
|
||||
<section className="card">
|
||||
<h2>⏰ Bald ablaufend</h2>
|
||||
<div className="card-head"><Icon name="clock" />
|
||||
<h2>Bald ablaufend</h2>
|
||||
</div>
|
||||
{expiring.length === 0 ? (
|
||||
<p className="muted">Nichts läuft demnächst ab. 🎉</p>
|
||||
<div className="empty">Nichts läuft demnächst ab.</div>
|
||||
) : (
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr><th>Produkt</th><th>Menge</th><th>MHD</th><th>Tage</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{expiring.map((it) => (
|
||||
<tr key={it.lot_id} className={it.days_left < 0 ? "row-danger" : it.days_left <= 2 ? "row-warn" : ""}>
|
||||
<td>{it.product_name}</td>
|
||||
<td>{fmt(it.quantity)} {unitShort(it.base_unit)}</td>
|
||||
<td>{it.best_before}</td>
|
||||
<td>{it.days_left < 0 ? `${-it.days_left} überf.` : it.days_left}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<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" : it.days_left <= 2 ? "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">
|
||||
<h2>🛒 Einkaufsliste</h2>
|
||||
{shopping.length === 0 ? (
|
||||
<p className="muted">Alle Mindestbestände erreicht. 👍</p>
|
||||
<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>
|
||||
) : (
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr><th>Produkt</th><th>Bestand</th><th>Mindest</th><th>Fehlt</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{shopping.map((it) => (
|
||||
<tr key={it.product_id}>
|
||||
<td>{it.name}</td>
|
||||
<td>{fmt(it.stock)} {unitShort(it.base_unit)}</td>
|
||||
<td>{fmt(it.min_stock)}</td>
|
||||
<td className="strong">{fmt(it.deficit)} {unitShort(it.base_unit)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<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)}</td>
|
||||
<td className="num strong">{fmt(it.deficit)}</td>
|
||||
</tr>
|
||||
))}
|
||||
{shopping.map((it) => (
|
||||
<tr key={`p${it.product_id}`}>
|
||||
<td>{it.name}</td>
|
||||
<td className="num">{fmt(it.stock)} {unitShort(it.base_unit)}</td>
|
||||
<td className="num strong">{fmt(it.deficit)} {unitShort(it.base_unit)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user