Files
Vorrania/ios/Sources/LookupScanView.swift
Scarriffle 21ab0a825a Nachschlagen: Lagerort-QR zeigt alle Artikel an diesem Ort
Neuer Endpunkt GET /location/{code} liefert alle Artikel an einem Lagerort inkl. der Unterorte (Lot-Bestaende und Einzelstuecke), sortiert nach Name mit Menge in Artikeleinheiten. In der App loest der /l/-QR beim Nachschlagen jetzt diese Liste auf; ein Tipp auf einen Eintrag oeffnet den Artikel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 13:10:29 +02:00

164 lines
6.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
/// Nur ansehen: Barcode Artikel, Einzelstück-QR genau dieses Stück,
/// Lagerort-QR Liste aller Artikel an diesem Ort. Kein Ein-/Auslagern.
struct LookupScanView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var display: DisplaySettings
@State private var paused = false
@State private var torchOn = false
@State private var torchLocked = false
@State private var error: String?
@State private var item: Item?
@State private var product: Product?
@State private var contents: LocationContents?
var body: some View {
ScrollView {
VStack(spacing: 14) {
ScannerView(onCode: { code in Task { await resolve(code) } },
isPaused: $paused, torchOn: $torchOn, torchLocked: $torchLocked)
.aspectRatio(4.0 / 3.0, contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 16))
.padding(.horizontal)
Text("Barcode, Einzelstück- oder Lagerort-QR scannen nur ansehen")
.font(.caption).foregroundStyle(.secondary)
.multilineTextAlignment(.center)
if let error {
Text(error).font(.callout).foregroundStyle(.red)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
}
}
.padding(.vertical)
}
.navigationTitle("Nachschlagen")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarLeading) { Button("Fertig") { dismiss() } }
ToolbarItem(placement: .topBarTrailing) { TorchButton(isOn: $torchOn, locked: $torchLocked) }
}
.sheet(item: $item, onDismiss: { paused = false }) { it in
NavigationStack { ItemEditView(item: it) }
}
.sheet(item: $product, onDismiss: { paused = false }) { p in
NavigationStack { ProductDetailView(product: p) }
.environmentObject(display)
}
.sheet(item: $contents, onDismiss: { paused = false }) { c in
NavigationStack { LocationContentsView(contents: c) }
.environmentObject(display)
}
}
private func resolve(_ code: String) async {
paused = true
error = nil
// Einzelstück-QR (/i/<UID>)?
if let r = code.range(of: "/i/") {
let uid = String(code[r.upperBound...])
.trimmingCharacters(in: CharacterSet(charactersIn: "/ ")).uppercased()
if !uid.isEmpty, let found = try? await APIClient.shared.itemByUid(uid: uid) {
item = found
return
}
}
// Lagerort-QR (/l/<Code>)? alles zeigen, was dort (inkl. Unterorte) liegt.
if let r = code.range(of: "/l/") {
let loc = String(code[r.upperBound...])
.trimmingCharacters(in: CharacterSet(charactersIn: "/ ")).uppercased()
if !loc.isEmpty {
do {
contents = try await APIClient.shared.locationContents(code: loc)
} catch {
self.error = "Lagerort nicht gefunden: \(loc)"
paused = false
}
return
}
}
// Sonst als Barcode auflösen.
do {
let result = try await APIClient.shared.lookup(barcode: code)
if let existing = result.existingProduct {
product = existing
} else {
error = "Nicht im Katalog: \(code)"
paused = false
}
} catch {
self.error = error.localizedDescription
paused = false
}
}
}
/// Liste aller Artikel eines gescannten Lagerorts (inkl. Unterorte).
struct LocationContentsView: View {
let contents: LocationContents
@EnvironmentObject private var display: DisplaySettings
var body: some View {
List {
if contents.products.isEmpty {
Text("Hier ist nichts eingelagert.").foregroundStyle(.secondary)
} else {
Section {
ForEach(contents.products) { e in
NavigationLink {
ProductLoaderView(productId: e.productId).environmentObject(display)
} label: {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(e.name)
if let brand = e.brand, !brand.isEmpty {
Text(brand).font(.caption).foregroundStyle(.secondary)
}
}
Spacer()
Text("\(mengeText(e.stock)) \(e.unitLabel)")
.foregroundStyle(.secondary)
}
}
}
} footer: {
Text("Bestand inkl. der Unterorte. Tippen öffnet den Artikel.")
}
}
}
.navigationTitle(contents.locationName)
.navigationBarTitleDisplayMode(.inline)
}
private func mengeText(_ v: Double) -> String {
v == v.rounded() ? String(Int(v)) : String(format: "%.2f", v)
}
}
/// Lädt einen Artikel per ID nach und zeigt dann seine Detailansicht.
struct ProductLoaderView: View {
let productId: Int
@EnvironmentObject private var display: DisplaySettings
@State private var product: Product?
@State private var error: String?
var body: some View {
Group {
if let product {
ProductDetailView(product: product).environmentObject(display)
} else if let error {
Text(error).foregroundStyle(.red).padding()
} else {
ProgressView()
}
}
.task {
do { product = try await APIClient.shared.product(id: productId) }
catch { self.error = error.localizedDescription }
}
}
}