Files
Vorrania/web/src/pages/Locations.jsx
Scarriffle d448754d2a Web: Lagerort-Inhalt ansehen + von dort einlagern
Der Lagerort-QR (/l/<code>) zeigt jetzt, was am Ort (inkl. Unterorte) liegt -
Artikel mit Bestand, jeweils verlinkt - statt nur den Namen. "Hier einlagern"
springt ins Einlagern mit vorbelegtem Lagerort (?location=). In der Lagerorte-
Verwaltung fuehrt ein neues Lupensymbol zur Inhaltsansicht.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 13:37:34 +02:00

212 lines
7.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 { Link } from "react-router-dom";
import { api } from "../api";
import { useConfirm } from "../confirm";
import Icon from "../components/Icon";
import CategorySelect from "../components/CategorySelect";
import { QrImg, printQrLabels } from "../qr";
export default function Locations() {
const confirm = useConfirm();
const [locations, setLocations] = useState([]);
const [name, setName] = useState("");
const [parentId, setParentId] = useState("");
const [error, setError] = useState(null);
// Inline-Bearbeiten (Umbenennen + Umhängen) eines vorhandenen Orts.
const [editId, setEditId] = useState(null);
const [editName, setEditName] = useState("");
const [editParent, setEditParent] = 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 : parentId });
setName("");
// Übergeordneten Ort absichtlich stehen lassen beim Anlegen vieler Orte
// unter demselben Elternteil spart das jedes Mal die Neuauswahl.
load();
} catch (err) {
setError(err.message);
}
}
function startEdit(l) {
setEditId(l.id);
setEditName(l.name);
setEditParent(l.parent_id ?? null);
setError(null);
}
function cancelEdit() {
setEditId(null);
}
async function saveEdit() {
setError(null);
try {
await api.updateLocation(editId, {
name: editName.trim(),
parent_id: editParent == null ? null : editParent,
});
setEditId(null);
load();
} catch (err) {
setError(err.message);
}
}
// Eigene Unterorte (rekursiv) als Ziel beim Umhängen ausgeschlossen.
function descendantIds(id) {
const result = new Set();
const stack = [id];
while (stack.length) {
const cur = stack.pop();
for (const c of locations.filter((l) => l.parent_id === cur)) {
if (!result.has(c.id)) { result.add(c.id); stack.push(c.id); }
}
}
return result;
}
async function remove(id) {
const ok = await confirm({
title: "Lagerort löschen?",
message: "Untergeordnete Orte rücken eine Ebene nach oben.",
confirmLabel: "Löschen",
danger: true,
});
if (!ok) return;
try {
await api.deleteLocation(id);
load();
} catch (err) {
setError(err.message);
}
}
const nameById = Object.fromEntries(locations.map((l) => [l.id, l.name]));
const ids = new Set(locations.map((l) => l.id));
const isRoot = (l) => !l.parent_id || !ids.has(l.parent_id);
// Rekursiv über beliebig viele Ebenen (Schrank -> Fach -> Kiste -> …).
const ordered = [];
const seen = new Set();
function walk(node, depth) {
if (seen.has(node.id)) return; // Schutz vor Zyklen
seen.add(node.id);
ordered.push({ ...node, depth });
for (const c of locations.filter((l) => l.parent_id === node.id)) walk(c, depth + 1);
}
for (const r of locations.filter(isRoot)) walk(r, 0);
// Mögliche neue Elternorte beim Bearbeiten: alle außer dem Ort selbst und
// seinen Unterorten (sonst entstünde ein Ring).
const editDesc = editId != null ? descendantIds(editId) : new Set();
const editParentNodes = ordered.filter((o) => o.id !== editId && !editDesc.has(o.id));
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>
{locations.length > 0 && (
<button className="btn" onClick={() => printQrLabels("Lagerort-Etiketten",
locations.map((l) => ({ text: `${window.location.origin}/l/${l.id}`, line1: l.name, line2: "Lagerort" })))}>
<Icon name="download" size={16} />QR-Etiketten drucken
</button>
)}
</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)
<CategorySelect
value={parentId === "" ? null : parentId}
nodes={ordered}
rootLabel=" keiner (oberste Ebene) "
onChange={(id) => setParentId(id == null ? "" : String(id))}
/>
</label>
</div>
<button className="btn primary"><Icon name="plus" size={16} />Hinzufügen</button>
</form>
<ul className="simple-list">
{ordered.map((l) => (
editId === l.id ? (
<li key={l.id}>
<div className="row" style={{ width: "100%", gap: 8, alignItems: "flex-end" }}>
<label className="grow" style={{ margin: 0 }}>
Name
<input value={editName} onChange={(e) => setEditName(e.target.value)} />
</label>
<label className="grow" style={{ margin: 0 }}>
Übergeordnet
<CategorySelect
value={editParent}
nodes={editParentNodes}
rootLabel=" keiner (oberste Ebene) "
onChange={(id) => setEditParent(id == null ? null : id)}
/>
</label>
<span style={{ display: "flex", gap: 6 }}>
<button className="btn primary" onClick={saveEdit} disabled={!editName.trim()}>
<Icon name="check" size={16} />Speichern
</button>
<button type="button" className="btn ghost" onClick={cancelEdit}>Abbrechen</button>
</span>
</div>
</li>
) : (
<li key={l.id}>
<span style={{ display: "flex", alignItems: "center", gap: 8, paddingLeft: l.depth * 22 }}>
{l.depth > 0 && <span className="muted"></span>}
<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>
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
<Link className="btn-icon" to={`/l/${l.id}`} title="Inhalt ansehen">
<Icon name="search" size={16} />
</Link>
<QrImg text={`${window.location.origin}/l/${l.id}`} size={44} />
<button className="btn-icon" onClick={() => startEdit(l)} title="Bearbeiten">
<Icon name="edit" size={16} />
</button>
<button className="btn-icon danger" onClick={() => remove(l.id)} title="Löschen">
<Icon name="trash" size={16} />
</button>
</span>
</li>
)
))}
{locations.length === 0 && <li className="empty">Noch keine Lagerorte.</li>}
</ul>
</div>
</div>
);
}