Gegenstands-Verwaltung (Non-Food) neben Lebensmitteln

Die Kategorie bestimmt die Verwaltungsart (food/object). Gegenstaende:
Menge je Lagerort statt Chargen/MHD, Umlagern (ohne Grund) und Entfernen
mit Pflicht-Grund samt Entnahme-Statistik. Beliebig viele eigene Felder
je Kategorie (vererbt an Unterkategorien), "gekauft bei" ueber eine
verwaltbare Shop-Liste und ein Produktlink. Barcode-Lookup zusaetzlich
ueber Open Products Facts.

Der Lebensmittel-Teil (Chargen/MHD/FEFO) bleibt unveraendert. Umgesetzt in
Backend (FastAPI, +Tests gruen), Web (React) und iOS (SwiftUI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-25 15:33:56 +02:00
parent 580afcc133
commit 5b952524d7
29 changed files with 3116 additions and 115 deletions

View File

@@ -66,9 +66,17 @@ struct Product: Codable, Identifiable, Hashable {
/// 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?]?
enum CodingKeys: String, CodingKey {
case id, barcode, name, brand, stock, kind, barcodes
case id, barcode, name, brand, stock, kind, barcodes, tracking
case datePrecision = "date_precision"
case categoryId = "category_id"
case categoryName = "category_name"
@@ -83,8 +91,15 @@ struct Product: Codable, Identifiable, Hashable {
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" }
/// Bezeichnung der Artikeleinheit (Gebinde, sonst Produkteinheit).
var articleUnitLabel: String {
if let size = packageSize, size > 0 { return packageLabel ?? "Packung" }
@@ -353,6 +368,10 @@ struct ProductUpdateRequest: Encodable {
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
@@ -361,6 +380,9 @@ struct ProductUpdateRequest: Encodable {
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 {
@@ -372,6 +394,10 @@ struct ProductUpdateRequest: Encodable {
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) }
}
}
@@ -399,16 +425,143 @@ struct GroupItem: Codable, Identifiable, Hashable {
let name: String
}
/// Kategorie: ordnet nur die Artikelliste, verschachtelbar (Suesswaren -> Schokolade).
/// 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
case id, name, tracking
case parentId = "parent_id"
}
var isObject: Bool { tracking == "object" }
}
/// 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: - Uebersicht und Verlauf