Verwendungsstatus: Weboberflaeche zum Umstellen und eine Spalte dafuer
Auf der Scan-Zielseite /i/<uid> 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
98e2d18142
commit
b9d097fe96
@@ -236,6 +236,12 @@ export const api = {
|
|||||||
lendItem: (itemId, body) => request(`/items/${itemId}/loan`, { method: "POST", body }),
|
lendItem: (itemId, body) => request(`/items/${itemId}/loan`, { method: "POST", body }),
|
||||||
returnItem: (itemId, body = {}) => request(`/items/${itemId}/return`, { method: "POST", body }),
|
returnItem: (itemId, body = {}) => request(`/items/${itemId}/return`, { method: "POST", body }),
|
||||||
itemLoans: (itemId) => request(`/items/${itemId}/loans`),
|
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
|
// Zubehör läuft bewusst nicht über updateItem: dessen Schema sendet immer alle
|
||||||
// Felder mit, ein vergessenes Feld würde die Beziehung still kappen.
|
// Felder mit, ein vergessenes Feld würde die Beziehung still kappen.
|
||||||
zubehoerZuordnen: (itemId, childItemId) =>
|
zubehoerZuordnen: (itemId, childItemId) =>
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ import {
|
|||||||
VerleihText,
|
VerleihText,
|
||||||
verleihRowClass,
|
verleihRowClass,
|
||||||
} from "./Verleih";
|
} from "./Verleih";
|
||||||
|
import {
|
||||||
|
VerwendungAktion,
|
||||||
|
VerwendungHistorie,
|
||||||
|
} from "./Verwendung";
|
||||||
|
|
||||||
const reasonLabel = (v) => REASONS.find((r) => r.value === v)?.label || v;
|
const reasonLabel = (v) => REASONS.find((r) => r.value === v)?.label || v;
|
||||||
|
|
||||||
@@ -322,7 +326,7 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<button type="button" className="btn-icon"
|
<button type="button" className="btn-icon"
|
||||||
title={offen.has(it.id) ? "Details zuklappen" : "Kaufdaten, Preis, Belege, Verleih-Verlauf"}
|
title={offen.has(it.id) ? "Details zuklappen" : "Kaufdaten, Preis, Belege, Verleih, Verwendung"}
|
||||||
aria-expanded={offen.has(it.id)}
|
aria-expanded={offen.has(it.id)}
|
||||||
onClick={() => klappe(it.id)}>
|
onClick={() => klappe(it.id)}>
|
||||||
<Icon name="chevron" size={18} className={offen.has(it.id) ? "" : "rot-90"} />
|
<Icon name="chevron" size={18} className={offen.has(it.id) ? "" : "rot-90"} />
|
||||||
@@ -363,6 +367,18 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
|||||||
<VerleihHistorie item={it} onError={onError} />
|
<VerleihHistorie item={it} onError={onError} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{it.verwendung_moeglich && (
|
||||||
|
<div>
|
||||||
|
<span className="muted small">Verwendung</span>
|
||||||
|
<div style={{ marginTop: "var(--sp-2)" }}>
|
||||||
|
<VerwendungAktion item={it} onChanged={nachAktion}
|
||||||
|
onError={onError} />
|
||||||
|
<div style={{ marginTop: "var(--sp-2)" }}>
|
||||||
|
<VerwendungHistorie item={it} onError={onError} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Zubehoer item={it} isAdmin={isAdmin}
|
<Zubehoer item={it} isAdmin={isAdmin}
|
||||||
onChanged={nachAktion} onError={onError} />
|
onChanged={nachAktion} onError={onError} />
|
||||||
<label style={{ margin: 0 }}>Kaufpreis
|
<label style={{ margin: 0 }}>Kaufpreis
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { api } from "../api";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
import { useToast } from "../toast";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwendungsstatus eines Einzelstücks – Anzeige und Umstellen, an drei Stellen
|
||||||
|
* benutzt: in der Einzelstück-Tabelle eines Artikels, in der Liste aller
|
||||||
|
* Einzelstücke und auf der Scan-Zielseite (/i/<uid>).
|
||||||
|
*
|
||||||
|
* Woran ist das Stück gerade? Der Lagerort sagt nur, wohin es gehört. Gedacht
|
||||||
|
* für eine Garderobe mit eingenähten NFC-Tags, aber bewusst allgemein: ein Auto
|
||||||
|
* in der Waschanlage ist genauso „in Reinigung".
|
||||||
|
*
|
||||||
|
* Erscheint nur bei Kategorien, die den Status führen (`verwendung_moeglich`).
|
||||||
|
* „Verfügbar" ist kein gespeicherter Wert, sondern das Fehlen eines laufenden
|
||||||
|
* Zeitraums – deshalb ist der Status hier überall `null`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Die vier Zustände in der Reihenfolge, in der sie als Knöpfe erscheinen. */
|
||||||
|
export const ZUSTAENDE = [
|
||||||
|
{ status: null, label: "Verfügbar" },
|
||||||
|
{ status: "in_use", label: "In Verwendung" },
|
||||||
|
{ status: "cleaning", label: "In Reinigung" },
|
||||||
|
{ status: "repair", label: "In Reparatur" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const LABEL = { in_use: "In Verwendung", cleaning: "In Reinigung", repair: "In Reparatur" };
|
||||||
|
// Rot = da muss jemand ran, Gelb = ist unterwegs, schlicht = in Benutzung.
|
||||||
|
const ART = { cleaning: "warn", repair: "danger" };
|
||||||
|
|
||||||
|
export function verwendungLabel(status) {
|
||||||
|
return LABEL[status] || "Verfügbar";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Zeitpunkt kurz: „14.03.2026, 18:42". Anders als bei Daten steht hier ein
|
||||||
|
* vollständiger Zeitstempel – ein Hemd wechselt mehrmals am Tag. */
|
||||||
|
function zeitpunkt(iso) {
|
||||||
|
if (!iso) return "";
|
||||||
|
const d = new Date(iso);
|
||||||
|
if (Number.isNaN(d.getTime())) return iso;
|
||||||
|
return d.toLocaleString("de-DE", {
|
||||||
|
day: "2-digit", month: "2-digit", year: "numeric",
|
||||||
|
hour: "2-digit", minute: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** „seit heute" / „seit 3 Tagen" – die Frage im Alltag ist die Dauer. */
|
||||||
|
function seit(iso) {
|
||||||
|
const d = new Date(iso);
|
||||||
|
if (Number.isNaN(d.getTime())) return "";
|
||||||
|
const tage = Math.floor((Date.now() - d.getTime()) / 86400000);
|
||||||
|
if (tage <= 0) return "seit heute";
|
||||||
|
return `seit ${tage} ${tage === 1 ? "Tag" : "Tagen"}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Der aktuelle Zustand als Plakette – für Tabellenspalten. */
|
||||||
|
export function VerwendungBadge({ item }) {
|
||||||
|
if (!item?.verwendung_moeglich) return <span className="muted">–</span>;
|
||||||
|
const status = item.verwendung?.status || null;
|
||||||
|
if (!status) return <span className="muted">Verfügbar</span>;
|
||||||
|
const art = ART[status];
|
||||||
|
return (
|
||||||
|
<span className={art ? `badge ${art}` : "badge"} title={seit(item.verwendung.started_at)}>
|
||||||
|
{verwendungLabel(status)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die vier Knöpfe, der aktuelle hervorgehoben. Ein Griff genügt – gedacht für
|
||||||
|
* „gerade aus der Wäsche geholt, ist wieder im Schrank" direkt nach dem Scannen.
|
||||||
|
*/
|
||||||
|
export function VerwendungAktion({ item, onChanged, onError, size = "sm" }) {
|
||||||
|
const toast = useToast();
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
if (!item?.verwendung_moeglich) return null;
|
||||||
|
const aktuell = item.verwendung?.status || null;
|
||||||
|
|
||||||
|
async function setze(status) {
|
||||||
|
if (status === aktuell) return;
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await api.setzeVerwendung(item.id, status);
|
||||||
|
toast(`${item.uid}: ${verwendungLabel(status)}`);
|
||||||
|
await onChanged?.();
|
||||||
|
} catch (err) { onError?.(err.message); } finally { setBusy(false); }
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="cell-row" style={{ gap: "var(--sp-2)", flexWrap: "wrap" }}>
|
||||||
|
{ZUSTAENDE.map(({ status, label }) => (
|
||||||
|
<button key={status || "frei"} type="button" disabled={busy}
|
||||||
|
className={`btn ${size} ${status === aktuell ? "primary" : "ghost"}`}
|
||||||
|
aria-pressed={status === aktuell}
|
||||||
|
onClick={() => setze(status)}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 (
|
||||||
|
<button type="button" className="btn sm ghost" onClick={laden}>
|
||||||
|
<Icon name="history" size={14} />Verwendungs-Verlauf
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (zeilen == null) return <span className="muted small">Wird geladen…</span>;
|
||||||
|
if (zeilen.length === 0) {
|
||||||
|
return <span className="muted small">War noch nie in Verwendung.</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="clean-list">
|
||||||
|
{zeilen.map((v) => (
|
||||||
|
<li key={v.id} className="cell-row" style={{ gap: "var(--sp-2)", flexWrap: "wrap" }}>
|
||||||
|
<span className="strong">{verwendungLabel(v.status)}</span>
|
||||||
|
<span className="muted small">
|
||||||
|
{zeitpunkt(v.started_at)}
|
||||||
|
{v.ended_at ? ` – ${zeitpunkt(v.ended_at)}` : " – läuft"}
|
||||||
|
{v.username ? ` · ${v.username}` : ""}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,7 +21,9 @@ export default function Categories() {
|
|||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const { isAdmin } = useAuth();
|
const { isAdmin } = useAuth();
|
||||||
const [categories, setCategories] = useState([]);
|
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);
|
const [error, setError] = useState(null);
|
||||||
// Welche Kategorie ihre Seitenkarte zeigt und welche davon: alles in der
|
// Welche Kategorie ihre Seitenkarte zeigt und welche davon: alles in der
|
||||||
// URL, damit ein Neuladen dieselbe Ansicht wiederherstellt. Gespeichert
|
// URL, damit ein Neuladen dieselbe Ansicht wiederherstellt. Gespeichert
|
||||||
@@ -68,8 +70,14 @@ export default function Categories() {
|
|||||||
name: form.name,
|
name: form.name,
|
||||||
parent_id: form.parent_id === "" ? null : Number(form.parent_id),
|
parent_id: form.parent_id === "" ? null : Number(form.parent_id),
|
||||||
tracking: form.tracking === "" ? null : form.tracking,
|
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();
|
load();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
@@ -137,6 +145,15 @@ export default function Categories() {
|
|||||||
<option value="object">Gegenstand</option>
|
<option value="object">Gegenstand</option>
|
||||||
</select>
|
</select>
|
||||||
) : <span className="badge">{MODUS[c.tracking] || c.tracking}</span>) },
|
) : <span className="badge">{MODUS[c.tracking] || c.tracking}</span>) },
|
||||||
|
// 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 ? (
|
||||||
|
<input type="checkbox" checked={!!c.usage_tracking}
|
||||||
|
onChange={(e) => patch(c, { usage_tracking: e.target.checked })} />
|
||||||
|
) : (c.usage_tracking ? <Icon name="check" size={16} /> : <span className="muted">–</span>)) },
|
||||||
{ key: "parent", header: "Übergeordnet", width: 220,
|
{ key: "parent", header: "Übergeordnet", width: 220,
|
||||||
filterText: (c) => (c.parent_id ? nameById[c.parent_id] || "" : ""),
|
filterText: (c) => (c.parent_id ? nameById[c.parent_id] || "" : ""),
|
||||||
render: (c) => {
|
render: (c) => {
|
||||||
@@ -229,10 +246,21 @@ export default function Categories() {
|
|||||||
<option value="object">Gegenstand (Menge je Lagerort)</option>
|
<option value="object">Gegenstand (Menge je Lagerort)</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<label className="check-inline">
|
||||||
|
<input type="checkbox" checked={form.usage_tracking}
|
||||||
|
onChange={(e) => setForm({ ...form, usage_tracking: e.target.checked })} />
|
||||||
|
<span>Verwendungsstatus führen</span>
|
||||||
|
</label>
|
||||||
<button className="btn primary"><Icon name="plus" size={16} />Anlegen</button>
|
<button className="btn primary"><Icon name="plus" size={16} />Anlegen</button>
|
||||||
<p className="muted small">
|
<p className="muted small">
|
||||||
Unterkategorien erben Art und Felder der Oberkategorie. Auf eine
|
Unterkategorien erben Art, Felder und Verwendungsstatus der
|
||||||
Oberkategorie gefiltert erscheinen auch die Artikel ihrer Unterkategorien.
|
Oberkategorie. Auf eine Oberkategorie gefiltert erscheinen auch
|
||||||
|
die Artikel ihrer Unterkategorien.
|
||||||
|
</p>
|
||||||
|
<p className="muted small">
|
||||||
|
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.
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
VerleihText,
|
VerleihText,
|
||||||
verleihRowClass,
|
verleihRowClass,
|
||||||
} from "../components/Verleih";
|
} from "../components/Verleih";
|
||||||
|
import { VerwendungBadge, verwendungLabel } from "../components/Verwendung";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Liste aller Einzelstücke (physische Exemplare, jedes mit eigener UID/Lagerort) –
|
* 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),
|
filterOptionLabel: (v) => (v === "" ? "(im Haus)" : v),
|
||||||
sortValue: (it) => it.loan?.borrower || "",
|
sortValue: (it) => it.loan?.borrower || "",
|
||||||
render: (it) => <VerleihText loan={it.loan} /> },
|
render: (it) => <VerleihText loan={it.loan} /> },
|
||||||
|
// 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) => <VerwendungBadge item={it} /> },
|
||||||
{ key: "due", header: "Zurück bis", width: 120,
|
{ key: "due", header: "Zurück bis", width: 120,
|
||||||
filterText: (it) => it.loan?.due_on || "",
|
filterText: (it) => it.loan?.due_on || "",
|
||||||
sortValue: (it) => it.loan?.due_on || "",
|
sortValue: (it) => it.loan?.due_on || "",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { api } from "../api";
|
|||||||
import Icon from "../components/Icon";
|
import Icon from "../components/Icon";
|
||||||
import { ProduktThumb } from "../components/ProduktBild";
|
import { ProduktThumb } from "../components/ProduktBild";
|
||||||
import { FristBadge, VerleihForm, VerleihHistorie } from "../components/Verleih";
|
import { FristBadge, VerleihForm, VerleihHistorie } from "../components/Verleih";
|
||||||
|
import { VerwendungAktion, VerwendungHistorie, verwendungLabel } from "../components/Verwendung";
|
||||||
import { useToast } from "../toast";
|
import { useToast } from "../toast";
|
||||||
import { locationPathById } from "../locationPath";
|
import { locationPathById } from "../locationPath";
|
||||||
|
|
||||||
@@ -64,6 +65,9 @@ export default function ItemResolve() {
|
|||||||
<span className="strong">{uid}</span>
|
<span className="strong">{uid}</span>
|
||||||
{item?.product_brand ? ` · ${item.product_brand}` : ""}
|
{item?.product_brand ? ` · ${item.product_brand}` : ""}
|
||||||
{ort ? ` · ${ort}` : ""}
|
{ort ? ` · ${ort}` : ""}
|
||||||
|
{item?.verwendung_moeglich
|
||||||
|
? ` · ${verwendungLabel(item.verwendung?.status || null)}`
|
||||||
|
: ""}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -95,6 +99,15 @@ export default function ItemResolve() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{item.verwendung_moeglich && (
|
||||||
|
<div style={{ marginBottom: "var(--sp-3)" }}>
|
||||||
|
<div className="muted small" style={{ marginBottom: "var(--sp-1)" }}>
|
||||||
|
Verwendung
|
||||||
|
</div>
|
||||||
|
<VerwendungAktion item={item} onError={setError} onChanged={laden} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<dl className="kv">
|
<dl className="kv">
|
||||||
<dt>Lagerort</dt>
|
<dt>Lagerort</dt>
|
||||||
<dd>
|
<dd>
|
||||||
@@ -121,6 +134,9 @@ export default function ItemResolve() {
|
|||||||
<div style={{ marginTop: "var(--sp-3)" }}>
|
<div style={{ marginTop: "var(--sp-3)" }}>
|
||||||
<VerleihHistorie item={item} onError={setError} />
|
<VerleihHistorie item={item} onError={setError} />
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ marginTop: "var(--sp-2)" }}>
|
||||||
|
<VerwendungHistorie item={item} onError={setError} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user