In der App gab es beides nicht. Chargen waren ueberhaupt nur innerhalb
eines Artikels zu sehen, und dort nur zum Korrigieren - eine
Chargenliste wie im Web fehlte, und damit jeder Weg, eine Packung
anzubrechen oder eine Teilmenge zu verbrauchen. Das Backend kann das
seit zwei Wochen.
Neu im Tab "Listen": der Eintrag "Chargen" mit allen Chargen ueber alle
Artikel, durchsuchbar, mit Schalter "Nur angebrochene". Je Zeile stehen
Menge, MHD bzw. Verbrauchsdatum, Lagerort und ein Abzeichen
"angebrochen"; bei einer offenen Packung die Restmenge in Basiseinheiten
("980 g von 1.000 g"), denn "0,98 Packungen" sagt niemandem etwas.
Nach links wischen gibt die zwei Buchungen:
"Anbrechen" mit Rueckfrage - eine Packung wird abgespalten und bekommt
ihr Verbrauchsdatum, der Rest bleibt zu und behaelt sein MHD.
"Verbrauchen" oeffnet ein Fenster fuer die VERBRAUCHTE Menge (nicht den
Rest - das ist die Zahl, die man kennt). Die Einheitenauswahl holt die
Hausmasse der Kategorie dazu, "2 EL" ist damit buchbar und landet bei
Mehl als 20 g. War die Charge noch zu, bricht die Buchung sie an; der
Haken dafuer ist abwaehlbar.
Loeschen fehlt in der Liste mit Absicht: Eine Charge verschwindet durchs
Verbrauchen, und das Korrigieren sitzt beim Artikel.
Dort zeigt die Chargenzeile jetzt auch "angebrochen · verbrauchen bis
..." - zwei Zeilen mit gleicher Menge waren sonst nicht zu
unterscheiden.
Alles in bestehende Dateien gelegt, keine neue Quelldatei: Das
mitgelieferte Xcode-Projekt listet die Dateien einzeln, eine neue waere
ohne "xcodegen generate" stillschweigend nicht mitgebaut worden.
Gegen den laufenden Server geprueft, genau mit den Aufrufen, die die App
macht: /lots/rows liefert alle Felder, die das Swift-Modell liest;
{"quantity":null} an /lots/{id}/open spaltet 1000 von 2000 g ab und
setzt consume_by; 2 Essloeffel an /lots/{id}/consume buchen 20 g ab und
die Antwort traegt opened und lot.
Nicht kompiliert: kein Xcode auf dieser Maschine. Geprueft sind Klammer-
und Anfuehrungsbalance (und dabei EIN echter Fehler gefunden: ein
deutsches Anfuehrungszeichen mit geradem Schlusszeichen haette eine
Zeichenkette vorzeitig beendet), dass formatAmount global erreichbar
ist, und dass die benutzten Hilfsfunktionen so heissen. Der Build in
Xcode bleibt die Probe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1827 lines
57 KiB
Swift
1827 lines
57 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
|
||
}
|
||
}
|
||
|
||
/// Ein am Server eingerichteter Anmeldedienst (Single Sign-on).
|
||
///
|
||
/// Kommt vom jeweiligen Server, nicht aus der App: So laesst sich ein weiterer
|
||
/// Anbieter allein durch Serverkonfiguration ergaenzen, und bei mehreren
|
||
/// Serverprofilen kann jedes einen eigenen haben.
|
||
struct OidcProviderResponse: Codable, Identifiable {
|
||
let slug: String
|
||
let displayName: String
|
||
let discoveryURL: String
|
||
/// Client-ID des oeffentlichen App-Clients. Fehlt sie, ist fuer diesen
|
||
/// Anbieter nur die Anmeldung im Webbrowser eingerichtet.
|
||
let nativeClientID: String?
|
||
let nativeRedirectURI: String
|
||
let scopes: String
|
||
|
||
var id: String { slug }
|
||
var scopeListe: [String] { scopes.split(separator: " ").map(String.init) }
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case slug
|
||
case displayName = "display_name"
|
||
case discoveryURL = "discovery_url"
|
||
case nativeClientID = "native_client_id"
|
||
case nativeRedirectURI = "native_redirect_uri"
|
||
case scopes
|
||
}
|
||
}
|
||
|
||
/// Anfragekoerper fuer POST /auth/oidc/<slug>/native.
|
||
struct OidcNativeLoginBody: Encodable {
|
||
let idToken: String
|
||
/// Die in der Anmeldeanfrage verwendete nonce. Nur beim ersten Anmelden
|
||
/// vorhanden – ein per Refresh geholtes ID-Token traegt sie nicht
|
||
/// zwangslaeufig, deshalb optional.
|
||
let nonce: String?
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case idToken = "id_token"
|
||
case nonce
|
||
}
|
||
}
|
||
|
||
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?
|
||
/// Zweiteinheit: „secondaryCount Basiseinheiten ≙ secondaryAmount
|
||
/// secondaryBase". Bruecke zwischen den sonst strikt getrennten Arten.
|
||
/// Optionals, damit die App auch gegen einen aelteren Server laeuft.
|
||
let secondaryBase: String?
|
||
let secondaryCount: Double?
|
||
let secondaryAmount: Double?
|
||
/// Vom Server ausgerechnet: Zweit-Basiseinheiten je EINER Basiseinheit.
|
||
let secondaryFactor: Double?
|
||
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 secondaryBase = "secondary_base"
|
||
case secondaryCount = "secondary_count"
|
||
case secondaryAmount = "secondary_amount"
|
||
case secondaryFactor = "secondary_factor"
|
||
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 }
|
||
|
||
/// Zweit-Basiseinheiten je EINER Basiseinheit – nil ohne Bruecke.
|
||
var zweitFaktor: Double? {
|
||
guard let basis = secondaryBase, !basis.isEmpty, basis != baseUnit else { return nil }
|
||
if let f = secondaryFactor, f > 0 { return f }
|
||
// Rueckfall, falls das abgeleitete Feld fehlt (aelterer Server).
|
||
guard let n = secondaryCount, let m = secondaryAmount, n > 0, m > 0 else { return nil }
|
||
return m / n
|
||
}
|
||
|
||
/// Art der Zweiteinheit ("count"/"weight"/"volume") – fuer die Einheitenwahl.
|
||
var zweitKind: String? {
|
||
switch secondaryBase {
|
||
case "piece": return "count"
|
||
case "gram": return "weight"
|
||
case "milliliter": return "volume"
|
||
default: return nil
|
||
}
|
||
}
|
||
|
||
/// Kuerzel der Zweit-Basiseinheit fuer die Anzeige.
|
||
var zweitKurz: String {
|
||
switch secondaryBase {
|
||
case "gram": return "g"
|
||
case "milliliter": return "ml"
|
||
case "piece": return "Stück"
|
||
default: return ""
|
||
}
|
||
}
|
||
|
||
/// Bestand in der Zweiteinheit, z.B. 500 g bei 6 Stueck.
|
||
var stockInZweit: Double? { zweitFaktor.map { stock * $0 } }
|
||
|
||
/// 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).
|
||
///
|
||
/// ``locationId`` nil heisst „Ueberall": egal wo, Hauptsache die Menge ist im
|
||
/// Haus. Das ersetzt den frueheren separaten Gesamt-Mindestbestand. Mengen in
|
||
/// Basiseinheiten (g/ml/Stueck).
|
||
struct LocationMinStock: Codable, Hashable, Identifiable {
|
||
let locationId: String?
|
||
let locationName: String?
|
||
let minStock: Double
|
||
/// Bestand an diesem Ort (inkl. Unterorte), ebenfalls in Basiseinheiten.
|
||
var stock: Double? = nil
|
||
var id: String { locationId ?? UEBERALL_ID }
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case stock
|
||
case minStock = "min_stock"
|
||
case locationId = "location_id"
|
||
case locationName = "location_name"
|
||
}
|
||
}
|
||
|
||
/// Ein Mindestbestand-Eintrag zum Speichern (Menge in Basiseinheiten).
|
||
/// ``locationId`` nil = „Ueberall".
|
||
struct LocationMinStockIn: Codable {
|
||
let locationId: String?
|
||
let minStock: Double
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case minStock = "min_stock"
|
||
case locationId = "location_id"
|
||
}
|
||
|
||
// Den Ort immer senden – auch null, sonst laesst sich „Ueberall" gar nicht
|
||
// ausdruecken (Swift laesst nil-Optionals sonst weg).
|
||
func encode(to encoder: Encoder) throws {
|
||
var c = encoder.container(keyedBy: CodingKeys.self)
|
||
try c.encode(locationId, forKey: .locationId)
|
||
try c.encode(minStock, forKey: .minStock)
|
||
}
|
||
}
|
||
|
||
/// Platzhalter-ID fuer „Ueberall" in Pickern – im JSON ist es schlicht null.
|
||
let UEBERALL_ID = "__ueberall__"
|
||
let UEBERALL_NAME = "Überall"
|
||
|
||
/// 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?
|
||
/// Aus „3 x 80 g" vorgeschlagene Umrechnung – nur ein Vorschlag.
|
||
let secondaryBase: String?
|
||
let secondaryCount: Double?
|
||
let secondaryAmount: Double?
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case barcode, name, brand
|
||
case imageUrl = "image_url"
|
||
case baseUnit = "base_unit"
|
||
case packageSize = "package_size"
|
||
case quantityText = "quantity_text"
|
||
case secondaryBase = "secondary_base"
|
||
case secondaryCount = "secondary_count"
|
||
case secondaryAmount = "secondary_amount"
|
||
}
|
||
}
|
||
}
|
||
|
||
struct Lot: Codable, Identifiable, Hashable {
|
||
let id: Int
|
||
let productId: Int
|
||
let quantity: Double
|
||
let bestBefore: String?
|
||
let bestBeforePrecision: String?
|
||
let locationId: String?
|
||
/// Oeffnungstag. nil = die Packung ist noch zu.
|
||
let openedOn: String?
|
||
/// "Zu verbrauchen bis" - beim Anbrechen aus der Frist gerechnet.
|
||
let consumeBy: String?
|
||
/// Menge beim Oeffnen, damit "noch 400 von 1000 ml" anzeigbar ist.
|
||
let openedQuantity: Double?
|
||
|
||
var istAngebrochen: Bool { openedOn != nil }
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case id, quantity
|
||
case productId = "product_id"
|
||
case bestBefore = "best_before"
|
||
case bestBeforePrecision = "best_before_precision"
|
||
case locationId = "location_id"
|
||
case openedOn = "opened_on"
|
||
case consumeBy = "consume_by"
|
||
case openedQuantity = "opened_quantity"
|
||
}
|
||
}
|
||
|
||
/// Eine Charge mit den Artikelangaben dran - fuer die uebergreifende
|
||
/// Chargenliste, in der Chargen verschiedener Artikel nebeneinander stehen.
|
||
struct LotRow: Codable, Identifiable, Hashable {
|
||
let id: Int
|
||
let productId: Int
|
||
let productName: String
|
||
let productBrand: String?
|
||
let quantity: Double
|
||
let baseUnit: String
|
||
let packageSize: Double?
|
||
let packageLabel: String?
|
||
let unitName: String
|
||
let unitFactor: Double
|
||
let bestBefore: String?
|
||
let bestBeforePrecision: String?
|
||
let locationId: String?
|
||
let openedOn: String?
|
||
let consumeBy: String?
|
||
let openedQuantity: Double?
|
||
|
||
var istAngebrochen: Bool { openedOn != nil }
|
||
|
||
/// Basiseinheiten je Artikeleinheit: das Gebinde, sonst die Anzeigeeinheit.
|
||
var artikelFaktor: Double {
|
||
if let p = packageSize, p > 0 { return p }
|
||
return unitFactor > 0 ? unitFactor : 1
|
||
}
|
||
|
||
/// Wie die Menge dasteht. Bei einer offenen Packung zaehlt die Restmenge in
|
||
/// Basiseinheiten - "0,98 Packungen" sagt niemandem etwas, "980 g" schon.
|
||
var mengeText: String {
|
||
if istAngebrochen { return "\(formatAmount(quantity)) \(baseKurz)" }
|
||
let menge = quantity / artikelFaktor
|
||
let label = (packageSize ?? 0) > 0 ? (packageLabel ?? "Packung")
|
||
: (unitName.isEmpty ? baseKurz : unitName)
|
||
return "\(formatAmount(menge)) \(label)"
|
||
}
|
||
|
||
var baseKurz: String {
|
||
switch baseUnit {
|
||
case "gram": return "g"
|
||
case "milliliter": return "ml"
|
||
default: return "Stück"
|
||
}
|
||
}
|
||
|
||
/// Token fuer die Basiseinheit, wie es das Backend beim Buchen erwartet.
|
||
var baseToken: String {
|
||
switch baseUnit {
|
||
case "gram": return "g"
|
||
case "milliliter": return "ml"
|
||
default: return "piece"
|
||
}
|
||
}
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case id, quantity
|
||
case productId = "product_id"
|
||
case productName = "product_name"
|
||
case productBrand = "product_brand"
|
||
case baseUnit = "base_unit"
|
||
case packageSize = "package_size"
|
||
case packageLabel = "package_label"
|
||
case unitName = "unit_name"
|
||
case unitFactor = "unit_factor"
|
||
case bestBefore = "best_before"
|
||
case bestBeforePrecision = "best_before_precision"
|
||
case locationId = "location_id"
|
||
case openedOn = "opened_on"
|
||
case consumeBy = "consume_by"
|
||
case openedQuantity = "opened_quantity"
|
||
}
|
||
}
|
||
|
||
/// Ein Hausmass, das fuer einen Artikel wirklich gilt - "1 EL = 10 g".
|
||
///
|
||
/// Die Mengen haengen an der Kategorie (oder am Artikel), nicht am Mass: Ein
|
||
/// Essloeffel Mehl wiegt anders als einer Zucker.
|
||
struct MeasureAmount: Codable, Identifiable, Hashable {
|
||
let measureId: Int
|
||
let name: String
|
||
let short: String?
|
||
let amount: Double
|
||
let base: String
|
||
let herkunft: String
|
||
|
||
var id: Int { measureId }
|
||
|
||
/// Was beim Buchen als Einheit mitgeschickt wird.
|
||
var token: String { name }
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case name, short, amount, base, herkunft
|
||
case measureId = "measure_id"
|
||
}
|
||
}
|
||
|
||
/// Antwort auf einen Teilverbrauch.
|
||
struct ConsumeResult: Codable {
|
||
/// Wurde die Charge dabei angebrochen?
|
||
let opened: Bool
|
||
/// Die offene Charge danach - nil, wenn sie dabei leer geworden ist.
|
||
let lot: Lot?
|
||
}
|
||
|
||
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
|
||
/// Umrechnung in die andere Art („3 Stück ≙ 250 g"), optional.
|
||
var secondaryBase: String? = nil
|
||
var secondaryCount: Double? = nil
|
||
var secondaryAmount: Double? = 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"
|
||
case secondaryBase = "secondary_base"
|
||
case secondaryCount = "secondary_count"
|
||
case secondaryAmount = "secondary_amount"
|
||
}
|
||
}
|
||
|
||
// MARK: - Listen (Einkaufsliste, bald ablaufend)
|
||
|
||
/// Ein Posten, den jemand selbst auf die Einkaufsliste gesetzt hat - von Hand
|
||
/// oder uebernommen aus einem Rezept.
|
||
///
|
||
/// Die gerechneten Posten (Artikel und Gruppen unter ihrem Mindestbestand)
|
||
/// entstehen bei jedem Aufruf neu; diese hier liegen in der Datenbank und
|
||
/// bleiben, bis sie abgehakt sind. Beides zusammen ergibt erst die Liste.
|
||
struct ShoppingEntry: Codable, Identifiable {
|
||
let id: Int
|
||
/// "product" | "group" | "text"
|
||
let art: String
|
||
let name: String
|
||
/// "2 Packungen" bzw. "150 g" - fertig formatiert vom Server, damit App und
|
||
/// Weboberflaeche dieselbe Sprache sprechen.
|
||
let mengeText: String
|
||
let source: String?
|
||
let recipeName: String?
|
||
let done: Bool
|
||
|
||
var istNotiz: Bool { art == "text" }
|
||
var istGruppe: Bool { art == "group" }
|
||
|
||
/// Zeile unter dem Namen: woher der Posten kommt und wie viel fehlt.
|
||
var untertitel: String {
|
||
[recipeName.map { "für \($0)" }, mengeText.isEmpty ? nil : mengeText]
|
||
.compactMap { $0 }
|
||
.joined(separator: " · ")
|
||
}
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case id, art, name, source, done
|
||
case mengeText = "menge_text"
|
||
case recipeName = "recipe_name"
|
||
}
|
||
}
|
||
|
||
/// Die komplette Einkaufsliste in einer Antwort: gerechnete Bedarfe UND
|
||
/// gespeicherte Eintraege. Die drei Einzelrouten liefern nur die gerechnete
|
||
/// Seite - wer nur sie abfragt, sieht die selbst draufgesetzten Posten nicht.
|
||
struct ShoppingListAll: Codable {
|
||
let products: [ShoppingItem]
|
||
let groups: [GroupShoppingItem]
|
||
let byLocation: [LocationNeeds]
|
||
let entries: [ShoppingEntry]
|
||
/// Kennungen der abgehakten GERECHNETEN Posten. Die App zeigt sie noch
|
||
/// nicht an, liest sie aber mit, damit das Feld nicht ueberrascht.
|
||
let checked: [String]
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case products, groups, entries, checked
|
||
case byLocation = "by_location"
|
||
}
|
||
}
|
||
|
||
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
|
||
/// Wie viele Untergruppen mitgezaehlt werden (0 = keine).
|
||
var subgroupCount: Int? = nil
|
||
|
||
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"
|
||
case subgroupCount = "subgroup_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
|
||
/// Darf fehlen! Eine angebrochene Packung ohne aufgedrucktes MHD hat nur ein
|
||
/// Verbrauchsdatum - und genau die gehoert in diese Liste. Stand hier frueher
|
||
/// ein nicht-optionaler String, scheiterte das Entschluesseln der GANZEN
|
||
/// Liste an einer einzigen solchen Charge, samt Erinnerungen.
|
||
let bestBefore: String?
|
||
let bestBeforePrecision: String?
|
||
let openedOn: String?
|
||
let consumeBy: String?
|
||
/// Das Datum, nach dem gewarnt wird: das fruehere von MHD und
|
||
/// Verbrauchsdatum. Immer gesetzt - der Server rechnet es aus.
|
||
let warnDate: String
|
||
/// "best_before" oder "consume_by" - sagt, welches der beiden Daten gewann.
|
||
let warnSource: String?
|
||
let daysLeft: Int
|
||
let packageSize: Double?
|
||
let packageLabel: String?
|
||
let unitName: String
|
||
let unitFactor: Double
|
||
|
||
var id: Int { lotId }
|
||
|
||
/// Ist die Packung angebrochen und draengt deshalb?
|
||
var istAngebrochen: Bool { warnSource == "consume_by" }
|
||
|
||
/// Beschriftung vor dem Datum. "MHD 05.10." und "verbrauchen bis 05.10."
|
||
/// sind zwei verschiedene Aussagen, und die Liste zeigt beide nebeneinander.
|
||
var fristLabel: String { istAngebrochen ? "verbrauchen bis" : "MHD" }
|
||
|
||
/// Genauigkeit des angezeigten Datums. Ein Verbrauchsdatum ist immer
|
||
/// taggenau, auch wenn auf der Packung nur ein Monat stand.
|
||
var fristPrecision: String? { istAngebrochen ? "day" : bestBeforePrecision }
|
||
|
||
/// Restlaufzeit als kurzer Text. „0 Tage" ergibt keinen Sinn - am Ablauftag
|
||
/// selbst steht „heute". Karte und Liste nutzen dieselbe Formulierung.
|
||
var restlaufzeitText: String {
|
||
if daysLeft < 0 { return "abgelaufen" }
|
||
if daysLeft == 0 { return "heute" }
|
||
if daysLeft == 1 { return "morgen" }
|
||
return "\(daysLeft) Tage"
|
||
}
|
||
|
||
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 openedOn = "opened_on"
|
||
case consumeBy = "consume_by"
|
||
case warnDate = "warn_date"
|
||
case warnSource = "warn_source"
|
||
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
|
||
// Zweiteinheit ebenfalls nur auf Wunsch – ein PATCH aus einer anderen Maske
|
||
// soll die Bruecke nicht unbeabsichtigt loeschen.
|
||
var secondaryBase: String? = nil
|
||
var secondaryCount: Double? = nil
|
||
var secondaryAmount: Double? = nil
|
||
var sendSecondary = 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"
|
||
case secondaryBase = "secondary_base"
|
||
case secondaryCount = "secondary_count"
|
||
case secondaryAmount = "secondary_amount"
|
||
}
|
||
|
||
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) }
|
||
}
|
||
if sendSecondary {
|
||
try container.encode(secondaryBase, forKey: .secondaryBase)
|
||
try container.encode(secondaryCount, forKey: .secondaryCount)
|
||
try container.encode(secondaryAmount, forKey: .secondaryAmount)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Teilverbrauch aus einer Charge.
|
||
///
|
||
/// `unit` nimmt dasselbe wie das Auslagern: die Basiseinheit, eine verwaltete
|
||
/// Einheit, "package" - und die Hausmasse der Kategorie ("EL", "Tasse").
|
||
struct ConsumeRequest: Encodable {
|
||
var quantity: Double
|
||
var unit: String
|
||
var note: String?
|
||
var autoOpen: Bool
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case quantity, unit, note
|
||
case autoOpen = "auto_open"
|
||
}
|
||
}
|
||
|
||
struct LotUpdateRequest: Encodable {
|
||
var quantity: Double?
|
||
var bestBefore: String?
|
||
var bestBeforePrecision: String?
|
||
var locationId: String?
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case quantity
|
||
case bestBefore = "best_before"
|
||
case bestBeforePrecision = "best_before_precision"
|
||
case locationId = "location_id"
|
||
}
|
||
|
||
// Den Lagerort immer senden (auch null), damit „ohne Ort" den gesetzten Ort
|
||
// wieder entfernt. Die übrigen Felder bleiben weg, wenn sie nicht gesetzt
|
||
// sind – das Backend fasst sie dann als „unverändert" auf.
|
||
func encode(to encoder: Encoder) throws {
|
||
var c = encoder.container(keyedBy: CodingKeys.self)
|
||
try c.encodeIfPresent(quantity, forKey: .quantity)
|
||
try c.encodeIfPresent(bestBefore, forKey: .bestBefore)
|
||
try c.encodeIfPresent(bestBeforePrecision, forKey: .bestBeforePrecision)
|
||
try c.encode(locationId, forKey: .locationId)
|
||
}
|
||
}
|
||
|
||
/// Ein Eintrag aus /settings (Schluessel/Wert).
|
||
struct SettingEntry: Codable {
|
||
let key: String
|
||
let value: String
|
||
}
|
||
|
||
/// Gruppe: fasst Bestaende mehrerer Marken zusammen (z.B. "Mehl").
|
||
///
|
||
/// Gruppen bilden einen Graphen, keinen Baum: eine Gruppe darf unter MEHREREN
|
||
/// Obergruppen haengen („Grillwurst" unter „Wurst" UND unter „Grillgut").
|
||
/// Deshalb ``parentIds`` als Liste – und deshalb konformiert GroupItem NICHT zu
|
||
/// ``TreeItem``, das genau einen Elternteil kennt.
|
||
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 minStockUnitId: Int? = nil
|
||
var minStockUnitName: String? = nil
|
||
var minStockUnitFactor: Double? = nil
|
||
var kind: String? = nil
|
||
var packageSize: Double? = nil
|
||
var packageLabel: String? = nil
|
||
var minStockInPackages: Bool? = nil
|
||
var stock: Double? = nil
|
||
var locationMinStocks: [LocationMinStock]? = nil
|
||
var parentIds: [Int]? = nil
|
||
var childIds: [Int]? = nil
|
||
/// Artikel inkl. Untergruppen; ``directProductCount`` nur die eigenen.
|
||
var productCount: Int? = nil
|
||
var directProductCount: Int? = nil
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case id, name, stock, kind
|
||
case minStock = "min_stock"
|
||
case minStockUnitId = "min_stock_unit_id"
|
||
case minStockUnitName = "min_stock_unit_name"
|
||
case minStockUnitFactor = "min_stock_unit_factor"
|
||
case packageSize = "package_size"
|
||
case packageLabel = "package_label"
|
||
case minStockInPackages = "min_stock_in_packages"
|
||
case locationMinStocks = "location_min_stocks"
|
||
case parentIds = "parent_ids"
|
||
case childIds = "child_ids"
|
||
case productCount = "product_count"
|
||
case directProductCount = "direct_product_count"
|
||
}
|
||
|
||
/// Basiseinheiten je Erfassungseinheit – zum Umrechnen der Mindestbestaende,
|
||
/// die in Basiseinheiten gespeichert sind.
|
||
var minFaktor: Double {
|
||
if minStockInPackages == true, let s = packageSize, s > 0 { return s }
|
||
return minStockUnitFactor ?? 1
|
||
}
|
||
|
||
/// Beschriftung der Erfassungseinheit („Glas", „Gramm", …).
|
||
var minEinheit: String {
|
||
if minStockInPackages == true, (packageSize ?? 0) > 0 {
|
||
return packageLabel ?? "Packung"
|
||
}
|
||
if let n = minStockUnitName { return n }
|
||
switch kind {
|
||
case "weight": return "g"
|
||
case "volume": return "ml"
|
||
case "count": return "Stück"
|
||
default: return ""
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Gruppe aendern. Nur mitgeschickte Felder wertet das Backend aus – deshalb
|
||
/// KEIN eigener Encoder: der synthetisierte laesst nil-Optionals weg
|
||
/// (encodeIfPresent). Ein handgeschriebener wuerde ``"name": null`` senden und
|
||
/// damit den Namen loeschen.
|
||
struct GroupUpdateRequest: Codable {
|
||
var name: String? = nil
|
||
var minStock: Double? = nil
|
||
var minStockUnitId: Int? = nil
|
||
var minStockInPackages: Bool? = nil
|
||
var parentIds: [Int]? = nil
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case name
|
||
case minStock = "min_stock"
|
||
case minStockUnitId = "min_stock_unit_id"
|
||
case minStockInPackages = "min_stock_in_packages"
|
||
case parentIds = "parent_ids"
|
||
}
|
||
}
|
||
|
||
/// 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"
|
||
}
|
||
}
|
||
|
||
/// Ein Verleih-Vorgang eines Einzelstuecks. `returnedOn == nil` heisst „noch
|
||
/// draussen"; je Stueck gibt es hoechstens einen solchen offenen Verleih.
|
||
///
|
||
/// `daysOverdue` rechnet der Server, damit App und Web nicht getrennt rechnen -
|
||
/// nil heisst: ohne Frist, zurueck, oder noch nicht faellig.
|
||
struct Loan: Codable, Identifiable, Hashable {
|
||
let id: Int
|
||
let itemId: Int
|
||
let borrower: String
|
||
let reason: String?
|
||
let lentOn: String // "yyyy-MM-dd"
|
||
let dueOn: String?
|
||
let returnedOn: String?
|
||
let createdAt: String
|
||
let username: String?
|
||
let daysOverdue: Int?
|
||
let itemUid: String?
|
||
let productId: Int?
|
||
let productName: String?
|
||
let productBrand: String?
|
||
let locationId: String?
|
||
let locationName: String?
|
||
|
||
var istOffen: Bool { returnedOn == nil }
|
||
var istUeberfaellig: Bool { (daysOverdue ?? 0) > 0 }
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case id, borrower, reason, username
|
||
case itemId = "item_id"
|
||
case lentOn = "lent_on"
|
||
case dueOn = "due_on"
|
||
case returnedOn = "returned_on"
|
||
case createdAt = "created_at"
|
||
case daysOverdue = "days_overdue"
|
||
case itemUid = "item_uid"
|
||
case productId = "product_id"
|
||
case productName = "product_name"
|
||
case productBrand = "product_brand"
|
||
case locationId = "location_id"
|
||
case locationName = "location_name"
|
||
}
|
||
}
|
||
|
||
struct LoanCreateRequest: Encodable {
|
||
let borrower: String
|
||
var reason: String? = nil
|
||
var lentOn: String? = nil
|
||
var dueOn: String? = nil
|
||
/// Zubehoerteile, die mitgehen. Leer = nur das Geraet selbst; der Server
|
||
/// nimmt nie ungefragt etwas mit.
|
||
var mitZubehoer: [Int] = []
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case borrower, reason
|
||
case lentOn = "lent_on"
|
||
case dueOn = "due_on"
|
||
case mitZubehoer = "mit_zubehoer"
|
||
}
|
||
}
|
||
|
||
struct LoanReturnRequest: Encodable {
|
||
var returnedOn: String? = nil
|
||
/// Spiegelbild zu LoanCreateRequest: Ohne das kaeme das Geraet zurueck und
|
||
/// sein Netzteil bliebe ewig „verliehen".
|
||
var mitZubehoer: [Int] = []
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case returnedOn = "returned_on"
|
||
case mitZubehoer = "mit_zubehoer"
|
||
}
|
||
}
|
||
|
||
struct ZubehoerZuordnenRequest: Encodable {
|
||
let childItemId: Int
|
||
enum CodingKeys: String, CodingKey { case childItemId = "child_item_id" }
|
||
}
|
||
|
||
/// Ein verknuepftes Einzelstueck in Kurzform (Zubehoer bzw. das Geraet dazu).
|
||
///
|
||
/// Bewusst nicht `Item`: sonst zoege das Geraet seine Teile und jedes Teil
|
||
/// wieder sein Geraet nach, endlos.
|
||
struct ItemKurz: Codable, Identifiable, Hashable {
|
||
let id: Int
|
||
let uid: String
|
||
let productId: Int
|
||
let productName: String?
|
||
let productBrand: String?
|
||
let locationName: String?
|
||
let verliehen: Bool
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case id, uid, verliehen
|
||
case productId = "product_id"
|
||
case productName = "product_name"
|
||
case productBrand = "product_brand"
|
||
case locationName = "location_name"
|
||
}
|
||
}
|
||
|
||
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]
|
||
/// Der offene Verleih, falls das Stueck gerade draussen ist - sonst nil.
|
||
/// Der Lagerort steht daneben unveraendert: dorthin gehoert es zurueck.
|
||
let loan: Loan?
|
||
/// Zubehoer dieses Geraets und, umgekehrt, das Geraet, zu dem dieses Stueck
|
||
/// gehoert.
|
||
///
|
||
/// Beide optional, und das ist keine Bequemlichkeit: Die App spricht mit
|
||
/// mehreren Serverprofilen, und ein aelterer Server schickt die Felder gar
|
||
/// nicht. Ein nicht-optionales Feld mit Vorgabewert reicht dafuer NICHT -
|
||
/// Swift nimmt den Vorgabewert beim Dekodieren nicht her, es wuerde werfen.
|
||
var zubehoer: [ItemKurz]?
|
||
var gehoertZu: ItemKurz?
|
||
|
||
/// Zubehoer ohne Optional-Gehampel an der Aufrufstelle.
|
||
var teile: [ItemKurz] { zubehoer ?? [] }
|
||
|
||
var istVerliehen: Bool { loan != nil }
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case id, uid, note, currency, documents, loan, zubehoer
|
||
case gehoertZu = "gehoert_zu"
|
||
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?
|
||
/// Obergruppen (mehrere moeglich) – optional, Standard: keine.
|
||
var parentIds: [Int]? = nil
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case name
|
||
case minStock = "min_stock"
|
||
case minStockUnitId = "min_stock_unit_id"
|
||
case parentIds = "parent_ids"
|
||
}
|
||
}
|