Lagerorte per 10-Zeichen-Code statt fortlaufender ID; iOS-Einlagern ohne Kamerazwang
Lagerort-IDs sind jetzt ein zufaelliger 10-Zeichen-Code (wie die Einzelstueck-UIDs) statt einer fortlaufenden Zahl - so kollidiert die Stammdaten-Sicherung zwischen zwei Instanzen praktisch nie mehr, und der Code ist zugleich der Inhalt des QR /l/<code>. Alle Fremdschluessel (lots, movements, items, Mindestbestaende, parent_id) ziehen mit; die Umstellung laeuft einmalig und transaktional beim Serverstart (_migrate_locations_to_code) und rollt bei Fehlern komplett zurueck. Vor dem Deploy ein DB-Backup machen. iOS-Einlagern oeffnet nicht mehr sofort die Kamera, sondern ein Formular mit Artikelsuche; die Kamera kommt erst per Button. Im Formular laesst sich der Lagerort zusaetzlich per /l/-QR scannen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -153,10 +153,18 @@ struct CheckInFormView: View {
|
||||
/// Charge, fuer die gerade das MHD abgescannt wird.
|
||||
@State private var scanLineId: LineRef?
|
||||
@State private var unit: String = ""
|
||||
@State private var locationId: Int?
|
||||
@State private var locationId: String?
|
||||
@State private var busy = false
|
||||
@State private var error: String?
|
||||
|
||||
// Lagerort per QR (…/l/<Code>) setzen, statt in der Liste zu suchen.
|
||||
@State private var locScanShown = false
|
||||
@State private var locScanPaused = false
|
||||
@State private var locTorchOn = false
|
||||
@State private var locTorchLocked = false
|
||||
@State private var locScanError: String?
|
||||
@State private var locScanHinweis: String?
|
||||
|
||||
/// Einheiten der passenden Art plus das Gebinde des Artikels.
|
||||
private var unitOptions: [UnitOption] {
|
||||
var options = units
|
||||
@@ -189,13 +197,31 @@ struct CheckInFormView: View {
|
||||
Picker("Einheit", selection: $unit) {
|
||||
ForEach(unitOptions) { Text($0.label).tag($0.value) }
|
||||
}
|
||||
if !locations.isEmpty {
|
||||
}
|
||||
|
||||
if !locations.isEmpty {
|
||||
Section {
|
||||
Picker("Lagerort", selection: $locationId) {
|
||||
Text("– keiner –").tag(Int?.none)
|
||||
Text("– keiner –").tag(String?.none)
|
||||
ForEach(locations) { location in
|
||||
Text(location.name).tag(Int?.some(location.id))
|
||||
Text(location.name).tag(String?.some(location.id))
|
||||
}
|
||||
}
|
||||
Button {
|
||||
locScanError = nil
|
||||
locScanPaused = false
|
||||
locScanShown = true
|
||||
} label: {
|
||||
Label("Lagerort-QR scannen", systemImage: "qrcode.viewfinder")
|
||||
}
|
||||
if let locScanHinweis {
|
||||
Label(locScanHinweis, systemImage: "checkmark.circle.fill")
|
||||
.font(.caption).foregroundStyle(.green)
|
||||
}
|
||||
} header: {
|
||||
Text("Lagerort")
|
||||
} footer: {
|
||||
Text("Den QR am Regal/Fach scannen, statt den Ort in der Liste zu suchen.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,6 +307,7 @@ struct CheckInFormView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $locScanShown) { locationScannerCover }
|
||||
.onAppear {
|
||||
// Gibt es ein Gebinde, ist das die naheliegende Eingabe.
|
||||
unit = hasPackage ? UnitOption.packageValue : product.unitName
|
||||
@@ -289,6 +316,60 @@ struct CheckInFormView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Lagerort scannen
|
||||
|
||||
private var locationScannerCover: some View {
|
||||
NavigationStack {
|
||||
ScannerView(onCode: { code in Task { await handleLocationScan(code) } },
|
||||
isPaused: $locScanPaused, torchOn: $locTorchOn, torchLocked: $locTorchLocked)
|
||||
.ignoresSafeArea(edges: .bottom)
|
||||
.overlay(alignment: .bottom) {
|
||||
VStack(spacing: 6) {
|
||||
if let locScanError {
|
||||
Text(locScanError)
|
||||
.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") { locScanShown = false }
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
TorchButton(isOn: $locTorchOn, locked: $locTorchLocked)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear { locScanPaused = false; locScanError = nil }
|
||||
}
|
||||
|
||||
/// Aus einem gescannten Lagerort-QR (…/l/<Code>) den Zielort setzen.
|
||||
private func handleLocationScan(_ code: String) async {
|
||||
guard let r = code.range(of: "/l/") else {
|
||||
locScanError = "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 }) {
|
||||
locationId = treffer.id
|
||||
locScanHinweis = treffer.name
|
||||
locScanShown = false
|
||||
} else {
|
||||
locScanError = "Dieser Lagerort ist hier nicht bekannt."
|
||||
}
|
||||
}
|
||||
|
||||
private func format(_ value: Double) -> String {
|
||||
value == value.rounded() ? String(Int(value)) : String(format: "%.2f", value)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user