iOS: Lagerort im Einzelstueck per QR scannen (ohne dessen eigenen QR)

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/<Code>).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-28 13:22:38 +02:00
parent 03c0f4043d
commit d029c882f1

View File

@@ -122,6 +122,66 @@ struct ProductItemsSection: View {
// MARK: - Detail / Bearbeiten (auch Ziel eines Scans)
/// Lagerort per QR (/l/<Code>) 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 {