In der Chargenzeile beim Einlagern gibt es einen Knopf "MHD scannen". Er oeffnet die Texterkennung von VisionKit; erkannte Daten erscheinen als Vorschlag und werden erst uebernommen, wenn man sie antippt. Bewusst kein automatisches Uebernehmen: Eine Fehllesung beim MHD faellt sonst erst auf, wenn die Ware bereits falsch einsortiert ist. Der Parser deckt die Schreibweisen ab, die auf Verpackungen vorkommen - Tagesdaten (12.09.2026, 2026-09-12, 12/09/2026, 12.09.26) und Monatsangaben (09/2026, 09.2026, 2026-09, 09/26). Erkennt der Aufdruck nur Monat und Jahr, stellt sich die Eingabe sichtbar auf Monat/Jahr um. Damit aus Chargennummern und EAN-Codes keine Phantasiedaten werden, greifen mehrere Einschraenkungen: Muster mit Tag werden zuerst geprueft, ein bereits erkannter Textabschnitt wird nicht erneut ausgewertet, Monate ausserhalb 1-12 entfallen und das Jahr muss im Fenster von fuenf Jahren rueckwaerts bis dreissig vorwaerts liegen. Der Knopf erscheint nur, wenn das Geraet Texterkennung unterstuetzt (Neural Engine); sonst bleibt die Eingabe von Hand. Getestet: Der Parser wurde eigenstaendig uebersetzt und gegen 14 Faelle geprueft, alle korrekt - darunter, dass "12.09.2026" nicht als Monatsangabe gelesen wird, dass eine EAN keine Treffer erzeugt und dass unplausible Jahre und Monate verworfen werden. Der iOS-Geraetebuild laeuft fehlerfrei durch. Die Erkennung an einer echten Verpackung habe ich nicht selbst ausprobiert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
320 lines
8.0 KiB
Swift
320 lines
8.0 KiB
Swift
import Foundation
|
|
|
|
// Entspricht den Schemas des Backends (backend/app/schemas.py).
|
|
|
|
struct TokenResponse: Codable {
|
|
let accessToken: String
|
|
let role: String
|
|
let username: String
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case accessToken = "access_token"
|
|
case role, username
|
|
}
|
|
}
|
|
|
|
struct MeResponse: Codable {
|
|
let id: Int
|
|
let username: String
|
|
let role: String
|
|
|
|
var isAdmin: Bool { role == "admin" }
|
|
}
|
|
|
|
struct Unit: Codable, Identifiable, Hashable {
|
|
let id: Int
|
|
let name: String
|
|
let kind: String
|
|
let factor: Double
|
|
}
|
|
|
|
struct BarcodeEntry: Codable, Identifiable, Hashable {
|
|
let id: Int
|
|
let code: String
|
|
let note: String?
|
|
}
|
|
|
|
struct Product: Codable, Identifiable, Hashable {
|
|
let id: Int
|
|
let barcode: String?
|
|
let name: String
|
|
let brand: String?
|
|
let imageUrl: String?
|
|
let baseUnit: String
|
|
let packageSize: Double?
|
|
let packageLabel: String?
|
|
let groupId: Int?
|
|
let minStock: Double?
|
|
let stock: Double
|
|
let expiredCount: Int
|
|
let kind: String
|
|
let unitName: String
|
|
let unitFactor: Double
|
|
let barcodes: [BarcodeEntry]
|
|
/// Welche MHD-Genauigkeit bei diesem Produkt ueblich ist ("day"/"month").
|
|
let datePrecision: String?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, barcode, name, brand, stock, kind, barcodes
|
|
case datePrecision = "date_precision"
|
|
case imageUrl = "image_url"
|
|
case baseUnit = "base_unit"
|
|
case packageSize = "package_size"
|
|
case packageLabel = "package_label"
|
|
case groupId = "group_id"
|
|
case minStock = "min_stock"
|
|
case expiredCount = "expired_count"
|
|
case unitName = "unit_name"
|
|
case unitFactor = "unit_factor"
|
|
}
|
|
|
|
/// Bezeichnung der Artikeleinheit (Gebinde, sonst Produkteinheit).
|
|
var articleUnitLabel: String {
|
|
if let size = packageSize, size > 0 { return packageLabel ?? "Packung" }
|
|
return unitName
|
|
}
|
|
|
|
/// Faktor, um Basiseinheiten in Artikeleinheiten umzurechnen.
|
|
var articleUnitFactor: Double {
|
|
if let size = packageSize, size > 0 { return size }
|
|
return unitFactor == 0 ? 1 : unitFactor
|
|
}
|
|
|
|
var stockInArticleUnits: Double { stock / articleUnitFactor }
|
|
}
|
|
|
|
struct LookupResult: Codable {
|
|
let found: Bool
|
|
let existingProduct: Product?
|
|
let suggestion: Suggestion?
|
|
let groupId: Int?
|
|
let groupName: String?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case found, suggestion
|
|
case existingProduct = "existing_product"
|
|
case groupId = "group_id"
|
|
case groupName = "group_name"
|
|
}
|
|
|
|
struct Suggestion: Codable {
|
|
let barcode: String?
|
|
let name: String
|
|
let brand: String?
|
|
let imageUrl: String?
|
|
let baseUnit: String?
|
|
let packageSize: Double?
|
|
let quantityText: String?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case barcode, name, brand
|
|
case imageUrl = "image_url"
|
|
case baseUnit = "base_unit"
|
|
case packageSize = "package_size"
|
|
case quantityText = "quantity_text"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Lot: Codable, Identifiable, Hashable {
|
|
let id: Int
|
|
let productId: Int
|
|
let quantity: Double
|
|
let bestBefore: String?
|
|
let bestBeforePrecision: String?
|
|
let locationId: Int?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, quantity
|
|
case productId = "product_id"
|
|
case bestBefore = "best_before"
|
|
case bestBeforePrecision = "best_before_precision"
|
|
case locationId = "location_id"
|
|
}
|
|
}
|
|
|
|
struct StorageLocation: Codable, Identifiable, Hashable {
|
|
let id: Int
|
|
let name: String
|
|
let parentId: Int?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, name
|
|
case parentId = "parent_id"
|
|
}
|
|
}
|
|
|
|
struct CheckInLine: Codable {
|
|
let quantity: Double
|
|
let bestBefore: String?
|
|
let bestBeforePrecision: String
|
|
let locationId: Int?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case quantity
|
|
case bestBefore = "best_before"
|
|
case bestBeforePrecision = "best_before_precision"
|
|
case locationId = "location_id"
|
|
}
|
|
}
|
|
|
|
struct BatchCheckInRequest: Codable {
|
|
let productId: Int
|
|
let unit: String
|
|
let lines: [CheckInLine]
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case unit, lines
|
|
case productId = "product_id"
|
|
}
|
|
}
|
|
|
|
struct CheckOutRequest: Codable {
|
|
let productId: Int
|
|
let quantity: Double
|
|
let unit: String
|
|
let lotId: Int?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case quantity, unit
|
|
case productId = "product_id"
|
|
case lotId = "lot_id"
|
|
}
|
|
}
|
|
|
|
struct StockResponse: Codable {
|
|
let productStock: Double
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case productStock = "product_stock"
|
|
}
|
|
}
|
|
|
|
struct NewProductRequest: Codable {
|
|
let barcode: String?
|
|
let name: String
|
|
let brand: String?
|
|
let imageUrl: String?
|
|
let baseUnit: String
|
|
let packageSize: Double?
|
|
let packageLabel: String?
|
|
let datePrecision: String
|
|
let groupId: Int?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case barcode, name, brand
|
|
case imageUrl = "image_url"
|
|
case baseUnit = "base_unit"
|
|
case packageSize = "package_size"
|
|
case packageLabel = "package_label"
|
|
case datePrecision = "date_precision"
|
|
case groupId = "group_id"
|
|
}
|
|
}
|
|
|
|
// MARK: - Listen (Einkaufsliste, bald ablaufend)
|
|
|
|
struct ShoppingItem: Codable, Identifiable {
|
|
let productId: Int
|
|
let name: String
|
|
let baseUnit: String
|
|
let packageSize: Double?
|
|
let stock: Double
|
|
let minStock: Double
|
|
let deficit: Double
|
|
|
|
var id: Int { productId }
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case name, stock, deficit
|
|
case productId = "product_id"
|
|
case baseUnit = "base_unit"
|
|
case packageSize = "package_size"
|
|
case minStock = "min_stock"
|
|
}
|
|
}
|
|
|
|
struct GroupShoppingItem: Codable, Identifiable {
|
|
let groupId: Int
|
|
let name: String
|
|
let stock: Double
|
|
let minStock: Double
|
|
let deficit: Double
|
|
let unitName: String
|
|
let productCount: Int
|
|
|
|
var id: Int { groupId }
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case name, stock, deficit
|
|
case groupId = "group_id"
|
|
case minStock = "min_stock"
|
|
case unitName = "unit_name"
|
|
case productCount = "product_count"
|
|
}
|
|
}
|
|
|
|
struct ExpiringItem: Codable, Identifiable {
|
|
let lotId: Int
|
|
let productId: Int
|
|
let productName: String
|
|
let quantity: Double
|
|
let baseUnit: String
|
|
let bestBefore: String
|
|
let bestBeforePrecision: String?
|
|
let daysLeft: Int
|
|
let packageSize: Double?
|
|
let packageLabel: String?
|
|
let unitName: String
|
|
let unitFactor: Double
|
|
|
|
var id: Int { lotId }
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case quantity
|
|
case lotId = "lot_id"
|
|
case productId = "product_id"
|
|
case productName = "product_name"
|
|
case baseUnit = "base_unit"
|
|
case bestBefore = "best_before"
|
|
case bestBeforePrecision = "best_before_precision"
|
|
case daysLeft = "days_left"
|
|
case packageSize = "package_size"
|
|
case packageLabel = "package_label"
|
|
case unitName = "unit_name"
|
|
case unitFactor = "unit_factor"
|
|
}
|
|
}
|
|
|
|
// MARK: - Aenderungen
|
|
|
|
/// Nur gesetzte Felder werden geschickt (PATCH).
|
|
struct ProductUpdateRequest: Codable {
|
|
var name: String?
|
|
var brand: String?
|
|
var packageSize: Double?
|
|
var packageLabel: String?
|
|
var datePrecision: String?
|
|
var minStock: Double?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case name, brand
|
|
case packageSize = "package_size"
|
|
case packageLabel = "package_label"
|
|
case datePrecision = "date_precision"
|
|
case minStock = "min_stock"
|
|
}
|
|
}
|
|
|
|
struct LotUpdateRequest: Codable {
|
|
var quantity: Double?
|
|
var bestBefore: String?
|
|
var bestBeforePrecision: String?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case quantity
|
|
case bestBefore = "best_before"
|
|
case bestBeforePrecision = "best_before_precision"
|
|
}
|
|
}
|