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,9 +1,11 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "../api";
|
||||
import Icon from "../components/Icon";
|
||||
|
||||
export default function Locations() {
|
||||
const [locations, setLocations] = useState([]);
|
||||
const [name, setName] = useState("");
|
||||
const [parentId, setParentId] = useState("");
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
async function load() {
|
||||
@@ -20,8 +22,9 @@ export default function Locations() {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
try {
|
||||
await api.createLocation({ name });
|
||||
await api.createLocation({ name, parent_id: parentId === "" ? null : Number(parentId) });
|
||||
setName("");
|
||||
setParentId("");
|
||||
load();
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
@@ -29,7 +32,7 @@ export default function Locations() {
|
||||
}
|
||||
|
||||
async function remove(id) {
|
||||
if (!confirm("Lagerort löschen?")) return;
|
||||
if (!confirm("Lagerort löschen? Untergeordnete Orte werden dann übergeordnet.")) return;
|
||||
try {
|
||||
await api.deleteLocation(id);
|
||||
load();
|
||||
@@ -38,29 +41,66 @@ export default function Locations() {
|
||||
}
|
||||
}
|
||||
|
||||
const nameById = Object.fromEntries(locations.map((l) => [l.id, l.name]));
|
||||
const roots = locations.filter((l) => !l.parent_id || !nameById[l.parent_id]);
|
||||
const childrenOf = (pid) => locations.filter((l) => l.parent_id === pid);
|
||||
const ordered = [];
|
||||
for (const r of roots) {
|
||||
ordered.push({ ...r, depth: 0 });
|
||||
for (const c of childrenOf(r.id)) ordered.push({ ...c, depth: 1 });
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="page-head"><h1>Lagerorte</h1></div>
|
||||
{error && <div className="alert error">{error}</div>}
|
||||
<div className="page-head">
|
||||
<div>
|
||||
<h1>Lagerorte</h1>
|
||||
<div className="sub">Orte lassen sich verschachteln (z.B. Keller → Regal 2 → Fach A)</div>
|
||||
</div>
|
||||
</div>
|
||||
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
|
||||
|
||||
<div className="card form-narrow">
|
||||
<form className="row" onSubmit={add}>
|
||||
<input className="grow" placeholder="Neuer Lagerort (z.B. Speisekammer)" value={name}
|
||||
onChange={(e) => setName(e.target.value)} required />
|
||||
<button className="btn primary">Hinzufügen</button>
|
||||
<form onSubmit={add}>
|
||||
<div className="row">
|
||||
<label className="grow">
|
||||
Name
|
||||
<input placeholder="z.B. Speisekammer oder Regal 2" value={name}
|
||||
onChange={(e) => setName(e.target.value)} required />
|
||||
</label>
|
||||
<label className="grow">
|
||||
Übergeordneter Lagerort (optional)
|
||||
<select value={parentId} onChange={(e) => setParentId(e.target.value)}>
|
||||
<option value="">– keiner (oberste Ebene) –</option>
|
||||
{locations.map((l) => (
|
||||
<option key={l.id} value={l.id}>
|
||||
{l.parent_id && nameById[l.parent_id] ? `${nameById[l.parent_id]} → ` : ""}{l.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<button className="btn primary"><Icon name="plus" size={16} />Hinzufügen</button>
|
||||
</form>
|
||||
|
||||
<ul className="simple-list">
|
||||
{locations.map((l) => (
|
||||
{ordered.map((l) => (
|
||||
<li key={l.id}>
|
||||
<span>{l.name}</span>
|
||||
<button className="btn ghost danger" onClick={() => remove(l.id)}>Löschen</button>
|
||||
<span className={l.depth ? "tree-child" : ""} style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<Icon name="location" size={15} className="muted" />
|
||||
{l.name}
|
||||
{l.depth > 0 && l.parent_id && nameById[l.parent_id] && (
|
||||
<span className="badge">in {nameById[l.parent_id]}</span>
|
||||
)}
|
||||
</span>
|
||||
<button className="btn-icon danger" onClick={() => remove(l.id)} title="Löschen">
|
||||
<Icon name="trash" size={16} />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{locations.length === 0 && <li className="muted">Noch keine Lagerorte.</li>}
|
||||
{locations.length === 0 && <li className="empty">Noch keine Lagerorte.</li>}
|
||||
</ul>
|
||||
</div>
|
||||
<p className="muted small">
|
||||
Unterlagerorte (Regal / Fach) folgen in einem späteren Ausbauschritt.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user