Web: Einzelstuecke verleihen, auch direkt nach dem Scannen

Verleihen und Zuruecknehmen in der Einzelstueck-Tabelle eines Artikels und
in der Liste aller Einzelstuecke, dort als Spalten "Verliehen an" und
"Zurueck bis" (filter- und sortierbar). Ueberfaellig ist rot, demnaechst
faellig gelb - dieselben Zustandsfarben wie beim MHD.

Die Scan-Zielseite /i/<uid> leitete vorher nur zum Artikel weiter. Fuer
"scannen und verleihen" braucht die Aktion aber genau dort ihren Platz:
Die Seite zeigt jetzt, was man in der Hand haelt - Stueck, Artikel,
Lagerort, Verleihzustand - und bietet Verleihen bzw. Zuruecknehmen an.
Gegenstueck zur Lagerort-Seite /l/<code>, die es fuers Einlagern genauso
macht.

Anzeige und Aktionen liegen einmal in components/Verleih.jsx, weil alle
drei Stellen dasselbe brauchen. Der Knopf haengt nicht an isAdmin:
Verleihen ist eine Buchung wie Ein- und Auslagern.

Dazu zwei Dashboard-Karten: alles Verliehene und, getrennt, nur das
Ueberfaellige. Stuecke ohne Rueckgabedatum sind nie faellig und tauchen in
der zweiten Karte nicht auf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-17 09:30:14 +02:00
co-authored by Claude Opus 5
parent 467ef3c804
commit 1332dd86fa
9 changed files with 472 additions and 36 deletions
+17
View File
@@ -227,6 +227,23 @@ export const api = {
deleteItemDocument: (itemId, docId) =>
request(`/items/${itemId}/documents/${docId}`, { method: "DELETE" }),
// Verleih von Einzelstücken. Der Lagerort bleibt dabei stehen – dorthin
// gehört das Stück zurück; verliehen zählt es aber nicht mehr als Bestand.
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`),
// Offene Verleihe, fälligste zuerst. dueIn grenzt auf die ein, deren Rückgabe
// in so vielen Tagen ansteht (überfällige sind immer dabei).
loans: ({ openOnly = true, dueIn } = {}) => {
const p = new URLSearchParams();
if (!openOnly) p.set("open_only", "false");
if (dueIn != null) p.set("due_in", String(dueIn));
const q = p.toString();
return request(`/loans${q ? `?${q}` : ""}`);
},
updateLoan: (loanId, body) => request(`/loans/${loanId}`, { method: "PATCH", body }),
deleteLoan: (loanId) => request(`/loans/${loanId}`, { method: "DELETE" }),
// Bestand
checkIn: (body) => request("/stock/checkin", { method: "POST", body }),
checkInBatch: (body) => request("/stock/checkin/batch", { method: "POST", body }),