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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user