diff --git a/ios/Sources/Models.swift b/ios/Sources/Models.swift index bade83e..adff696 100644 --- a/ios/Sources/Models.swift +++ b/ios/Sources/Models.swift @@ -605,15 +605,28 @@ struct ProductUpdateRequest: Encodable { } } -struct LotUpdateRequest: Codable { +struct LotUpdateRequest: Encodable { var quantity: Double? var bestBefore: String? var bestBeforePrecision: String? + var locationId: String? enum CodingKeys: String, CodingKey { case quantity case bestBefore = "best_before" case bestBeforePrecision = "best_before_precision" + case locationId = "location_id" + } + + // Den Lagerort immer senden (auch null), damit „ohne Ort" den gesetzten Ort + // wieder entfernt. Die übrigen Felder bleiben weg, wenn sie nicht gesetzt + // sind – das Backend fasst sie dann als „unverändert" auf. + func encode(to encoder: Encoder) throws { + var c = encoder.container(keyedBy: CodingKeys.self) + try c.encodeIfPresent(quantity, forKey: .quantity) + try c.encodeIfPresent(bestBefore, forKey: .bestBefore) + try c.encodeIfPresent(bestBeforePrecision, forKey: .bestBeforePrecision) + try c.encode(locationId, forKey: .locationId) } } diff --git a/ios/Sources/ProductDetailView.swift b/ios/Sources/ProductDetailView.swift index 362c3c9..719d5e4 100644 --- a/ios/Sources/ProductDetailView.swift +++ b/ios/Sources/ProductDetailView.swift @@ -241,7 +241,10 @@ struct ProductDetailView: View { HStack { VStack(alignment: .leading, spacing: 2) { Text("MHD \(display.formatBestBefore(lot.bestBefore, precision: lot.bestBeforePrecision))") - Text("\(formatAmount(lot.quantity / current.articleUnitFactor)) \(current.articleUnitLabel)") + // Ohne den Lagerort sehen zwei Chargen mit + // gleichem MHD identisch aus – er ist hier + // oft das einzige Unterscheidungsmerkmal. + Text("\(formatAmount(lot.quantity / current.articleUnitFactor)) \(current.articleUnitLabel) · \(lotLocationLabel(lot))") .font(.caption).foregroundStyle(.secondary) } Spacer() @@ -286,7 +289,7 @@ struct ProductDetailView: View { } .sheet(item: $editLot) { lot in NavigationStack { - LotEditView(lot: lot, product: current) { + LotEditView(lot: lot, product: current, locations: locations) { Task { await reload() } } } @@ -364,6 +367,14 @@ struct ProductDetailView: View { return Binding(get: { fieldValues[key] ?? "" }, set: { fieldValues[key] = $0 }) } + /// Lagerort einer Charge als Pfad („Keller → Regal 2"). Chargen ohne Ort – + /// und solche, deren Ort (noch) nicht geladen ist – bleiben „ohne Ort". + private func lotLocationLabel(_ lot: Lot) -> String { + guard let id = lot.locationId, + let ort = locations.first(where: { $0.id == id }) else { return "ohne Ort" } + return ort.path(in: locations) + } + private func reload() async { groups = (try? await APIClient.shared.groups()) ?? [] categories = (try? await APIClient.shared.categories()) ?? [] @@ -508,10 +519,11 @@ struct ProductByIdView: View { } } -/// Menge und MHD einer Charge korrigieren. +/// Menge, MHD und Lagerort einer Charge korrigieren. struct LotEditView: View { let lot: Lot let product: Product + let locations: [StorageLocation] var onSaved: () -> Void @Environment(\.dismiss) private var dismiss @@ -520,6 +532,8 @@ struct LotEditView: View { @State private var hasDate = false @State private var bestBefore = Date() @State private var precision = "day" + @State private var locationId: String? + @State private var locScanShown = false @State private var busy = false @State private var error: String? @@ -545,6 +559,19 @@ struct LotEditView: View { } } + if !locations.isEmpty { + Section("Lagerort") { + Picker("Lagerort", selection: $locationId) { + Text("– ohne –").tag(String?.none) + ForEach(locations) { l in Text(l.path(in: locations)).tag(String?.some(l.id)) } + } + // Ort per QR am Regal/Fach setzen, statt in der Liste zu suchen. + Button { locScanShown = true } label: { + Label("Lagerort scannen", systemImage: "qrcode.viewfinder") + } + } + } + if let error { Section { Text(error).foregroundStyle(.red).font(.callout) } } @@ -559,11 +586,16 @@ struct LotEditView: View { .toolbar { ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { dismiss() } } } + .fullScreenCover(isPresented: $locScanShown) { + LocationScannerView(locations: locations) { ort in locationId = ort.id } + .ignoresSafeArea() + } .onAppear(perform: fill) } private func fill() { quantity = formatAmount(lot.quantity / product.articleUnitFactor) + locationId = lot.locationId precision = lot.bestBeforePrecision == "month" ? "month" : "day" if let raw = lot.bestBefore { let formatter = DateFormatter() @@ -595,7 +627,8 @@ struct LotEditView: View { LotUpdateRequest( quantity: menge * product.articleUnitFactor, bestBefore: hasDate ? formatter.string(from: datum) : nil, - bestBeforePrecision: precision + bestBeforePrecision: precision, + locationId: locationId ) ) onSaved()