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>
156 lines
5.6 KiB
Swift
156 lines
5.6 KiB
Swift
import SwiftUI
|
||
|
||
/// Mengen-Erfassung nach dem Scan: mehrere Chargen mit eigenem MHD.
|
||
struct CheckInFormView: View {
|
||
let product: Product
|
||
let units: [Unit]
|
||
let locations: [StorageLocation]
|
||
var onDone: (String) -> Void
|
||
|
||
@Environment(\.dismiss) private var dismiss
|
||
|
||
struct Line: Identifiable {
|
||
let id = UUID()
|
||
var quantity: String = "1"
|
||
var hasDate: Bool = false
|
||
var bestBefore: Date = Date()
|
||
}
|
||
|
||
@State private var lines: [Line] = [Line()]
|
||
@State private var unit: String = ""
|
||
@State private var locationId: Int?
|
||
@State private var busy = false
|
||
@State private var error: String?
|
||
|
||
/// Einheiten der passenden Art plus das Gebinde des Artikels.
|
||
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
|
||
}
|
||
|
||
private var packageOptionName: String? {
|
||
guard let size = product.packageSize, size > 0 else { return nil }
|
||
return product.packageLabel ?? "Packung"
|
||
}
|
||
|
||
var body: some View {
|
||
Form {
|
||
Section {
|
||
HStack {
|
||
VStack(alignment: .leading) {
|
||
Text(product.name).font(.headline)
|
||
Text("Bestand: \(format(product.stockInArticleUnits)) \(product.articleUnitLabel)")
|
||
.font(.caption).foregroundStyle(.secondary)
|
||
}
|
||
Spacer()
|
||
}
|
||
}
|
||
|
||
Section("Einheit") {
|
||
Picker("Einheit", selection: $unit) {
|
||
ForEach(unitOptions, id: \.self) { Text($0).tag($0) }
|
||
}
|
||
if !locations.isEmpty {
|
||
Picker("Lagerort", selection: $locationId) {
|
||
Text("– keiner –").tag(Int?.none)
|
||
ForEach(locations) { location in
|
||
Text(location.name).tag(Int?.some(location.id))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Section("Chargen") {
|
||
ForEach($lines) { $line in
|
||
VStack(alignment: .leading, spacing: 6) {
|
||
HStack {
|
||
// Ohne Beschriftung sah die vorbelegte 1 wie eine
|
||
// Nummerierung der Charge aus.
|
||
Text("Menge")
|
||
Spacer(minLength: 12)
|
||
TextField("Menge", text: $line.quantity)
|
||
.keyboardType(.decimalPad)
|
||
.multilineTextAlignment(.trailing)
|
||
.frame(maxWidth: 110)
|
||
if lines.count > 1 {
|
||
Button(role: .destructive) {
|
||
lines.removeAll { $0.id == line.id }
|
||
} label: { Image(systemName: "trash") }
|
||
.buttonStyle(.borderless)
|
||
}
|
||
}
|
||
Toggle("MHD angeben", isOn: $line.hasDate)
|
||
if line.hasDate {
|
||
DatePicker("MHD", selection: $line.bestBefore, displayedComponents: .date)
|
||
}
|
||
}
|
||
.padding(.vertical, 2)
|
||
}
|
||
Button {
|
||
lines.append(Line())
|
||
} label: {
|
||
Label("Weitere Charge (anderes MHD)", systemImage: "plus")
|
||
}
|
||
}
|
||
|
||
if let error {
|
||
Section { Text(error).foregroundStyle(.red).font(.callout) }
|
||
}
|
||
|
||
Section {
|
||
Button(busy ? "Speichern…" : "Einlagern") { Task { await submit() } }
|
||
.disabled(busy)
|
||
}
|
||
}
|
||
.navigationTitle("Einlagern")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { dismiss() } }
|
||
}
|
||
.onAppear {
|
||
// Gibt es ein Gebinde, ist das die naheliegende Eingabe.
|
||
unit = packageOptionName ?? product.unitName
|
||
}
|
||
}
|
||
|
||
private func format(_ value: Double) -> String {
|
||
value == value.rounded() ? String(Int(value)) : String(format: "%.2f", value)
|
||
}
|
||
|
||
private func submit() async {
|
||
error = nil
|
||
let formatter = DateFormatter()
|
||
formatter.dateFormat = "yyyy-MM-dd"
|
||
|
||
let payloadLines: [CheckInLine] = lines.compactMap { line in
|
||
guard let quantity = Double(line.quantity.replacingOccurrences(of: ",", with: ".")),
|
||
quantity > 0 else { return nil }
|
||
return CheckInLine(
|
||
quantity: quantity,
|
||
bestBefore: line.hasDate ? formatter.string(from: line.bestBefore) : nil,
|
||
locationId: locationId
|
||
)
|
||
}
|
||
guard !payloadLines.isEmpty else {
|
||
error = "Bitte mindestens eine Menge angeben."
|
||
return
|
||
}
|
||
|
||
busy = true
|
||
defer { busy = false }
|
||
do {
|
||
let response = try await APIClient.shared.checkInBatch(
|
||
BatchCheckInRequest(productId: product.id, unit: unit, lines: payloadLines)
|
||
)
|
||
let total = response.productStock / product.articleUnitFactor
|
||
onDone("Eingelagert. Neuer Bestand: \(format(total)) \(product.articleUnitLabel)")
|
||
dismiss()
|
||
} catch {
|
||
self.error = error.localizedDescription
|
||
}
|
||
}
|
||
}
|