Die App zeigte an mehreren Stellen Rohwerte statt lesbarer Angaben, und die
Bedienelemente sahen nicht nach Bedienelementen aus.
Deutsche Lokalisierung: Im Chargen-Abschnitt stand "10. Dec 2027" - deutsches
Format mit englischem Monatsnamen. Die Ursache lag nicht in der Formatierung,
sondern darin, dass die App als englische App gebaut wurde
(CFBundleDevelopmentRegion = $(DEVELOPMENT_LANGUAGE), also "en"). Jetzt ist
Deutsch als Sprache gesetzt; damit zeichnet iOS DatePicker und Monatsnamen von
selbst richtig. Der MonthYearPicker legt seinen Kalender zusaetzlich fest auf
de_DE, damit die Monatsnamen auch bei anderssprachigem Geraet stimmen.
Datumsformat: Die Anzeige folgt jetzt der Servereinstellung (Einstellungen ->
Darstellung), wie die Web-Oberflaeche. Neu DisplaySettings mit den fuenf
Formaten, den deutschen Beschriftungen der Basiseinheiten und der Mengenangabe
in der Leitangabe. Die doppelt ausprogrammierte MHD-Formatierung in
ListViews und CheckOutView ist damit weg.
Chargen-Abschnitt: Jede Charge steht jetzt in einem eigenen Abschnitt mit
Kopfzeile statt gestapelt in einer einzigen Formularzeile - das war der
gequetschte Eindruck. Loeschen sitzt in der Kopfzeile.
Mengenfelder haben einen sichtbaren Rahmen. Ohne ihn las sich das
rechtsbuendige Feld wie fester Text, und es war nicht erkennbar, dass sich die
Menge ueberhaupt aendern laesst.
Kamera: Der Sucher fuellte per ZStack und ignoresSafeArea den ganzen Bildschirm
und wirkte dadurch erdrueckend. Er sitzt jetzt als 4:3-Feld oben, darunter
Hinweis, Meldungen und Knoepfe in gewohnter Form. Am ScannerViewController
musste nichts geaendert werden.
Einkaufsliste: Artikel und Kategorien tragen ein Symbol, Kategoriezeilen
zusaetzlich eine Kennzeichnung. Den Artikelzeilen fehlte bisher jede Einheit
("fehlt 2 - Bestand 7 von 10"); sie nennen jetzt Packungen und Basismenge.
Produktdetails: Die Felder waren blanke TextFields - sobald ein Wert drinstand,
verschwand der Platzhalter und man las nur noch "Bratbutter" / "M-Classic" /
"450". Jetzt mit Beschriftung links, Einheit hinter der Packungsgroesse (vorher
stand dort der rohe API-Wert "gram") und einem Abschnitt "Erkennung" mit
Barcode. Dasselbe im Formular zum Anlegen.
Gruppen heissen in der Oberflaeche jetzt Kategorien - in Web und App. Tabelle,
Feld group_id und die Endpunkte behalten ihren Namen, damit bestehende Zugriffe
wie Home Assistant weiterlaufen; der Unterschied ist rein sprachlich. Die
Kategorie laesst sich nun auch in der App zuweisen, was vorher gar nicht ging.
Dabei ein Fehler gefunden und behoben: Swift laesst nil-Optionals beim Kodieren
weg, das Backend wertet nur mitgeschickte Felder aus. "Keine Kategorie" oder
eine geloeschte Marke waeren damit stumm verpufft. ProductUpdateRequest kodiert
diese Felder jetzt ausdruecklich. Der Mindestbestand steht bewusst nicht mehr
darin, weil die App ihn nicht bearbeitet und ein mitgesendetes null ihn
geloescht haette.
Getestet: iOS-Geraetebuild fehler- und warnungsfrei, Web-Build laeuft durch,
40 pytest-Tests gruen. Die Datumsformatierung wurde eigenstaendig uebersetzt
und gegen elf Faelle geprueft (alle fuenf Formate, Monatsangabe, fehlendes und
unlesbares Datum) - alle korrekt. Im gebauten Bundle ist belegt, dass
CFBundleDevelopmentRegion auf "de" steht. Gegen die laufende API geprueft:
/settings und /groups liefern die erwarteten Felder, das Leeren der Kategorie
wirkt jetzt und der Mindestbestand bleibt dabei erhalten.
Die Oberflaeche selbst habe ich nicht auf dem Geraet bedient.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
365 lines
9.7 KiB
Swift
365 lines
9.7 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 }
|
|
}
|
|
|
|
/// Auswahleintrag fuer Einheiten.
|
|
///
|
|
/// Anzeige und uebertragener Wert sind bewusst getrennt: Das Backend kennt
|
|
/// fuer das Gebinde nur das Schluesselwort "package" (siehe
|
|
/// services/conversion.py). Die Bezeichnung "Glas" oder "Dose" ist reine
|
|
/// Anzeige - wird sie mitgeschickt, antwortet der Server
|
|
/// "Unbekannte Einheit: Glas".
|
|
struct UnitOption: Hashable, Identifiable {
|
|
let value: String
|
|
let label: String
|
|
var id: String { value }
|
|
|
|
static let packageValue = "package"
|
|
}
|
|
|
|
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
|
|
|
|
/// Die Felder, die das Produktformular der App besitzt (PATCH).
|
|
///
|
|
/// Bewusst mit eigener Kodierung: Swift laesst `nil`-Optionals sonst einfach
|
|
/// weg, und das Backend wertet nur mitgeschickte Felder aus. Leeren waere damit
|
|
/// unmoeglich - "keine Kategorie" oder eine geloeschte Marke wuerden stumm
|
|
/// verpuffen. Felder, die die App nicht bearbeitet (etwa der Mindestbestand),
|
|
/// stehen deshalb gar nicht erst hier drin.
|
|
struct ProductUpdateRequest: Encodable {
|
|
var name: String?
|
|
var brand: String?
|
|
var packageSize: Double?
|
|
var packageLabel: String?
|
|
var datePrecision: String?
|
|
/// Kategorie (heisst in der API weiterhin group_id).
|
|
var groupId: Int?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case name, brand
|
|
case packageSize = "package_size"
|
|
case packageLabel = "package_label"
|
|
case datePrecision = "date_precision"
|
|
case groupId = "group_id"
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(name, forKey: .name)
|
|
try container.encode(brand, forKey: .brand)
|
|
try container.encode(packageSize, forKey: .packageSize)
|
|
try container.encode(packageLabel, forKey: .packageLabel)
|
|
try container.encode(datePrecision, forKey: .datePrecision)
|
|
try container.encode(groupId, forKey: .groupId)
|
|
}
|
|
}
|
|
|
|
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"
|
|
}
|
|
}
|
|
|
|
/// Ein Eintrag aus /settings (Schluessel/Wert).
|
|
struct SettingEntry: Codable {
|
|
let key: String
|
|
let value: String
|
|
}
|
|
|
|
/// Kategorie (heisst in der Datenbank und in der API weiterhin "group";
|
|
/// nur die Beschriftung in der Oberflaeche wurde auf "Kategorie" geaendert).
|
|
struct GroupItem: Codable, Identifiable, Hashable {
|
|
let id: Int
|
|
let name: String
|
|
}
|