Files
Vorrania/ios/Sources/Models.swift
Scarriffle 2ad197df42 iOS zeigt dieselben Dashboards und kann direkt buchen
Die Uebersicht in der App war fest verdrahtet: vier Kacheln, unabhaengig
davon, was am Rechner zusammengestellt wurde. Jetzt liest sie dieselben
Dashboards wie die Web-Oberflaeche und stellt sie nativ dar.

Das Raster laesst sich auf einem Telefon nicht sinnvoll nachbilden, deshalb
stehen die Karten untereinander - in der Lesereihenfolge des Rasters, erst
Zeile, dann Spalte. Zusammengestellt wird weiterhin am Rechner; gibt es
mehrere Dashboards, schaltet oben ein Menue um.

Neu ist die Buchungskarte: Artikel suchen, Menge, ein- oder auslagern, ohne
den Scan-Ablauf zu oeffnen. Nach dem Buchen zieht die Karte den Bestand nach
und laesst die uebrigen Karten neu laden - sonst stuende dort weiter die alte
Zahl.

Diagrammkarten bekommen bewusst keinen halbgaren Nachbau, sondern den
ehrlichen Hinweis, dass es sie in der Web-Oberflaeche gibt.

Geprueft mit der echten Antwort eines laufenden Servers: Sie decodiert in die
Modelle der App, samt zweier Karten derselben Art mit verschiedenen Artikeln.
OverviewView faellt weg, DashboardView tritt an ihre Stelle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:27:01 +02:00

