Files
Vorrania/web/src/pages/Locations.jsx
Scarriffle b683f2d93b 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>
2026-07-22 10:18:30 +02:00

107 lines
3.5 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 { 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>
);
}