Files
Vorrania/ios/Sources/CheckInFormView.swift
Scarriffle 4bb9a10366 iOS-App angefangen; Import-Modi; MHD-Formatierung; Gebinde in Ablauf-Ansichten
iOS (neu, ios/):
- SwiftUI-App: Login (Server + Keychain-Token), Kamera-Scanner (EAN/UPC/Code128),
  Einlagern mit mehreren Chargen und eigenem MHD, Auslagern mit Chargenauswahl
  oder FEFO, Artikelsuche, Anlegen mit Open-Food-Facts-Vorbefuellung.
- Home-Screen-Shortcuts: Schnellaktionen (langer Druck) und URL-Schema
  projectgood://checkin | ://checkout fuer eigene Symbole via Kurzbefehle.
- project.yml (XcodeGen) + README mit Build-Anleitung. NICHT kompiliert - auf
  diesem Rechner ist kein Xcode vorhanden.

Import-Modi (wie besprochen sinnvoll):
- add (Standard, nichts loeschen), replace_listed (Bestaende der in der Datei
  genannten Produkte ersetzen - fuer Inventur), replace_all (alles ersetzen).
  Geleerte Bestaende werden als Korrektur-Bewegung protokolliert; die Oberflaeche
  fragt bei den zerstoerenden Modi nach.

Anzeige:
- "Bald ablaufend"/"Abgelaufen" zeigen jetzt das Gebinde ("1 Glas") mit der
  Basiseinheit klein darunter (ExpiringItem liefert die Einheiten mit).
- MHD als Zeitspanne mit korrektem Numerus: "in 3 Tagen", "in 1 Woche",
  "vor 2 Wochen", dazu heute/morgen/gestern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 14:59:04 +02:00

150 lines
5.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 {
TextField("Menge", text: $line.quantity)
.keyboardType(.decimalPad)
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
}
}
}