588 lines
16 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
/// Eingebaute Einheiten lassen sich umbenennen, aber nicht loeschen.
let isBuiltin: Bool
enum CodingKeys: String, CodingKey {
case id, name, kind, factor
case isBuiltin = "is_builtin"
}
}
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?
/// Kategorie: reine Ordnungshilfe fuer die Artikelliste (nicht die Gruppe).
let categoryId: Int?
let categoryName: String?
/// Mindestbestand in der erfassten Einheit, fuer die Anzeige.
let minStockDisplay: Double?
let minStockUnitLabel: String?
enum CodingKeys: String, CodingKey {
case id, barcode, name, brand, stock, kind, barcodes
case datePrecision = "date_precision"
case categoryId = "category_id"
case categoryName = "category_name"
case minStockDisplay = "min_stock_display"
case minStockUnitLabel = "min_stock_unit_label"
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 }
/// Wie es um den Mindestbestand steht. Beides in Basiseinheiten verglichen.
enum StockLevel { case none, ok, close, below }
var stockLevel: StockLevel {
guard let minimum = minStock, minimum > 0 else { return .none }
if stock < minimum { return .below }
// Weniger als ein Viertel Luft: bald nachkaufen.
return stock < minimum * 1.25 ? .close : .ok
}
}
/// 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?
/// Gruppe: nur gesetzt, wenn der Code dort ausdruecklich hinterlegt ist.
let groupId: Int?
let groupName: String?
/// Kategorie: aus der Open-Food-Facts-Einordnung vorgeschlagen.
let categoryId: Int?
let categoryName: String?
enum CodingKeys: String, CodingKey {
case found, suggestion
case existingProduct = "existing_product"
case groupId = "group_id"
case groupName = "group_name"
case categoryId = "category_id"
case categoryName = "category_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?
let categoryId: 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"
case categoryId = "category_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?
var groupId: Int?
var categoryId: Int?
enum CodingKeys: String, CodingKey {
case name, brand
case packageSize = "package_size"
case packageLabel = "package_label"
case datePrecision = "date_precision"
case groupId = "group_id"
case categoryId = "category_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)
try container.encode(categoryId, forKey: .categoryId)
}
}
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
}
/// Gruppe: fasst Bestaende mehrerer Marken zusammen (z.B. "Mehl").
struct GroupItem: Codable, Identifiable, Hashable {
let id: Int
let name: String
}
/// Kategorie: ordnet nur die Artikelliste, verschachtelbar (Suesswaren -> Schokolade).
struct CategoryItem: Codable, Identifiable, Hashable {
let id: Int
let name: String
let parentId: Int?
enum CodingKeys: String, CodingKey {
case id, name
case parentId = "parent_id"
}
}
// MARK: - Uebersicht und Verlauf
/// Kennzahlen der Startseite (GET /dashboard/stats).
struct DashboardStats: Codable {
let productsInStock: Int
let articleUnits: Double
let expiringSoon: Int
let expired: Int
let shoppingItems: Int
let productsTotal: Int
enum CodingKeys: String, CodingKey {
case productsInStock = "products_in_stock"
case articleUnits = "article_units"
case expiringSoon = "expiring_soon"
case expired
case shoppingItems = "shopping_items"
case productsTotal = "products_total"
}
}
/// Ein Eintrag im Bewegungsverlauf (GET /movements).
struct Movement: Codable, Identifiable, Hashable {
let id: Int
let productId: Int
let productName: String
/// "in" oder "out"
let type: String
/// in Basiseinheiten
let quantity: Double
let baseUnit: String
let unitUsed: String
let username: String?
let note: String?
let createdAt: String
let packageSize: Double?
let packageLabel: String?
let unitName: String
let unitFactor: Double
enum CodingKeys: String, CodingKey {
case id, type, quantity, username, note
case productId = "product_id"
case productName = "product_name"
case baseUnit = "base_unit"
case unitUsed = "unit_used"
case createdAt = "created_at"
case packageSize = "package_size"
case packageLabel = "package_label"
case unitName = "unit_name"
case unitFactor = "unit_factor"
}
var isIncoming: Bool { type == "in" }
}
// MARK: - Dashboards
/// Eine Karte auf einem Dashboard.
///
/// `i` ist die Kennung **dieser** Karte, `type` ihre Art - erst dadurch kann
/// dieselbe Art mehrfach auf einem Dashboard liegen. `props` traegt, was nur
/// diese eine Karte angeht (etwa welcher Artikel gemeint ist).
struct DashboardCard: Codable, Identifiable, Hashable {
let i: String
let type: String
let x: Int
let y: Int
let w: Int
let h: Int
let tage: Int?
let props: CardProps?
var id: String { i }
/// Nur die Felder, die die App auswertet. Die Rasterangaben kommen aus der
/// Web-Oberflaeche; hier bestimmen sie allein die Reihenfolge.
struct CardProps: Codable, Hashable {
let productId: Int?
enum CodingKeys: String, CodingKey {
case productId = "product_id"
}
}
}
struct Dashboard: Codable, Identifiable, Hashable {
let id: Int
let name: String
let position: Int
let layout: [DashboardCard]
}
struct DashboardList: Codable {
let dashboards: [Dashboard]
let source: String
let enforced: Bool
let hasDefault: Bool
enum CodingKeys: String, CodingKey {
case dashboards, source, enforced
case hasDefault = "has_default"
}
}
// MARK: - Stammdaten
/// Gebinde (Packung, Glas, ...) mit Einzahl und Mehrzahl.
struct PackageType: Codable, Identifiable, Hashable {
let id: Int
let singular: String
let plural: String
let isBuiltin: Bool
enum CodingKeys: String, CodingKey {
case id, singular, plural
case isBuiltin = "is_builtin"
}
}
struct NewLocationRequest: Codable {
let name: String
let parentId: Int?
enum CodingKeys: String, CodingKey {
case name
case parentId = "parent_id"
}
}
struct NewUnitRequest: Codable {
let name: String
let kind: String
let factor: Double
}
struct NewPackageTypeRequest: Codable {
let singular: String
let plural: String
}
struct NewCategoryRequest: Codable {
let name: String
let parentId: Int?
enum CodingKeys: String, CodingKey {
case name
case parentId = "parent_id"
}
}
/// Umbenennen von Stammdaten. Ein einzelnes Feld reicht, weil das Backend nur
/// mitgeschickte Schluessel auswertet.
struct RenameRequest: Codable {
let name: String
}
struct PackageTypeUpdateRequest: Codable {
let singular: String
let plural: String
}
/// Neue Gruppe anlegen (nur fuer Administratoren).
struct NewGroupRequest: Codable {
let name: String
let minStock: Double?
let minStockUnitId: Int?
enum CodingKeys: String, CodingKey {
case name
case minStock = "min_stock"
case minStockUnitId = "min_stock_unit_id"
}
}