Files
Vorrania/ios/Sources/Models.swift
Scarriffle 580afcc133 iOS: eigene, gestaltbare App-Uebersicht mit nativen Diagrammen
Der Start-Tab spiegelte bisher fest die Web-Dashboards - dieselbe gespeicherte
Anordnung, und Diagramme gab es nur den Hinweis "im Web". Das war der Punkt,
der geaergert hat.

Jetzt ist der Modus je Server umschaltbar: "Web-Uebersicht spiegeln" (weiter
Standard, damit nach dem Update nichts ueberrascht) oder "Eigene
App-Uebersicht". Die eigene Uebersicht liegt nur auf dem Geraet (pro Server),
unabhaengig vom Web und nicht synchronisiert. Beim ersten Umschalten entsteht
gleich eine sinnvolle Startanordnung.

Sie funktioniert wie im Web, nur fuers Telefon: mehrere Dashboards, Karten
einspaltig untereinander, im Bearbeiten-Modus hinzufuegen, per Ziehen sortieren
und loeschen; Artikelkarten bekommen ihren Artikel zugewiesen. Anzeigen und
Bearbeiten sind getrennt - so laufen Sortieren und Loeschen sauber, statt an
Sektionsgrenzen zu haken.

Die Diagramme sind nativ mit Swift Charts: Ablauf- und Kategorien-Anteile
(Ring ab iOS 17, sonst ein 100-%-Balken - das Ziel ist iOS 16, deshalb kein
stilles Anheben), Kategorien nach Zustand als gestapelte Balken, Bestands- und
Artikelverlauf als Linie, Ein-/Auslagerungen als gruppierte Balken. Dieselben
Endpunkte wie das Web-Dashboard, kein Backend-Eingriff. Der "gibt es nur im
Web"-Hinweis faellt damit weg - auch beim Spiegeln werden die Diagramme jetzt
nativ gezeichnet.

Geprueft: Build; alle vier Diagramm-Modelle gegen echte Serverantworten
dekodiert (auch der Zeitstempel mit Z und Sekundenbruchteilen); refactorte
DashboardCardView ohne alte Aufrufstellen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 21:29:37 +02:00

651 lines
17 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: - Diagrammdaten (Übersicht)
/// Artikeleinheiten je Ablaufzustand (GET /dashboard/expiry-split).
struct ExpirySplit: Codable {
let ok: Double
let soon: Double
let expired: Double
let noDate: Double
enum CodingKeys: String, CodingKey {
case ok, soon, expired
case noDate = "no_date"
}
}
/// Anteil einer Kategorie am Bestand (GET /dashboard/by-category).
struct CategoryShare: Codable, Identifiable {
let categoryId: Int?
let name: String
let articleUnits: Double
let ok: Double
let soon: Double
let expired: Double
let noDate: Double
var id: Int { categoryId ?? -1 }
enum CodingKeys: String, CodingKey {
case name, ok, soon, expired
case categoryId = "category_id"
case articleUnits = "article_units"
case noDate = "no_date"
}
}
/// Ein Punkt des Bestandsverlaufs (GET /dashboard/timeline).
struct TimelinePoint: Codable, Identifiable {
let at: String
let articleUnits: Double
var id: String { at }
enum CodingKeys: String, CodingKey {
case at
case articleUnits = "article_units"
}
}
/// Ein- und Auslagerungen je Zeitpunkt (GET /dashboard/activity).
struct ActivityPoint: Codable, Identifiable {
let at: String
let checkedIn: Int
let checkedOut: Int
var id: String { at }
enum CodingKeys: String, CodingKey {
case at
case checkedIn = "checked_in"
case checkedOut = "checked_out"
}
}
// 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"
}
}