From b9d097fe96814ab11d114b311bfaf49f47211842 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Tue, 20 Oct 2026 21:57:27 +0200 Subject: [PATCH] Verwendungsstatus: Weboberflaeche zum Umstellen und eine Spalte dafuer Auf der Scan-Zielseite /i/ stehen vier Knoepfe - Verfuegbar, In Verwendung, In Reinigung, In Reparatur -, der aktuelle hervorgehoben. Ein Griff genuegt, genau dafuer ist die Seite da. Dazu der Verlauf mit Zeitstempeln, wer wann umgestellt hat. Dieselbe Bedienung in der Einzelstueck-Tabelle eines Artikels, und in der Liste aller Einzelstuecke eine Spalte mit Plakette: gelb in Reinigung, rot in Reparatur. Filterbar nach allen vier Zustaenden, "Verfuegbar" eingeschlossen - "was liegt bereit" ist eine echte Frage. Der Haken je Kategorie steht in der Kategorientabelle und im Anlegen-Formular. Setzt man ihn an einer Oberkategorie, wandert er in die Unterkategorien; das sieht man dort auch stehen. Ohne den Haken erscheint nirgends etwas davon. Co-Authored-By: Claude Opus 5 --- web/src/api.js | 6 ++ web/src/components/Einzelstuecke.jsx | 18 +++- web/src/components/Verwendung.jsx | 142 +++++++++++++++++++++++++++ web/src/pages/Categories.jsx | 36 ++++++- web/src/pages/ItemList.jsx | 13 +++ web/src/pages/ItemResolve.jsx | 16 +++ 6 files changed, 226 insertions(+), 5 deletions(-) create mode 100644 web/src/components/Verwendung.jsx diff --git a/web/src/api.js b/web/src/api.js index a208f53..94da712 100644 --- a/web/src/api.js +++ b/web/src/api.js @@ -236,6 +236,12 @@ export const api = { lendItem: (itemId, body) => request(`/items/${itemId}/loan`, { method: "POST", body }), returnItem: (itemId, body = {}) => request(`/items/${itemId}/return`, { method: "POST", body }), itemLoans: (itemId) => request(`/items/${itemId}/loans`), + // Verwendungsstatus. Leerer Status = verfügbar. Eigener Endpunkt aus + // demselben Grund wie Zubehör (siehe unten) und weil updateItem Admins + // vorbehalten ist – umstellen darf jeder. + setzeVerwendung: (itemId, status) => + request(`/items/${itemId}/verwendung`, { method: "PUT", body: { status } }), + itemVerwendungen: (itemId) => request(`/items/${itemId}/verwendungen`), // Zubehör läuft bewusst nicht über updateItem: dessen Schema sendet immer alle // Felder mit, ein vergessenes Feld würde die Beziehung still kappen. zubehoerZuordnen: (itemId, childItemId) => diff --git a/web/src/components/Einzelstuecke.jsx b/web/src/components/Einzelstuecke.jsx index a069255..59264a2 100644 --- a/web/src/components/Einzelstuecke.jsx +++ b/web/src/components/Einzelstuecke.jsx @@ -15,6 +15,10 @@ import { VerleihText, verleihRowClass, } from "./Verleih"; +import { + VerwendungAktion, + VerwendungHistorie, +} from "./Verwendung"; const reasonLabel = (v) => REASONS.find((r) => r.value === v)?.label || v; @@ -322,7 +326,7 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh )} + ))} + + ); +} + +/** Verwendungs-Verlauf eines Stücks (neueste zuerst), bei Bedarf nachgeladen. */ +export function VerwendungHistorie({ item, onError }) { + const [zeilen, setZeilen] = useState(null); + const [offen, setOffen] = useState(false); + if (!item?.verwendung_moeglich) return null; + + async function laden() { + setOffen(true); + try { setZeilen(await api.itemVerwendungen(item.id)); } + catch (err) { onError?.(err.message); } + } + + if (!offen) { + return ( + + ); + } + if (zeilen == null) return Wird geladen…; + if (zeilen.length === 0) { + return War noch nie in Verwendung.; + } + + return ( +
    + {zeilen.map((v) => ( +
  • + {verwendungLabel(v.status)} + + {zeitpunkt(v.started_at)} + {v.ended_at ? ` – ${zeitpunkt(v.ended_at)}` : " – läuft"} + {v.username ? ` · ${v.username}` : ""} + +
  • + ))} +
+ ); +} diff --git a/web/src/pages/Categories.jsx b/web/src/pages/Categories.jsx index ddc493c..11ad465 100644 --- a/web/src/pages/Categories.jsx +++ b/web/src/pages/Categories.jsx @@ -21,7 +21,9 @@ export default function Categories() { const confirm = useConfirm(); const { isAdmin } = useAuth(); const [categories, setCategories] = useState([]); - const [form, setForm] = useState({ name: "", parent_id: "", tracking: "" }); + const [form, setForm] = useState({ + name: "", parent_id: "", tracking: "", usage_tracking: false, + }); const [error, setError] = useState(null); // Welche Kategorie ihre Seitenkarte zeigt und welche davon: alles in der // URL, damit ein Neuladen dieselbe Ansicht wiederherstellt. Gespeichert @@ -68,8 +70,14 @@ export default function Categories() { name: form.name, parent_id: form.parent_id === "" ? null : Number(form.parent_id), tracking: form.tracking === "" ? null : form.tracking, + // Nur mitschicken, wenn angehakt: sonst erbt eine Unterkategorie die + // Markierung ihrer Oberkategorie (null = erben, siehe Backend). + usage_tracking: form.usage_tracking ? true : null, + }); + setForm({ + name: "", parent_id: form.parent_id, tracking: form.tracking, + usage_tracking: form.usage_tracking, }); - setForm({ name: "", parent_id: form.parent_id, tracking: form.tracking }); load(); } catch (err) { setError(err.message); @@ -137,6 +145,15 @@ export default function Categories() { ) : {MODUS[c.tracking] || c.tracking}) }, + // Ein Haken hier wandert in den ganzen Unterbaum (der Server tut das, + // siehe routers/categories.py) – „Kleidung" anhaken genuegt also. + { key: "verwendung", header: "Verwendungsstatus", width: 170, align: "num", + filterText: (c) => (c.usage_tracking ? "Ja" : "Nein"), + sortValue: (c) => (c.usage_tracking ? 1 : 0), + render: (c) => (isAdmin ? ( + patch(c, { usage_tracking: e.target.checked })} /> + ) : (c.usage_tracking ? : –)) }, { key: "parent", header: "Übergeordnet", width: 220, filterText: (c) => (c.parent_id ? nameById[c.parent_id] || "" : ""), render: (c) => { @@ -229,10 +246,21 @@ export default function Categories() { +

- Unterkategorien erben Art und Felder der Oberkategorie. Auf eine - Oberkategorie gefiltert erscheinen auch die Artikel ihrer Unterkategorien. + Unterkategorien erben Art, Felder und Verwendungsstatus der + Oberkategorie. Auf eine Oberkategorie gefiltert erscheinen auch + die Artikel ihrer Unterkategorien. +

+

+ Verwendungsstatus: Einzelstücke dieser Kategorie führen, woran + sie gerade sind – getragen, in Reinigung, in Reparatur. Gedacht + für Kleidung und Werkzeug, nicht für Vorräte.

)} diff --git a/web/src/pages/ItemList.jsx b/web/src/pages/ItemList.jsx index 2216c70..7f150b0 100644 --- a/web/src/pages/ItemList.jsx +++ b/web/src/pages/ItemList.jsx @@ -14,6 +14,7 @@ import { VerleihText, verleihRowClass, } from "../components/Verleih"; +import { VerwendungBadge, verwendungLabel } from "../components/Verwendung"; /** * Liste aller Einzelstücke (physische Exemplare, jedes mit eigener UID/Lagerort) – @@ -84,6 +85,18 @@ export default function ItemList() { filterOptionLabel: (v) => (v === "" ? "(im Haus)" : v), sortValue: (it) => it.loan?.borrower || "", render: (it) => }, + // Nur bei Kategorien, die ihn fuehren – sonst steht hier ein Strich. + // „Verfuegbar" ist das Fehlen eines Zeitraums, taucht in der Werteauswahl + // aber als eigener Eintrag auf: „was liegt bereit?" ist eine echte Frage. + { key: "verwendung", header: "Verwendung", width: 150, + filterText: (it) => (it.verwendung_moeglich + ? verwendungLabel(it.verwendung?.status || null) : ""), + filterValues: (it) => [it.verwendung_moeglich + ? verwendungLabel(it.verwendung?.status || null) : ""], + filterOptionLabel: (v) => (v === "" ? "(ohne Verwendungsstatus)" : v), + sortValue: (it) => (it.verwendung_moeglich + ? verwendungLabel(it.verwendung?.status || null) : ""), + render: (it) => }, { key: "due", header: "Zurück bis", width: 120, filterText: (it) => it.loan?.due_on || "", sortValue: (it) => it.loan?.due_on || "", diff --git a/web/src/pages/ItemResolve.jsx b/web/src/pages/ItemResolve.jsx index fff5d4f..c054fd0 100644 --- a/web/src/pages/ItemResolve.jsx +++ b/web/src/pages/ItemResolve.jsx @@ -4,6 +4,7 @@ import { api } from "../api"; import Icon from "../components/Icon"; import { ProduktThumb } from "../components/ProduktBild"; import { FristBadge, VerleihForm, VerleihHistorie } from "../components/Verleih"; +import { VerwendungAktion, VerwendungHistorie, verwendungLabel } from "../components/Verwendung"; import { useToast } from "../toast"; import { locationPathById } from "../locationPath"; @@ -64,6 +65,9 @@ export default function ItemResolve() { {uid} {item?.product_brand ? ` · ${item.product_brand}` : ""} {ort ? ` · ${ort}` : ""} + {item?.verwendung_moeglich + ? ` · ${verwendungLabel(item.verwendung?.status || null)}` + : ""} )} @@ -95,6 +99,15 @@ export default function ItemResolve() { + {item.verwendung_moeglich && ( +
+
+ Verwendung +
+ +
+ )} +
Lagerort
@@ -121,6 +134,9 @@ export default function ItemResolve() {
+
+ +
)}