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:
Scarriffle
2026-07-27 12:15:25 +02:00
parent 6e48cd0a7a
commit d518b4aa23
23 changed files with 507 additions and 220 deletions

View File

@@ -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)
}