ItemEditView bekommt einen Kaufpreis (Betrag + CHF/EUR) und eine Beleg-Sektion: Rechnung/Garantieschein als Bild (Fotoauswahl) oder PDF (Datei-Import) hochladen, ansehen (QuickLook) und loeschen. Nach einem PDF-Upload schlaegt der Server "Garantie bis" und Kaufpreis vor – per Knopf uebernehmbar. Item-Model um priceCents/currency/documents erweitert; Client-Methoden fuer Upload, Download und Loeschen der Belege. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1000 lines
27 KiB
Swift
1000 lines
27 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?
|
||
/// 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?
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case id, barcode, name, brand, stock, kind, barcodes, tracking, individual
|
||
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"
|
||
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 }
|
||
|
||
/// 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?
|
||
var individual: Bool? = nil
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case barcode, name, brand, individual
|
||
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?
|
||
// Nur fuer Gegenstaende (Standard nil = nicht mitschicken bei fieldValues).
|
||
var shopId: Int? = nil
|
||
var productUrl: String? = nil
|
||
var fieldValues: [String: String?]? = nil
|
||
|
||
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"
|
||
case shopId = "shop_id"
|
||
case productUrl = "product_url"
|
||
case fieldValues = "field_values"
|
||
}
|
||
|
||
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) }
|
||
}
|
||
}
|
||
|
||
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 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 where ID == Int {
|
||
var name: String { get }
|
||
var parentId: Int? { get }
|
||
}
|
||
|
||
extension CategoryItem: TreeItem {}
|
||
extension StorageLocation: TreeItem {}
|
||
|
||
/// 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: Int?
|
||
|
||
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: Int?
|
||
let toLocationId: Int?
|
||
|
||
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: Int?
|
||
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: Int?
|
||
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: Int?
|
||
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 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"
|
||
}
|
||
}
|
||
|
||
/// Antwort nach dem Beleg-Upload – mit den aus dem PDF geschätzten Werten.
|
||
struct ItemDocumentUpload: Codable {
|
||
let id: Int
|
||
let suggestedWarrantyUntil: String?
|
||
let suggestedPriceCents: Int?
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case id
|
||
case suggestedWarrantyUntil = "suggested_warranty_until"
|
||
case suggestedPriceCents = "suggested_price_cents"
|
||
}
|
||
}
|
||
|
||
struct ItemCreateRequest: Codable {
|
||
let count: Int
|
||
let locationId: Int?
|
||
let shopId: Int?
|
||
let acquiredOn: String?
|
||
let warrantyUntil: String?
|
||
let note: String?
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case count, note
|
||
case locationId = "location_id"
|
||
case shopId = "shop_id"
|
||
case acquiredOn = "acquired_on"
|
||
case warrantyUntil = "warranty_until"
|
||
}
|
||
}
|
||
|
||
struct ItemUpdateRequest: Encodable {
|
||
var locationId: Int?
|
||
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?
|
||
|
||
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"
|
||
}
|
||
}
|
||
|
||
/// 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: Int?
|
||
|
||
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"
|
||
}
|
||
}
|