From d029c882f1c40f8e0f6f1a343c30ae75f7c51bab Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Tue, 28 Jul 2026 13:22:38 +0200 Subject: [PATCH] iOS: Lagerort im Einzelstueck per QR scannen (ohne dessen eigenen QR) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Im Einzelstueck-Formular (bearbeiten und anlegen) gibt es jetzt neben dem Lagerort-Dropdown einen "Lagerort scannen"-Knopf: den QR am Regal/Fach scannen setzt den Ort direkt. So kann man z.B. Kleidung ohne eigenen Stueck-QR einem Ort zuordnen. Neue wiederverwendbare LocationScannerView (…/l/). Co-Authored-By: Claude Opus 4.8 --- ios/Sources/ItemViews.swift | 78 +++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/ios/Sources/ItemViews.swift b/ios/Sources/ItemViews.swift index 80655a9..8485b02 100644 --- a/ios/Sources/ItemViews.swift +++ b/ios/Sources/ItemViews.swift @@ -122,6 +122,66 @@ struct ProductItemsSection: View { // MARK: - Detail / Bearbeiten (auch Ziel eines Scans) +/// Lagerort per QR (…/l/) auswählen – selbstständige Scan-Ansicht, meldet +/// den getroffenen Lagerort zurück. So kann man einen Ort auch ohne den QR des +/// Einzelstücks setzen (z.B. bei Kleidung ohne eigenen Code). +struct LocationScannerView: View { + let locations: [StorageLocation] + var onPicked: (StorageLocation) -> Void + + @Environment(\.dismiss) private var dismiss + @State private var paused = false + @State private var torchOn = false + @State private var torchLocked = false + @State private var error: String? + + var body: some View { + NavigationStack { + ScannerView(onCode: { code in handle(code) }, + isPaused: $paused, torchOn: $torchOn, torchLocked: $torchLocked) + .ignoresSafeArea(edges: .bottom) + .overlay(alignment: .bottom) { + VStack(spacing: 6) { + if let error { + Text(error) + .font(.callout).foregroundStyle(.white) + .padding(.horizontal, 12).padding(.vertical, 8) + .background(.red, in: Capsule()) + } + Text("QR am Regal/Fach vor die Kamera halten") + .font(.callout).padding(10) + .background(.ultraThinMaterial, in: Capsule()) + } + .padding(.bottom, 24) + } + .navigationTitle("Lagerort scannen") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { dismiss() } } + ToolbarItem(placement: .topBarTrailing) { + TorchButton(isOn: $torchOn, locked: $torchLocked) + } + } + } + } + + private func handle(_ code: String) { + guard let r = code.range(of: "/l/") else { + error = "Das ist kein Lagerort-QR." + return + } + let ziel = String(code[r.upperBound...]) + .trimmingCharacters(in: CharacterSet(charactersIn: "/ ")) + .uppercased() + if let treffer = locations.first(where: { $0.id == ziel }) { + onPicked(treffer) + dismiss() + } else { + error = "Dieser Lagerort ist hier nicht bekannt." + } + } +} + struct ItemEditView: View { let item: Item var onChanged: (() -> Void)? = nil @@ -156,6 +216,7 @@ struct ItemEditView: View { @State private var showFileImporter = false @State private var pickerItem: PhotosPickerItem? @State private var previewURL: URL? + @State private var locScanShown = false var body: some View { Form { @@ -180,6 +241,11 @@ struct ItemEditView: View { Text("– ohne –").tag(String?.none) ForEach(locations) { l in Text(l.path(in: locations)).tag(String?.some(l.id)) } } + // Lagerort per QR setzen – ohne den (evtl. nicht vorhandenen) QR + // des Einzelstücks. Praktisch z.B. bei Kleidung. + Button { locScanShown = true } label: { + Label("Lagerort scannen", systemImage: "qrcode.viewfinder") + } Picker("Gekauft bei", selection: $shopId) { Text("– unbekannt –").tag(Int?.none) ForEach(shops) { s in Text(s.name).tag(Int?.some(s.id)) } @@ -267,6 +333,10 @@ struct ItemEditView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { Button("Fertig") { dismiss() } } } .task { await load() } + .fullScreenCover(isPresented: $locScanShown) { + LocationScannerView(locations: locations) { loc in locationId = loc.id } + .ignoresSafeArea() + } .sheet(isPresented: $showRemove) { NavigationStack { removeSheet } } @@ -465,6 +535,7 @@ struct ItemAddSheet: View { @State private var pickerItem: PhotosPickerItem? @State private var busy = false @State private var error: String? + @State private var locScanShown = false var body: some View { Form { @@ -474,6 +545,9 @@ struct ItemAddSheet: View { Text("– ohne –").tag(String?.none) ForEach(locations) { l in Text(l.path(in: locations)).tag(String?.some(l.id)) } } + Button { locScanShown = true } label: { + Label("Lagerort scannen", systemImage: "qrcode.viewfinder") + } Picker("Gekauft bei", selection: $shopId) { Text("– unbekannt –").tag(Int?.none) ForEach(shops) { s in Text(s.name).tag(Int?.some(s.id)) } @@ -529,6 +603,10 @@ struct ItemAddSheet: View { .navigationTitle("Einzelstücke anlegen") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { dismiss() } } } + .fullScreenCover(isPresented: $locScanShown) { + LocationScannerView(locations: locations) { loc in locationId = loc.id } + .ignoresSafeArea() + } .fileImporter(isPresented: $showFileImporter, allowedContentTypes: [.pdf]) { result in if case .success(let url) = result { Task {