App: Einzelstuecke verleihen - alle Scan-Wege enden schon dort

Verleih-Abschnitt im Einzelstueck: an wen, seit wann, bis wann, dazu
Herausgeben bzw. Zuruecknehmen und der Verleih-Verlauf. Pflicht ist nur der
Name; Grund und Rueckgabedatum sind abschaltbar, weil "kurz mal
mitgenommen" nicht an einem Datum scheitern soll.

Der Abschnitt sitzt bewusst oben in ItemEditView, und zwar nicht nur der
Wichtigkeit wegen: QR ueber Nachschlagen, eingenaehtes NFC-Tag und ein
angetipptes Tag (vorrania://i/<UID>) landen alle in genau dieser Ansicht.
Damit ist "scannen und verleihen" ueberall moeglich, ohne einen eigenen
Weg dafuer zu bauen.

In der Einzelstueck-Liste steht der Verleih unter dem Stueck - ueberfaellig
in Rot - und das Filter-Menue kann auf Verliehen, Ueberfaellig oder Im Haus
eingrenzen. Wo etwas ist, war bisher immer der Lagerort; ist es verliehen,
ist der Entleiher die Antwort, und der Lagerort steht daneben.

days_overdue rechnet der Server (GET /loans, ItemOut.loan), damit App und
Web nicht getrennt rechnen und nicht auseinanderlaufen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-17 09:36:05 +02:00
co-authored by Claude Opus 5
parent 1332dd86fa
commit aa72d8342a
7 changed files with 366 additions and 9 deletions
+41 -2
View File
@@ -10,6 +10,22 @@ struct ItemListView: View {
@State private var query = ""
@State private var locationFilter: String? // Lagerort-ID
@State private var categoryFilter: Int? // Kategorie-ID (Unterkategorien zählen mit)
@State private var verleihFilter: VerleihFilter = .alle
/// Blickwinkel auf den Verleihzustand – „was ist gerade draußen?" ist die
/// Frage, die man im Alltag an diese Liste stellt.
enum VerleihFilter: String, CaseIterable {
case alle, verliehen, ueberfaellig, imHaus
var label: String {
switch self {
case .alle: return "Alle"
case .verliehen: return "Verliehen"
case .ueberfaellig: return "Überfällig"
case .imHaus: return "Im Haus"
}
}
}
@State private var busy = true
@State private var error: String?
@@ -73,6 +89,12 @@ struct ItemListView: View {
return items.filter { it in
if let cat = categoryFilter, !imTeilbaum(it.categoryId, target: cat) { return false }
if let loc = locationFilter, it.locationId != loc { return false }
switch verleihFilter {
case .alle: break
case .verliehen: if it.loan == nil { return false }
case .ueberfaellig: if !(it.loan?.istUeberfaellig ?? false) { return false }
case .imHaus: if it.loan != nil { return false }
}
if !q.isEmpty {
let heu = [it.productName, it.uid, it.note, it.productBrand]
.compactMap { $0 }.joined(separator: " ").lowercased()
@@ -82,7 +104,9 @@ struct ItemListView: View {
}
}
private var hatFilter: Bool { locationFilter != nil || categoryFilter != nil }
private var hatFilter: Bool {
locationFilter != nil || categoryFilter != nil || verleihFilter != .alle
}
var body: some View {
List {
@@ -117,6 +141,11 @@ struct ItemListView: View {
check(loc.path(in: locations), locationFilter == loc.id) { locationFilter = loc.id }
}
}
Menu("Verleih") {
ForEach(VerleihFilter.allCases, id: \.self) { f in
check(f.label, verleihFilter == f) { verleihFilter = f }
}
}
if !filterKategorien.isEmpty {
Menu("Kategorie") {
check("Alle", categoryFilter == nil) { categoryFilter = nil }
@@ -127,7 +156,9 @@ struct ItemListView: View {
}
if hatFilter {
Divider()
Button(role: .destructive) { locationFilter = nil; categoryFilter = nil } label: {
Button(role: .destructive) {
locationFilter = nil; categoryFilter = nil; verleihFilter = .alle
} label: {
Label("Filter zurücksetzen", systemImage: "xmark.circle")
}
}
@@ -175,6 +206,14 @@ struct ItemRow: View {
Text("· \(pfad)").font(.caption).foregroundStyle(.secondary).lineLimit(1)
}
}
// Verliehen ist die Antwort auf „wo ist es?" - deshalb eigene
// Zeile, ueberfaellig in Rot.
if let loan = item.loan {
Text("verliehen an \(verleihZeile(loan))")
.font(.caption)
.foregroundStyle(loan.istUeberfaellig ? Color.red : .secondary)
.lineLimit(2)
}
}
Spacer()
}