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>
107 lines
3.5 KiB
JavaScript
107 lines
3.5 KiB
JavaScript
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() {
|
||
try {
|
||
setLocations(await api.listLocations());
|
||
} catch (err) {
|
||
setError(err.message);
|
||
}
|
||
}
|
||
|
||
useEffect(() => { load(); }, []);
|
||
|
||
async function add(e) {
|
||
e.preventDefault();
|
||
setError(null);
|
||
try {
|
||
await api.createLocation({ name, parent_id: parentId === "" ? null : Number(parentId) });
|
||
setName("");
|
||
setParentId("");
|
||
load();
|
||
} catch (err) {
|
||
setError(err.message);
|
||
}
|
||
}
|
||
|
||
async function remove(id) {
|
||
if (!confirm("Lagerort löschen? Untergeordnete Orte werden dann übergeordnet.")) return;
|
||
try {
|
||
await api.deleteLocation(id);
|
||
load();
|
||
} catch (err) {
|
||
setError(err.message);
|
||
}
|
||
}
|
||
|
||
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">
|
||
<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 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">
|
||
{ordered.map((l) => (
|
||
<li key={l.id}>
|
||
<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="empty">Noch keine Lagerorte.</li>}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|