import SwiftUI struct CheckOutView: View { @Environment(\.dismiss) private var dismiss @State private var paused = false @State private var torchOn = false @State private var torchLocked = false @State private var status: String? @State private var error: String? @State private var product: Product? @State private var units: [Unit] = [] @State private var manualCodeShown = false @State private var manualCode = "" @State private var searchShown = false var body: some View { // Kamera als Feld oben, siehe CheckInView. 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 vor die Kamera halten") .font(.caption).foregroundStyle(.secondary) if let status { banner(status, color: .green) } if let error { banner(error, color: .red) } VStack(spacing: 10) { Button { paused = true searchShown = true } label: { Label("Artikel suchen", systemImage: "magnifyingglass") .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) Button { paused = true manualCodeShown = true } label: { Label("EAN manuell", systemImage: "keyboard") .frame(maxWidth: .infinity) } .buttonStyle(.bordered) } .padding(.horizontal) } .padding(.vertical) } .navigationTitle("Auslagern") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { Button("Fertig") { dismiss() } } ToolbarItem(placement: .topBarTrailing) { TorchButton(isOn: $torchOn, locked: $torchLocked) } } .task { units = (try? await APIClient.shared.units()) ?? [] } .alert("EAN eingeben", isPresented: $manualCodeShown) { TextField("z.B. 8076809572569", text: $manualCode) .keyboardType(.numberPad) Button("Suchen") { let code = manualCode manualCode = "" Task { await resolve(code) } } Button("Abbrechen", role: .cancel) { paused = false } } .sheet(isPresented: $searchShown) { NavigationStack { ProductSearchView { picked in searchShown = false product = picked } } } .sheet(item: $product) { item in NavigationStack { CheckOutFormView(product: item, units: units) { message in status = message product = nil paused = false } } } } private func banner(_ text: String, color: Color) -> some View { Text(text) .font(.callout) .padding(10) .frame(maxWidth: .infinity, alignment: .leading) .background(color.opacity(0.9)) .foregroundStyle(.white) .clipShape(RoundedRectangle(cornerRadius: 10)) .padding(.horizontal) } private func resolve(_ code: String) async { paused = true error = nil status = nil do { let result = try await APIClient.shared.lookup(barcode: code) if let existing = result.existingProduct { product = existing } else { error = "Kein bekanntes Produkt zu diesem Code im Lager." paused = false } } catch { self.error = error.localizedDescription paused = false } } } /// Auslagern: Menge, Einheit und optional eine bestimmte Charge (sonst FEFO). struct CheckOutFormView: View { let product: Product let units: [Unit] var onDone: (String) -> Void @Environment(\.dismiss) private var dismiss @EnvironmentObject private var display: DisplaySettings @State private var quantity = "1" @State private var unit = "" @State private var lots: [Lot] = [] @State private var lotId: Int? @State private var busy = false @State private var error: String? private var unitOptions: [UnitOption] { var options = units .filter { $0.kind == product.kind } .map { UnitOption(value: $0.name, label: $0.name) } if let size = product.packageSize, size > 0 { // Angezeigt wird "Glas"/"Dose", geschickt wird "package". options.append(UnitOption(value: UnitOption.packageValue, label: product.packageLabel ?? "Packung")) } return options } var body: some View { Form { Section { VStack(alignment: .leading) { Text(product.name).font(.headline) Text("Verfügbar: \(format(product.stockInArticleUnits)) \(product.articleUnitLabel)") .font(.caption).foregroundStyle(.secondary) } } Section("Charge") { Picker("Charge", selection: $lotId) { Text("Automatisch (zuerst ablaufende)").tag(Int?.none) ForEach(lots) { lot in Text(label(for: lot)).tag(Int?.some(lot.id)) } } } Section("Menge") { QuantityField(label: "Menge", text: $quantity) Picker("Einheit", selection: $unit) { ForEach(unitOptions) { Text($0.label).tag($0.value) } } } if let error { Section { Text(error).foregroundStyle(.red).font(.callout) } } Section { Button(busy ? "Speichern…" : "Auslagern") { Task { await submit() } } .disabled(busy) } } .navigationTitle("Auslagern") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { dismiss() } } } .task { unit = (product.packageSize ?? 0) > 0 ? UnitOption.packageValue : product.unitName lots = (try? await APIClient.shared.lots(productId: product.id)) ?? [] } } private func label(for lot: Lot) -> String { let amount = format(lot.quantity / product.articleUnitFactor) return "\(display.formatBestBefore(lot.bestBefore, precision: lot.bestBeforePrecision)) · \(amount) \(product.articleUnitLabel)" } private func format(_ value: Double) -> String { value == value.rounded() ? String(Int(value)) : String(format: "%.2f", value) } private func submit() async { error = nil guard let amount = Double(quantity.replacingOccurrences(of: ",", with: ".")), amount > 0 else { error = "Bitte eine Menge größer 0 angeben." return } busy = true defer { busy = false } do { let response = try await APIClient.shared.checkOut( CheckOutRequest(productId: product.id, quantity: amount, unit: unit, lotId: lotId) ) let total = response.productStock / product.articleUnitFactor onDone("Ausgelagert\nNeuer Bestand: \(format(total)) \(display.articleUnit(product, count: total))") dismiss() } catch { self.error = error.localizedDescription } } }