Files
Vorrania/ios/Sources/Models.swift
Scarriffle d680614526 iOS: Diagramm-Einstellungen der Web-Karten beim Spiegeln uebernehmen
Die gespiegelte Web-Uebersicht ignorierte bisher die Karten-Props der
Diagramme. Jetzt werden uebernommen:
- Ablauf-Ring: 'Ohne MHD einbeziehen' (mit_ohne_mhd)
- Kategorien-Ring: 'Detailansicht' (alle einzeln statt 'Uebrige') (detail)
- Kategorien-Ring: 'Kategorie-Tiefe' (tiefe -> /by-category?depth=N)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 22:41:36 +02:00

1271 lines
36 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 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
/// Epoch der letzten Bildänderung (nil = kein Bild) steuert den Bild-Cache.
let imageVersion: 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?
/// Verwaltungsart aus der Kategorie: "food" (Chargen+MHD) oder "object" (Menge je Ort).
let tracking: String?
/// Nur fuer Gegenstaende: Bezugsquelle und Onlineshop-Link.
let shopId: Int?
let shopName: String?
let productUrl: String?
/// Selbst definierte Feldwerte: {feld_id (als Text): Wert}.
let fieldValues: [String: String?]?
/// Gegenstand als Einzelstücke (Items mit UID/QR) statt als Menge geführt.
let individual: Bool?
/// Gegenstand als Verbrauchsgegenstand wie ein Lebensmittel (Charge) geführt.
let bulk: Bool?
/// Mindestbestand je Lagerort (zusaetzlich zum globalen Mindestbestand).
let locationMinStocks: [LocationMinStock]?
/// Zeitpunkt der letzten Änderung (ISO-String), zum Sortieren Zuletzt geändert".
let updatedAt: String?
enum CodingKeys: String, CodingKey {
case id, barcode, name, brand, stock, kind, barcodes, tracking, individual, bulk
case locationMinStocks = "location_min_stocks"
case updatedAt = "updated_at"
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 imageVersion = "image_version"
case unitName = "unit_name"
case unitFactor = "unit_factor"
case shopId = "shop_id"
case shopName = "shop_name"
case productUrl = "product_url"
case fieldValues = "field_values"
}
/// Gegenstand (Menge je Lagerort) statt Lebensmittel (Chargen/MHD)?
var isObject: Bool { tracking == "object" }
/// Gegenstand, der als Einzelstücke (UID/QR) geführt wird?
var isIndividual: Bool { individual == true }
/// Verbrauchsgegenstand: Gegenstand, der wie ein Lebensmittel als Charge läuft.
var isBulk: Bool { bulk == true }
/// Nutzt die Lebensmittel-Pfade (Chargen, Einheit, MHD, Mindestbestand):
/// echte Lebensmittel UND Verbrauchsgegenstände.
var foodLike: Bool { !isObject || isBulk }
/// 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
}
}
/// Mindestbestand eines Produkts/einer Gruppe an einem Lagerort (Anzeige).
struct LocationMinStock: Codable, Hashable, Identifiable {
let locationId: String
let locationName: String?
let minStock: Double
var id: String { locationId }
enum CodingKeys: String, CodingKey {
case minStock = "min_stock"
case locationId = "location_id"
case locationName = "location_name"
}
}
/// Ein Mindestbestand-Eintrag zum Speichern (Menge in Artikeleinheiten).
struct LocationMinStockIn: Codable {
let locationId: String
let minStock: Double
enum CodingKeys: String, CodingKey {
case minStock = "min_stock"
case locationId = "location_id"
}
}
/// 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: String?
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: String
let name: String
let parentId: String?
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: String?
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"
}
}
// MARK: - Kassenzettel-Abgleich
struct MatchRequest: Codable {
let lines: [String]
let threshold: Int?
}
struct MatchCandidate: Codable, Identifiable, Hashable {
let productId: Int
let name: String
let brand: String?
let score: Int
let packageSize: Double?
let baseUnit: String
var id: Int { productId }
/// Einheit fuers Einlagern: ganze Gebinde, sonst die Basiseinheit.
var checkInUnit: String { (packageSize ?? 0) > 0 ? "package" : baseUnit }
enum CodingKeys: String, CodingKey {
case name, brand, score
case productId = "product_id"
case packageSize = "package_size"
case baseUnit = "base_unit"
}
}
struct MatchLine: Codable {
let text: String
let candidates: [MatchCandidate]
}
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?
var individual: Bool? = nil
/// Verbrauchsgegenstand: Gegenstand wie ein Lebensmittel (Charge) geführt.
var bulk: Bool? = nil
/// Selbst definierte Feldwerte der Kategorie: {feld_id (als Text): Wert}.
var fieldValues: [String: String?]? = nil
enum CodingKeys: String, CodingKey {
case barcode, name, brand, individual, bulk
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"
case fieldValues = "field_values"
}
}
// 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"
}
}
// MARK: - Bedarfe je Lagerort
struct LocationNeedProduct: Codable, Identifiable {
let productId: Int
let name: String
let unitLabel: String
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 unitLabel = "unit_label"
case minStock = "min_stock"
}
}
struct LocationNeedGroup: Codable, Identifiable {
let groupId: Int
let name: String
let unitName: String
let stock: Double
let minStock: Double
let deficit: Double
var id: Int { groupId }
enum CodingKeys: String, CodingKey {
case name, stock, deficit
case groupId = "group_id"
case unitName = "unit_name"
case minStock = "min_stock"
}
}
struct LocationNeeds: Codable, Identifiable {
let locationId: String
let locationName: String
let products: [LocationNeedProduct]
let groups: [LocationNeedGroup]
var id: String { locationId }
enum CodingKeys: String, CodingKey {
case products, groups
case locationId = "location_id"
case locationName = "location_name"
}
}
/// Ein Artikel mit seinem Bestand an einem Lagerort (in Artikeleinheiten).
struct LocationContentEntry: Codable, Identifiable, Hashable {
let productId: Int
let name: String
let brand: String?
let stock: Double
let unitLabel: String
let individual: Bool
var id: Int { productId }
enum CodingKeys: String, CodingKey {
case name, brand, stock, individual
case productId = "product_id"
case unitLabel = "unit_label"
}
}
/// Alles, was an einem gescannten Lagerort liegt inklusive der Unterorte.
struct LocationContents: Codable, Identifiable {
let locationId: String
let locationName: String
let products: [LocationContentEntry]
var id: String { locationId }
enum CodingKeys: String, CodingKey {
case products
case locationId = "location_id"
case locationName = "location_name"
}
}
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?
// Nur fuer Gegenstaende (Standard nil = nicht mitschicken bei fieldValues).
var shopId: Int? = nil
var productUrl: String? = nil
var fieldValues: [String: String?]? = nil
// Mindestbestand nur senden, wenn ausdrücklich gewünscht sonst bliebe der
// erfasste Modus (z.B. 5 kg") sonst erhalten und wird nicht überschrieben.
var minStock: Double? = nil
var minStockInPackages: Bool? = nil
var minStockUnitId: Int? = nil
var sendMinStock = false
// Verwaltungsart eines Gegenstands (Einzelstück/Verbrauchsgegenstand) und die
// Basiseinheit nur senden, wenn ausdrücklich gewünscht sonst nichts ändern.
var baseUnit: String? = nil
var individual: Bool? = nil
var bulk: Bool? = nil
var sendMode = false
enum CodingKeys: String, CodingKey {
case name, brand, individual, bulk
case packageSize = "package_size"
case packageLabel = "package_label"
case datePrecision = "date_precision"
case groupId = "group_id"
case categoryId = "category_id"
case shopId = "shop_id"
case productUrl = "product_url"
case fieldValues = "field_values"
case minStock = "min_stock"
case minStockInPackages = "min_stock_in_packages"
case minStockUnitId = "min_stock_unit_id"
case baseUnit = "base_unit"
}
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)
try container.encode(shopId, forKey: .shopId)
try container.encode(productUrl, forKey: .productUrl)
// Feldwerte nur senden, wenn gesetzt sonst nichts an den Feldern ändern.
if let fieldValues { try container.encode(fieldValues, forKey: .fieldValues) }
if sendMinStock {
try container.encode(minStock, forKey: .minStock)
try container.encode(minStockInPackages, forKey: .minStockInPackages)
try container.encode(minStockUnitId, forKey: .minStockUnitId)
}
if sendMode {
try container.encode(individual, forKey: .individual)
try container.encode(bulk, forKey: .bulk)
if let baseUnit { try container.encode(baseUnit, forKey: .baseUnit) }
}
}
}
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
// Mindestbestand-Infos (nur aus /groups befuellt; Picker brauchen sie nicht).
var minStock: Double? = nil
var minStockUnitName: String? = nil
var stock: Double? = nil
var locationMinStocks: [LocationMinStock]? = nil
enum CodingKeys: String, CodingKey {
case id, name, stock
case minStock = "min_stock"
case minStockUnitName = "min_stock_unit_name"
case locationMinStocks = "location_min_stocks"
}
}
/// Kategorie: ordnet die Artikelliste und bestimmt die Verwaltungsart
/// ("food"/"object"), verschachtelbar (Suesswaren -> Schokolade).
struct CategoryItem: Codable, Identifiable, Hashable {
let id: Int
let name: String
let parentId: Int?
let tracking: String?
enum CodingKeys: String, CodingKey {
case id, name, tracking
case parentId = "parent_id"
}
var isObject: Bool { tracking == "object" }
}
// MARK: - Hierarchische Stammdaten
/// Gemeinsame Form baumartiger Stammdaten (Kategorien, Lagerorte): eine flache
/// Liste mit Eltern-Verweis, aus der sich der Baum aufbauen lässt.
protocol TreeItem: Identifiable {
var name: String { get }
var parentId: ID? { get }
}
extension CategoryItem: TreeItem {}
extension StorageLocation: TreeItem {}
extension StorageLocation {
/// Voller Pfad Hedingen Keller Schublade 1" aus der flachen Liste
/// macht gleichnamige Orte in den Auswahlfeldern unterscheidbar.
func path(in all: [StorageLocation]) -> String {
let byId = Dictionary(all.map { ($0.id, $0) }, uniquingKeysWith: { a, _ in a })
var teile = [name]
var gesehen: Set<String> = [id]
var pid = parentId
while let cur = pid, !gesehen.contains(cur), let parent = byId[cur] {
teile.insert(parent.name, at: 0)
gesehen.insert(cur)
pid = parent.parentId
}
return teile.joined(separator: "")
}
}
/// Pfad zu einer Lagerort-ID (leer, wenn unbekannt).
func locationPath(_ id: String?, in all: [StorageLocation]) -> String {
guard let id, let loc = all.first(where: { $0.id == id }) else { return "" }
return loc.path(in: all)
}
/// Shop / Bezugsquelle fuer Gegenstaende ("gekauft bei").
struct ShopItem: Codable, Identifiable, Hashable {
let id: Int
let name: String
let website: String?
let productCount: Int?
enum CodingKeys: String, CodingKey {
case id, name, website
case productCount = "product_count"
}
}
/// Selbst definiertes Feld einer Kategorie (inkl. der geerbten).
struct FieldDefinition: Codable, Identifiable, Hashable {
let id: Int
let categoryId: Int
let label: String
let key: String
let fieldType: String
let unit: String?
let options: [String]
let required: Bool
let position: Int
let isBuiltin: Bool
let inherited: Bool
enum CodingKeys: String, CodingKey {
case id, label, key, unit, options, required, position, inherited
case categoryId = "category_id"
case fieldType = "field_type"
case isBuiltin = "is_builtin"
}
}
// MARK: - Gegenstaende: Bestandsbuchungen
struct ObjectCheckInRequest: Codable {
let productId: Int
let quantity: Double
let unit: String
let locationId: String?
enum CodingKeys: String, CodingKey {
case quantity, unit
case productId = "product_id"
case locationId = "location_id"
}
}
struct RelocateRequest: Codable {
let productId: Int
let quantity: Double
let fromLocationId: String?
let toLocationId: String?
enum CodingKeys: String, CodingKey {
case quantity
case productId = "product_id"
case fromLocationId = "from_location_id"
case toLocationId = "to_location_id"
}
}
struct RemoveRequest: Codable {
let productId: Int
let quantity: Double
let locationId: String?
let reason: String
let note: String?
enum CodingKeys: String, CodingKey {
case quantity, reason, note
case productId = "product_id"
case locationId = "location_id"
}
}
struct RemovalStat: Codable, Identifiable, Hashable {
let reason: String
let quantity: Double
let count: Int
var id: String { reason }
}
struct RemovalHistoryItem: Codable, Identifiable, Hashable {
let reason: String
let quantity: Double
let locationId: String?
let locationName: String?
let note: String?
let username: String?
let createdAt: String
var id: String { "\(reason)-\(createdAt)-\(quantity)" }
enum CodingKeys: String, CodingKey {
case reason, quantity, note, username
case locationId = "location_id"
case locationName = "location_name"
case createdAt = "created_at"
}
}
struct RemovalSummary: Codable {
let stats: [RemovalStat]
let history: [RemovalHistoryItem]
}
/// Anzeige-Bezeichnungen der Entnahmegruende.
enum RemovalReasons {
static let all: [(value: String, label: String)] = [
("broken", "kaputt"),
("lost", "verloren"),
("given_away", "verschenkt"),
("sold", "verkauft"),
("used_up", "aufgebraucht"),
("other", "sonstiges"),
]
static func label(_ value: String) -> String {
all.first { $0.value == value }?.label ?? value
}
}
// MARK: - Einzelstücke (Items mit UID/QR)
struct ItemDocument: Codable, Identifiable, Hashable {
let id: Int
let filename: String
let contentType: String
let uploadedAt: String
enum CodingKeys: String, CodingKey {
case id, filename
case contentType = "content_type"
case uploadedAt = "uploaded_at"
}
}
struct Item: Codable, Identifiable, Hashable {
let id: Int
let uid: String
let productId: Int
let locationId: String?
let locationName: String?
let shopId: Int?
let shopName: String?
let acquiredOn: String? // "yyyy-MM-dd"
let warrantyUntil: String?
let note: String?
let priceCents: Int?
let currency: String?
let createdAt: String
let productName: String?
let productBrand: String?
let categoryId: Int?
let categoryName: String?
let documents: [ItemDocument]
enum CodingKeys: String, CodingKey {
case id, uid, note, currency, documents
case productId = "product_id"
case locationId = "location_id"
case locationName = "location_name"
case shopId = "shop_id"
case shopName = "shop_name"
case acquiredOn = "acquired_on"
case warrantyUntil = "warranty_until"
case priceCents = "price_cents"
case createdAt = "created_at"
case productName = "product_name"
case productBrand = "product_brand"
case categoryId = "category_id"
case categoryName = "category_name"
}
}
/// Antwort nach dem Beleg-Upload mit den aus dem PDF geschätzten Werten.
/// Aus einem Beleg-PDF geschätzte Werte (Analyse ohne Speichern).
struct DocumentSuggestions: Codable {
var suggestedWarrantyUntil: String? = nil
var suggestedPriceCents: Int? = nil
var suggestedPriceCandidates: [Int] = []
var suggestedAcquiredOn: String? = nil
var suggestedShopId: Int? = nil
var suggestedShopName: String? = nil
enum CodingKeys: String, CodingKey {
case suggestedWarrantyUntil = "suggested_warranty_until"
case suggestedPriceCents = "suggested_price_cents"
case suggestedPriceCandidates = "suggested_price_candidates"
case suggestedAcquiredOn = "suggested_acquired_on"
case suggestedShopId = "suggested_shop_id"
case suggestedShopName = "suggested_shop_name"
}
init() {}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
suggestedWarrantyUntil = try c.decodeIfPresent(String.self, forKey: .suggestedWarrantyUntil)
suggestedPriceCents = try c.decodeIfPresent(Int.self, forKey: .suggestedPriceCents)
suggestedPriceCandidates = try c.decodeIfPresent([Int].self, forKey: .suggestedPriceCandidates) ?? []
suggestedAcquiredOn = try c.decodeIfPresent(String.self, forKey: .suggestedAcquiredOn)
suggestedShopId = try c.decodeIfPresent(Int.self, forKey: .suggestedShopId)
suggestedShopName = try c.decodeIfPresent(String.self, forKey: .suggestedShopName)
}
}
struct ItemDocumentUpload: Codable {
let id: Int
let suggestions: DocumentSuggestions
enum IdKey: String, CodingKey { case id }
init(from decoder: Decoder) throws {
id = try decoder.container(keyedBy: IdKey.self).decode(Int.self, forKey: .id)
suggestions = try DocumentSuggestions(from: decoder)
}
}
struct ItemCreateRequest: Codable {
let count: Int
let locationId: String?
let shopId: Int?
let acquiredOn: String?
let warrantyUntil: String?
let note: String?
var priceCents: Int? = nil
var currency: String? = nil
enum CodingKeys: String, CodingKey {
case count, note, currency
case locationId = "location_id"
case shopId = "shop_id"
case acquiredOn = "acquired_on"
case warrantyUntil = "warranty_until"
case priceCents = "price_cents"
}
}
struct ItemUpdateRequest: Encodable {
var locationId: String?
var shopId: Int?
var acquiredOn: String?
var warrantyUntil: String?
var note: String?
var priceCents: Int?
var currency: String?
enum CodingKeys: String, CodingKey {
case note, currency
case locationId = "location_id"
case shopId = "shop_id"
case acquiredOn = "acquired_on"
case warrantyUntil = "warranty_until"
case priceCents = "price_cents"
}
// Immer alle Felder senden (auch null), damit sich ein Feld leeren lässt.
func encode(to encoder: Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
try c.encode(locationId, forKey: .locationId)
try c.encode(shopId, forKey: .shopId)
try c.encode(acquiredOn, forKey: .acquiredOn)
try c.encode(warrantyUntil, forKey: .warrantyUntil)
try c.encode(note, forKey: .note)
try c.encode(priceCents, forKey: .priceCents)
try c.encode(currency, forKey: .currency)
}
}
struct ItemRemoveRequest: Codable {
let reason: String
let note: String?
}
// 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?
let locationId: String?
let detail: Bool? // Kategorien-Ring: alle einzeln statt Übrige"
let tiefe: String? // Kategorie-Tiefe ("" = feinste, "1"/"2"/)
let mitOhneMhd: Bool? // Ablauf-Ring: Ohne MHD" einbeziehen
enum CodingKeys: String, CodingKey {
case productId = "product_id"
case locationId = "location_id"
case detail
case tiefe
case mitOhneMhd = "mit_ohne_mhd"
}
}
}
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: String?
enum CodingKeys: String, CodingKey {
case name
case parentId = "parent_id"
}
}
/// Lagerort umbenennen und/oder umhängen. `parentId` wird bewusst immer gesendet
/// (auch `null` = oberste Ebene), damit auf oberste Ebene holen" ankommt.
struct LocationUpdateRequest: Codable {
let name: String
let parentId: String?
enum CodingKeys: String, CodingKey {
case name
case parentId = "parent_id"
}
func encode(to encoder: Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
try c.encode(name, forKey: .name)
try c.encode(parentId, forKey: .parentId) // encodet null bei nil
}
}
/// Eigenes Feld einer Kategorie anlegen. `unit`/`options` werden immer
/// mitgeschickt; der Server ignoriert sie bei unpassendem Typ.
struct FieldDefinitionCreate: Codable {
let categoryId: Int
let label: String
let fieldType: String
let unit: String
let options: [String]
let required: Bool
enum CodingKeys: String, CodingKey {
case label, unit, options, required
case categoryId = "category_id"
case fieldType = "field_type"
}
}
struct FieldDefinitionUpdate: Codable {
let label: String
let fieldType: String
let unit: String
let options: [String]
let required: Bool
enum CodingKeys: String, CodingKey {
case label, unit, options, required
case fieldType = "field_type"
}
}
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?
var tracking: String? = nil
enum CodingKeys: String, CodingKey {
case name, tracking
case parentId = "parent_id"
}
}
struct CategoryUpdateRequest: Codable {
let name: String?
let tracking: String?
}
struct NewShopRequest: Codable {
let name: String
let website: String?
}
struct ShopUpdateRequest: Codable {
let name: String?
let website: String?
}
/// 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"
}
}