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:
@@ -122,6 +122,66 @@ struct ProductItemsSection: View {
|
|||||||
|
|
||||||
// MARK: - Detail / Bearbeiten (auch Ziel eines Scans)
|
// 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 {
|
struct ItemEditView: View {
|
||||||
let item: Item
|
let item: Item
|
||||||
var onChanged: (() -> Void)? = nil
|
var onChanged: (() -> Void)? = nil
|
||||||
@@ -156,6 +216,7 @@ struct ItemEditView: View {
|
|||||||
@State private var showFileImporter = false
|
@State private var showFileImporter = false
|
||||||
@State private var pickerItem: PhotosPickerItem?
|
@State private var pickerItem: PhotosPickerItem?
|
||||||
@State private var previewURL: URL?
|
@State private var previewURL: URL?
|
||||||
|
@State private var locScanShown = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
@@ -180,6 +241,11 @@ struct ItemEditView: View {
|
|||||||
Text("– ohne –").tag(String?.none)
|
Text("– ohne –").tag(String?.none)
|
||||||
ForEach(locations) { l in Text(l.path(in: locations)).tag(String?.some(l.id)) }
|
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) {
|
Picker("Gekauft bei", selection: $shopId) {
|
||||||
Text("– unbekannt –").tag(Int?.none)
|
Text("– unbekannt –").tag(Int?.none)
|
||||||
ForEach(shops) { s in Text(s.name).tag(Int?.some(s.id)) }
|
ForEach(shops) { s in Text(s.name).tag(Int?.some(s.id)) }
|
||||||
@@ -267,6 +333,10 @@ struct ItemEditView: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar { ToolbarItem(placement: .topBarLeading) { Button("Fertig") { dismiss() } } }
|
.toolbar { ToolbarItem(placement: .topBarLeading) { Button("Fertig") { dismiss() } } }
|
||||||
.task { await load() }
|
.task { await load() }
|
||||||
|
.fullScreenCover(isPresented: $locScanShown) {
|
||||||
|
LocationScannerView(locations: locations) { loc in locationId = loc.id }
|
||||||
|
.ignoresSafeArea()
|
||||||
|
}
|
||||||
.sheet(isPresented: $showRemove) {
|
.sheet(isPresented: $showRemove) {
|
||||||
NavigationStack { removeSheet }
|
NavigationStack { removeSheet }
|
||||||
}
|
}
|
||||||
@@ -465,6 +535,7 @@ struct ItemAddSheet: View {
|
|||||||
@State private var pickerItem: PhotosPickerItem?
|
@State private var pickerItem: PhotosPickerItem?
|
||||||
@State private var busy = false
|
@State private var busy = false
|
||||||
@State private var error: String?
|
@State private var error: String?
|
||||||
|
@State private var locScanShown = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
@@ -474,6 +545,9 @@ struct ItemAddSheet: View {
|
|||||||
Text("– ohne –").tag(String?.none)
|
Text("– ohne –").tag(String?.none)
|
||||||
ForEach(locations) { l in Text(l.path(in: locations)).tag(String?.some(l.id)) }
|
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) {
|
Picker("Gekauft bei", selection: $shopId) {
|
||||||
Text("– unbekannt –").tag(Int?.none)
|
Text("– unbekannt –").tag(Int?.none)
|
||||||
ForEach(shops) { s in Text(s.name).tag(Int?.some(s.id)) }
|
ForEach(shops) { s in Text(s.name).tag(Int?.some(s.id)) }
|
||||||
@@ -529,6 +603,10 @@ struct ItemAddSheet: View {
|
|||||||
.navigationTitle("Einzelstücke anlegen")
|
.navigationTitle("Einzelstücke anlegen")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar { ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { dismiss() } } }
|
.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
|
.fileImporter(isPresented: $showFileImporter, allowedContentTypes: [.pdf]) { result in
|
||||||
if case .success(let url) = result {
|
if case .success(let url) = result {
|
||||||
Task {
|
Task {
|
||||||
|
|||||||
Reference in New Issue
Block a user