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>
This commit is contained in:
Scarriffle
2026-07-27 13:10:29 +02:00
parent 4607e3a6a8
commit 21ab0a825a
6 changed files with 258 additions and 5 deletions

View File

@@ -1,7 +1,7 @@
import SwiftUI
/// Nur ansehen: Barcode Artikel, Einzelstück-QR genau dieses Stück.
/// Kein Ein-/Auslagern zum schnellen Nachschauen (z.B. wann gekauft, Garantie).
/// 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
@@ -12,6 +12,7 @@ struct LookupScanView: View {
@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 {
@@ -22,8 +23,9 @@ struct LookupScanView: View {
.clipShape(RoundedRectangle(cornerRadius: 16))
.padding(.horizontal)
Text("Barcode oder Einzelstück-QR scannen nur ansehen")
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)
@@ -46,6 +48,10 @@ struct LookupScanView: View {
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 {
@@ -60,6 +66,20 @@ struct LookupScanView: View {
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)
@@ -75,3 +95,69 @@ struct LookupScanView: View {
}
}
}
/// 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 }
}
}
}