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:
co-authored by
Claude Opus 5
parent
1332dd86fa
commit
aa72d8342a
+19
-1
@@ -6,7 +6,8 @@ REST-API wie die Web-Oberfläche.
|
||||
> **Stand:** Voll nutzbar. Login; Scannen von Barcode, MHD/Datum (per
|
||||
> Texterkennung, auch nur Tag+Monat ohne Jahr), Lagerort-QR und Name/Marke;
|
||||
> Ein-/Auslagern mit mehreren Chargen bzw. Menge je Lagerort; Einzelstücke mit
|
||||
> UID/QR, Kaufdatum, Garantie und Beleg; Verbrauchsgegenstände als Charge;
|
||||
> UID/QR, Kaufdatum, Garantie und Beleg – auch **verleihen** und
|
||||
> zurücknehmen; Verbrauchsgegenstände als Charge;
|
||||
> Artikel anlegen (Open Food/Products Facts oder manuell, mit Foto); getrennte
|
||||
> Listen für Lebensmittel/Gegenstände/Einzelstücke, Kategoriefilter, Sortierung;
|
||||
> Einkaufsliste, Ablaufliste, Mindestbestände; Verlauf mit Sprung zum Produkt;
|
||||
@@ -63,6 +64,23 @@ Es gibt zwei Wege direkt in den Scan-Bildschirm:
|
||||
|
||||
Beide Wege öffnen die App **direkt in der Kamera**.
|
||||
|
||||
## Einzelstücke verleihen
|
||||
|
||||
In der Ansicht eines Einzelstücks steht **Verleih** ganz oben: verliehen an wen,
|
||||
seit wann, bis wann – und der Knopf zum Herausgeben bzw. Zurücknehmen. Pflicht
|
||||
ist nur der Name; Grund und Rückgabedatum sind optional, denn „kurz mal
|
||||
mitgenommen" soll nicht an einem Datum scheitern.
|
||||
|
||||
Dass der Knopf genau dort sitzt, macht **scannen und verleihen** möglich, ohne
|
||||
einen eigenen Weg dafür zu bauen: Alle Scan-Wege der App enden in dieser
|
||||
Ansicht – der QR über *Nachschlagen*, ein eingenähtes NFC-Tag über *NFC-Tag
|
||||
lesen* und ein angetipptes Tag über `vorrania://i/<UID>`.
|
||||
|
||||
Der Lagerort bleibt beim Verleihen stehen – dorthin gehört das Stück zurück. Als
|
||||
verliehen zählt es aber nicht mehr zum Bestand, es ist ja nicht im Haus. In der
|
||||
Einzelstück-Liste steht der Verleih unter dem Stück (überfällig in Rot) und lässt
|
||||
sich über das Filter-Menü auf *Verliehen*, *Überfällig* oder *Im Haus* eingrenzen.
|
||||
|
||||
## NFC-Tags für Kleidung
|
||||
Ein aufgeklebtes QR-Etikett übersteht keine Wäsche. Für Kleidungsstücke gibt es
|
||||
deshalb eingenähte NFC-Tags: Einzelstück öffnen ▸ **NFC-Tag beschreiben** ▸ Tag
|
||||
|
||||
@@ -276,6 +276,34 @@ actor APIClient {
|
||||
return try await send(request, as: Item.self)
|
||||
}
|
||||
|
||||
// MARK: - Verleih von Einzelstuecken
|
||||
|
||||
/// Herausgeben. Scheitert mit 409, wenn das Stueck schon verliehen ist.
|
||||
func lendItem(id: Int, _ payload: LoanCreateRequest) async throws -> Loan {
|
||||
var request = try makeRequest("/items/\(id)/loan", method: "POST")
|
||||
try jsonBody(&request, payload)
|
||||
return try await send(request, as: Loan.self)
|
||||
}
|
||||
|
||||
/// Rueckgabe buchen. Leeres Datum = heute.
|
||||
func returnItem(id: Int, _ payload: LoanReturnRequest = LoanReturnRequest()) async throws -> Loan {
|
||||
var request = try makeRequest("/items/\(id)/return", method: "POST")
|
||||
try jsonBody(&request, payload)
|
||||
return try await send(request, as: Loan.self)
|
||||
}
|
||||
|
||||
/// Verleih-Historie eines Stuecks, neueste zuerst.
|
||||
func itemLoans(id: Int) async throws -> [Loan] {
|
||||
try await send(try makeRequest("/items/\(id)/loans"), as: [Loan].self)
|
||||
}
|
||||
|
||||
/// Offene Verleihe, faelligste zuerst. `dueIn` grenzt auf die ein, deren
|
||||
/// Rueckgabe in so vielen Tagen ansteht (ueberfaellige sind immer dabei).
|
||||
func loans(dueIn: Int? = nil) async throws -> [Loan] {
|
||||
let pfad = dueIn.map { "/loans?due_in=\($0)" } ?? "/loans"
|
||||
return try await send(try makeRequest(pfad), as: [Loan].self)
|
||||
}
|
||||
|
||||
func removeItem(id: Int, _ payload: ItemRemoveRequest) async throws {
|
||||
var request = try makeRequest("/items/\(id)/remove", method: "POST")
|
||||
try jsonBody(&request, payload)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
+197
-1
@@ -9,6 +9,29 @@ private func dateToString(_ d: Date) -> String {
|
||||
private func stringToDate(_ s: String) -> Date? {
|
||||
let f = DateFormatter(); f.dateFormat = "yyyy-MM-dd"; return f.date(from: s)
|
||||
}
|
||||
/// "2026-09-20" -> "20.09.2026". Faellt auf den Rohwert zurueck, damit nie
|
||||
/// eine leere Zeile steht.
|
||||
private func anzeigeDatum(_ iso: String?) -> String {
|
||||
guard let iso else { return "–" }
|
||||
guard let d = stringToDate(iso) else { return iso }
|
||||
let f = DateFormatter()
|
||||
f.locale = Locale(identifier: "de_DE")
|
||||
f.dateFormat = "dd.MM.yyyy"
|
||||
return f.string(from: d)
|
||||
}
|
||||
|
||||
/// Verleihzustand in einer Zeile: „Markus · zurueck bis 20.09.2026".
|
||||
/// Ueberfaelliges kommt beim Aufrufer rot daher.
|
||||
func verleihZeile(_ loan: Loan) -> String {
|
||||
var teile = [loan.borrower]
|
||||
if let tage = loan.daysOverdue {
|
||||
teile.append("überfällig seit \(tage) \(tage == 1 ? "Tag" : "Tagen")")
|
||||
} else if let due = loan.dueOn {
|
||||
teile.append("zurück bis \(anzeigeDatum(due))")
|
||||
}
|
||||
if let grund = loan.reason, !grund.isEmpty { teile.append(grund) }
|
||||
return teile.joined(separator: " · ")
|
||||
}
|
||||
|
||||
/// Datumszeile mit einheitlichem Format (dd.MM.yyyy). Die native compact-DatePicker
|
||||
/// zeigt das Jahr je nach Feld mal zwei-, mal vierstellig – deshalb hier ein fester
|
||||
@@ -88,7 +111,12 @@ struct ProductItemsSection: View {
|
||||
}
|
||||
|
||||
private func untertitel(_ it: Item) -> String {
|
||||
[it.locationName, it.acquiredOn.map { "gekauft \($0)" }, it.shopName]
|
||||
// Ist das Stueck draussen, ist das die wichtigste Angabe - der Lagerort
|
||||
// steht daneben, weil es dorthin zurueckgehoert.
|
||||
if let loan = it.loan {
|
||||
return "verliehen an \(verleihZeile(loan))"
|
||||
}
|
||||
return [it.locationName, it.acquiredOn.map { "gekauft \($0)" }, it.shopName]
|
||||
.compactMap { $0 }.joined(separator: " · ")
|
||||
}
|
||||
private func reload() async {
|
||||
@@ -195,6 +223,15 @@ struct ItemEditView: View {
|
||||
@State private var previewURL: URL?
|
||||
@State private var locScanShown = false
|
||||
@State private var nfcShown = false
|
||||
// Verleih: der offene Vorgang (nil = im Haus) und die Eingaben dafuer.
|
||||
@State private var loan: Loan?
|
||||
@State private var loanHistory: [Loan] = []
|
||||
@State private var showLend = false
|
||||
@State private var borrower = ""
|
||||
@State private var loanReason = ""
|
||||
@State private var hasDue = false
|
||||
@State private var due = Date()
|
||||
@State private var busyLoan = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
@@ -221,6 +258,44 @@ struct ItemEditView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// Verleih steht vor allem anderen: Ist das Stueck draussen, ist das
|
||||
// die Antwort auf „wo ist es?". Alle Scan-Wege der App (QR, NFC,
|
||||
// vorrania://i/<UID>) landen in dieser Ansicht - deshalb genuegt der
|
||||
// Knopf hier, um „scannen und verleihen" moeglich zu machen.
|
||||
Section("Verleih") {
|
||||
if let loan {
|
||||
LabeledContent("Verliehen an", value: loan.borrower)
|
||||
if let grund = loan.reason, !grund.isEmpty {
|
||||
LabeledContent("Grund", value: grund)
|
||||
}
|
||||
LabeledContent("Seit", value: anzeigeDatum(loan.lentOn))
|
||||
if let tage = loan.daysOverdue {
|
||||
LabeledContent("Zurück bis") {
|
||||
Text("\(anzeigeDatum(loan.dueOn)) · überfällig seit "
|
||||
+ "\(tage) \(tage == 1 ? "Tag" : "Tagen")")
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
} else {
|
||||
LabeledContent("Zurück bis",
|
||||
value: loan.dueOn == nil ? "offen" : anzeigeDatum(loan.dueOn))
|
||||
}
|
||||
Button { Task { await nimmZurueck() } } label: {
|
||||
Label(busyLoan ? "Moment…" : "Zurücknehmen", systemImage: "arrow.uturn.down")
|
||||
}
|
||||
.disabled(busyLoan)
|
||||
} else {
|
||||
Text("Im Haus.").foregroundStyle(.secondary)
|
||||
Button { starteVerleih() } label: {
|
||||
Label("Verleihen", systemImage: "hand.raised")
|
||||
}
|
||||
}
|
||||
if !loanHistory.isEmpty {
|
||||
NavigationLink("Verleih-Verlauf (\(loanHistory.count))") {
|
||||
LoanHistoryView(uid: item.uid, loans: loanHistory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Picker("Lagerort", selection: $locationId) {
|
||||
Text("– ohne –").tag(String?.none)
|
||||
@@ -328,6 +403,9 @@ struct ItemEditView: View {
|
||||
.sheet(isPresented: $showRemove) {
|
||||
NavigationStack { removeSheet }
|
||||
}
|
||||
.sheet(isPresented: $showLend) {
|
||||
NavigationStack { lendSheet }
|
||||
}
|
||||
.fileImporter(isPresented: $showFileImporter, allowedContentTypes: [.pdf]) { result in
|
||||
if case .success(let url) = result {
|
||||
Task {
|
||||
@@ -354,6 +432,32 @@ struct ItemEditView: View {
|
||||
.quickLookPreview($previewURL)
|
||||
}
|
||||
|
||||
/// Herausgeben: Nur der Name ist Pflicht. Das Rueckgabedatum ist bewusst
|
||||
/// abschaltbar - „kurz mal mitgenommen" soll nicht an einem Datum scheitern.
|
||||
private var lendSheet: some View {
|
||||
Form {
|
||||
Section {
|
||||
LabeledField(label: "Verliehen an", text: $borrower)
|
||||
LabeledField(label: "Grund (optional)", text: $loanReason)
|
||||
Toggle("Rückgabe bis", isOn: $hasDue)
|
||||
if hasDue { DateRow(label: "Zurück bis", date: $due) }
|
||||
} footer: {
|
||||
Text("Der Lagerort bleibt stehen – dorthin gehört das Stück zurück. "
|
||||
+ "Als verliehen zählt es aber nicht mehr zum Bestand.")
|
||||
}
|
||||
if let error { Section { Text(error).foregroundStyle(.red).font(.callout) } }
|
||||
Section {
|
||||
Button(busyLoan ? "Moment…" : "Verleihen") { Task { await verleihe() } }
|
||||
.disabled(busyLoan || borrower.trimmingCharacters(in: .whitespaces).isEmpty)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Verleihen")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { showLend = false } }
|
||||
}
|
||||
}
|
||||
|
||||
private var removeSheet: some View {
|
||||
Form {
|
||||
Picker("Grund", selection: $reason) {
|
||||
@@ -369,6 +473,47 @@ struct ItemEditView: View {
|
||||
.toolbar { ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { showRemove = false } } }
|
||||
}
|
||||
|
||||
// MARK: - Verleih
|
||||
|
||||
private func starteVerleih() {
|
||||
borrower = ""
|
||||
loanReason = ""
|
||||
hasDue = false
|
||||
due = Date()
|
||||
error = nil
|
||||
showLend = true
|
||||
}
|
||||
|
||||
private func verleihe() async {
|
||||
busyLoan = true; defer { busyLoan = false }; error = nil
|
||||
do {
|
||||
loan = try await APIClient.shared.lendItem(id: item.id, LoanCreateRequest(
|
||||
borrower: borrower.trimmingCharacters(in: .whitespaces),
|
||||
reason: loanReason.isEmpty ? nil : loanReason,
|
||||
dueOn: hasDue ? dateToString(due) : nil))
|
||||
showLend = false
|
||||
await ladeVerleihe()
|
||||
onChanged?()
|
||||
} catch { self.error = error.localizedDescription }
|
||||
}
|
||||
|
||||
private func nimmZurueck() async {
|
||||
busyLoan = true; defer { busyLoan = false }; error = nil
|
||||
do {
|
||||
_ = try await APIClient.shared.returnItem(id: item.id)
|
||||
loan = nil
|
||||
await ladeVerleihe()
|
||||
onChanged?()
|
||||
} catch { self.error = error.localizedDescription }
|
||||
}
|
||||
|
||||
/// Offenen Verleih und Historie frisch holen - nach jeder Buchung, damit die
|
||||
/// Ansicht nicht auf dem Stand des uebergebenen `item` haengen bleibt.
|
||||
private func ladeVerleihe() async {
|
||||
loanHistory = (try? await APIClient.shared.itemLoans(id: item.id)) ?? []
|
||||
loan = loanHistory.first(where: { $0.istOffen })
|
||||
}
|
||||
|
||||
private func load() async {
|
||||
locations = (try? await APIClient.shared.locations()) ?? []
|
||||
shops = (try? await APIClient.shared.shops()) ?? []
|
||||
@@ -381,6 +526,8 @@ struct ItemEditView: View {
|
||||
if let p = item.priceCents { priceText = ItemEditView.formatCents(p) }
|
||||
if let s = item.acquiredOn, let d = stringToDate(s) { acquired = d; hasAcquired = true }
|
||||
if let s = item.warrantyUntil, let d = stringToDate(s) { warranty = d; hasWarranty = true }
|
||||
loan = item.loan
|
||||
await ladeVerleihe()
|
||||
}
|
||||
|
||||
private func save() async {
|
||||
@@ -677,3 +824,52 @@ struct ItemAddSheet: View {
|
||||
} catch { self.error = error.localizedDescription }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Verleih-Verlauf
|
||||
|
||||
/// Alle Verleihe eines Stuecks, neueste zuerst. Zurueckgegebene Vorgaenge bleiben
|
||||
/// erhalten - so ist nachvollziehbar, wer das Stueck wann hatte.
|
||||
struct LoanHistoryView: View {
|
||||
let uid: String
|
||||
let loans: [Loan]
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(loans) { loan in
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(loan.borrower).bold()
|
||||
Spacer()
|
||||
if loan.istOffen {
|
||||
Text(loan.istUeberfaellig ? "überfällig" : "offen")
|
||||
.font(.caption)
|
||||
.foregroundStyle(loan.istUeberfaellig ? Color.red : .secondary)
|
||||
}
|
||||
}
|
||||
Text(zeitraum(loan)).font(.caption).foregroundStyle(.secondary)
|
||||
if let grund = loan.reason, !grund.isEmpty {
|
||||
Text(grund).font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
if let von = loan.username {
|
||||
Text("herausgegeben von \(von)").font(.caption2).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
if loans.isEmpty {
|
||||
Text("Noch nie verliehen.").foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Verlauf \(uid)")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
private func zeitraum(_ loan: Loan) -> String {
|
||||
let von = anzeigeDatum(loan.lentOn)
|
||||
guard let zurueck = loan.returnedOn else {
|
||||
return loan.dueOn == nil
|
||||
? "seit \(von)"
|
||||
: "seit \(von) · zurück bis \(anzeigeDatum(loan.dueOn))"
|
||||
}
|
||||
return "\(von) – \(anzeigeDatum(zurueck))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1046,6 +1046,68 @@ struct ItemDocument: Codable, Identifiable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ein Verleih-Vorgang eines Einzelstuecks. `returnedOn == nil` heisst „noch
|
||||
/// draussen"; je Stueck gibt es hoechstens einen solchen offenen Verleih.
|
||||
///
|
||||
/// `daysOverdue` rechnet der Server, damit App und Web nicht getrennt rechnen -
|
||||
/// nil heisst: ohne Frist, zurueck, oder noch nicht faellig.
|
||||
struct Loan: Codable, Identifiable, Hashable {
|
||||
let id: Int
|
||||
let itemId: Int
|
||||
let borrower: String
|
||||
let reason: String?
|
||||
let lentOn: String // "yyyy-MM-dd"
|
||||
let dueOn: String?
|
||||
let returnedOn: String?
|
||||
let createdAt: String
|
||||
let username: String?
|
||||
let daysOverdue: Int?
|
||||
let itemUid: String?
|
||||
let productId: Int?
|
||||
let productName: String?
|
||||
let productBrand: String?
|
||||
let locationId: String?
|
||||
let locationName: String?
|
||||
|
||||
var istOffen: Bool { returnedOn == nil }
|
||||
var istUeberfaellig: Bool { (daysOverdue ?? 0) > 0 }
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, borrower, reason, username
|
||||
case itemId = "item_id"
|
||||
case lentOn = "lent_on"
|
||||
case dueOn = "due_on"
|
||||
case returnedOn = "returned_on"
|
||||
case createdAt = "created_at"
|
||||
case daysOverdue = "days_overdue"
|
||||
case itemUid = "item_uid"
|
||||
case productId = "product_id"
|
||||
case productName = "product_name"
|
||||
case productBrand = "product_brand"
|
||||
case locationId = "location_id"
|
||||
case locationName = "location_name"
|
||||
}
|
||||
}
|
||||
|
||||
struct LoanCreateRequest: Encodable {
|
||||
let borrower: String
|
||||
var reason: String? = nil
|
||||
var lentOn: String? = nil
|
||||
var dueOn: String? = nil
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case borrower, reason
|
||||
case lentOn = "lent_on"
|
||||
case dueOn = "due_on"
|
||||
}
|
||||
}
|
||||
|
||||
struct LoanReturnRequest: Encodable {
|
||||
var returnedOn: String? = nil
|
||||
|
||||
enum CodingKeys: String, CodingKey { case returnedOn = "returned_on" }
|
||||
}
|
||||
|
||||
struct Item: Codable, Identifiable, Hashable {
|
||||
let id: Int
|
||||
let uid: String
|
||||
@@ -1065,9 +1127,14 @@ struct Item: Codable, Identifiable, Hashable {
|
||||
let categoryId: Int?
|
||||
let categoryName: String?
|
||||
let documents: [ItemDocument]
|
||||
/// Der offene Verleih, falls das Stueck gerade draussen ist - sonst nil.
|
||||
/// Der Lagerort steht daneben unveraendert: dorthin gehoert es zurueck.
|
||||
let loan: Loan?
|
||||
|
||||
var istVerliehen: Bool { loan != nil }
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, uid, note, currency, documents
|
||||
case id, uid, note, currency, documents, loan
|
||||
case productId = "product_id"
|
||||
case locationId = "location_id"
|
||||
case locationName = "location_name"
|
||||
|
||||
Reference in New Issue
Block a user