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:
@@ -104,6 +104,11 @@ actor APIClient {
|
||||
try await send(try makeRequest("/products/\(id)"), as: Product.self)
|
||||
}
|
||||
|
||||
/// Alles, was an einem Lagerort (inkl. Unterorte) liegt – Ziel des /l/-QR beim Nachschlagen.
|
||||
func locationContents(code: String) async throws -> LocationContents {
|
||||
try await send(try makeRequest("/location/\(code)"), as: LocationContents.self)
|
||||
}
|
||||
|
||||
func lookup(barcode: String) async throws -> LookupResult {
|
||||
let escaped = barcode.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
|
||||
return try await send(try makeRequest("/products/lookup?barcode=\(escaped)"), as: LookupResult.self)
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,6 +405,39 @@ struct LocationNeeds: Codable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ein Artikel mit seinem Bestand an einem Lagerort (in Artikeleinheiten).
|
||||
struct LocationContentEntry: Codable, Identifiable, Hashable {
|
||||
let productId: Int
|
||||
let name: String
|
||||
let brand: String?
|
||||
let stock: Double
|
||||
let unitLabel: String
|
||||
let individual: Bool
|
||||
|
||||
var id: Int { productId }
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name, brand, stock, individual
|
||||
case productId = "product_id"
|
||||
case unitLabel = "unit_label"
|
||||
}
|
||||
}
|
||||
|
||||
/// Alles, was an einem gescannten Lagerort liegt – inklusive der Unterorte.
|
||||
struct LocationContents: Codable, Identifiable {
|
||||
let locationId: String
|
||||
let locationName: String
|
||||
let products: [LocationContentEntry]
|
||||
|
||||
var id: String { locationId }
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case products
|
||||
case locationId = "location_id"
|
||||
case locationName = "location_name"
|
||||
}
|
||||
}
|
||||
|
||||
struct ExpiringItem: Codable, Identifiable {
|
||||
let lotId: Int
|
||||
let productId: Int
|
||||
|
||||
Reference in New Issue
Block a user