Beim Einlagern stand in der Chargenzeile eine nackte 1 ohne Beschriftung. Das las sich wie eine Nummerierung der Charge, war aber das Mengenfeld. Die Zeile hat jetzt links die Beschriftung "Menge", der Wert steht rechtsbuendig; die 1 bleibt als Vorgabe, weil meistens genau ein Stueck eingelagert wird. Beim Auslagern war dieselbe Zeile ebenso unbeschriftet und ist jetzt gleich aufgebaut. Anmeldung: Das Token wanderte bisher immer dauerhaft in den Schluesselbund. Der neue Haken "Angemeldet bleiben" laesst sich abwaehlen - dann gilt die Anmeldung nur, solange die App laeuft, und es bleibt nichts auf dem Geraet zurueck. Die Wahl wird gemerkt, damit der Haken beim naechsten Mal richtig steht. Dabei ist aufgefallen, dass der Benutzername nirgends gespeichert wurde: Nach einem Neustart stand im Menue nur noch "Angemeldet als" ohne Namen. Er wird jetzt zusammen mit dem Token abgelegt und beim Abmelden wieder entfernt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
221 lines
7.6 KiB
Swift
221 lines
7.6 KiB
Swift
import SwiftUI
|
|
|
|
struct CheckOutView: View {
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
@State private var paused = false
|
|
@State private var torchOn = 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 {
|
|
ZStack(alignment: .bottom) {
|
|
ScannerView(onCode: { code in Task { await resolve(code) } },
|
|
isPaused: $paused, torchOn: $torchOn)
|
|
.ignoresSafeArea()
|
|
|
|
VStack(spacing: 10) {
|
|
if let status { banner(status, color: .green) }
|
|
if let error { banner(error, color: .red) }
|
|
|
|
HStack(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(.bottom, 8)
|
|
}
|
|
}
|
|
.navigationTitle("Auslagern")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarLeading) { Button("Fertig") { dismiss() } }
|
|
ToolbarItem(placement: .topBarTrailing) { TorchButton(isOn: $torchOn) }
|
|
}
|
|
.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
|
|
|
|
@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: [String] {
|
|
var options = units.filter { $0.kind == product.kind }.map(\.name)
|
|
if let size = product.packageSize, size > 0 {
|
|
options.append(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") {
|
|
HStack {
|
|
Text("Menge")
|
|
Spacer(minLength: 12)
|
|
TextField("Menge", text: $quantity)
|
|
.keyboardType(.decimalPad)
|
|
.multilineTextAlignment(.trailing)
|
|
.frame(maxWidth: 110)
|
|
}
|
|
Picker("Einheit", selection: $unit) {
|
|
ForEach(unitOptions, id: \.self) { Text($0).tag($0) }
|
|
}
|
|
}
|
|
|
|
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 ? (product.packageLabel ?? "Packung") : 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)
|
|
let date = lot.bestBefore ?? "ohne MHD"
|
|
return "\(date) · \(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. Neuer Bestand: \(format(total)) \(product.articleUnitLabel)")
|
|
dismiss()
|
|
} catch {
|
|
self.error = error.localizedDescription
|
|
}
|
|
}
|
|
}
|