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:
Scarriffle
2026-07-22 10:18:30 +02:00
parent 9d4fd030d8
commit b683f2d93b
19 changed files with 1304 additions and 421 deletions

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { api } from "../api";
import { useAuth } from "../auth";
import Icon from "../components/Icon";
import { fmt, unitOptions, unitShort } from "../units";
export default function CheckIn() {
@@ -30,8 +31,7 @@ export default function CheckIn() {
setProduct(p);
setResults([]);
setUnknownBarcode(null);
const opts = unitOptions(p);
setUnit(opts[0].value);
setUnit(unitOptions(p)[0].value);
}
async function doLookup() {
@@ -44,7 +44,6 @@ export default function CheckIn() {
setInfo(`Produkt erkannt: ${res.existing_product.name}`);
} else {
setUnknownBarcode(barcode.trim());
setInfo(null);
}
} catch (err) {
setError(err.message);
@@ -84,36 +83,40 @@ export default function CheckIn() {
return (
<div>
<div className="page-head"><h1>📥 Einlagern</h1></div>
{error && <div className="alert error">{error}</div>}
{info && <div className="alert info">{info}</div>}
<div className="page-head"><h1>Einlagern</h1></div>
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
{info && <div className="alert ok"><Icon name="check" size={16} />{info}</div>}
{!product && (
<div className="grid-2">
<div className="card">
<h2>Per Barcode</h2>
<div className="row">
<input className="grow" placeholder="Barcode" value={barcode}
onChange={(e) => setBarcode(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && doLookup()} />
<div className="card-head"><Icon name="search" /><h2>Per Barcode</h2></div>
<div className="field-inline">
<label className="grow" style={{ margin: 0 }}>
<input placeholder="Barcode eingeben oder scannen" value={barcode}
onChange={(e) => setBarcode(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && doLookup()} autoFocus />
</label>
<button className="btn primary" onClick={doLookup}>Suchen</button>
</div>
{unknownBarcode && (
<div className="alert warn">
Barcode <strong>{unknownBarcode}</strong> ist unbekannt.{" "}
{isAdmin ? (
<Link to={`/products/new`}>Produkt anlegen</Link>
) : (
"Bitte einen Administrator bitten, das Produkt anzulegen."
)}
<div className="alert warn" style={{ marginTop: "var(--sp-3)" }}>
<Icon name="alert" size={16} />
<span>
Barcode <strong>{unknownBarcode}</strong> ist unbekannt.{" "}
{isAdmin
? <Link to="/products/new">Produkt anlegen</Link>
: "Bitte einen Administrator bitten, das Produkt anzulegen."}
</span>
</div>
)}
</div>
<div className="card">
<h2>Aus Produktliste</h2>
<form className="row" onSubmit={doSearch}>
<input className="grow" placeholder="Name suchen…" value={search}
onChange={(e) => setSearch(e.target.value)} />
<div className="card-head"><Icon name="package" /><h2>Aus Produktliste</h2></div>
<form className="field-inline" onSubmit={doSearch}>
<label className="grow" style={{ margin: 0 }}>
<input placeholder="Name suchen…" value={search} onChange={(e) => setSearch(e.target.value)} />
</label>
<button className="btn">Suchen</button>
</form>
<ul className="picklist">
@@ -132,12 +135,12 @@ export default function CheckIn() {
{product && (
<form className="card form-narrow" onSubmit={submit}>
<div className="selected-product">
{product.image_url && <img className="thumb" src={product.image_url} alt="" />}
<div>
<strong>{product.name}</strong>
<div className="muted">
Aktueller Bestand: {fmt(product.stock)} {unitShort(product.base_unit)}
</div>
{product.image_url
? <img className="thumb" src={product.image_url} alt="" />
: <span className="thumb thumb-fallback"><Icon name="box" size={16} /></span>}
<div className="info">
<div className="title">{product.name}</div>
<div className="muted small">Bestand: {fmt(product.stock)} {unitShort(product.base_unit)}</div>
</div>
<button type="button" className="btn ghost" onClick={() => setProduct(null)}>Ändern</button>
</div>
@@ -151,15 +154,13 @@ export default function CheckIn() {
<label className="grow">
Einheit
<select value={unit} onChange={(e) => setUnit(e.target.value)}>
{unitOptions(product).map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
{unitOptions(product).map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
</label>
</div>
<div className="row">
<label className="grow">
MHD (optional)
Mindesthaltbarkeit (optional)
<input type="date" value={bestBefore} onChange={(e) => setBestBefore(e.target.value)} />
</label>
<label className="grow">
@@ -170,7 +171,7 @@ export default function CheckIn() {
</select>
</label>
</div>
<button className="btn primary" disabled={busy}>{busy ? "…" : "Einlagern"}</button>
<button className="btn primary" disabled={busy}><Icon name="checkin" size={16} />{busy ? "…" : "Einlagern"}</button>
</form>
)}
</div>