iOS: Verbrauchsgegenstand als dritte Gegenstands-Art (wie ein Lebensmittel)

Neuer Artikel: 3-Wege-Verwaltung fuer Gegenstaende (Menge je Lagerort /
Einzelstuecke / Verbrauchsgegenstand). Verbrauchsgegenstaende laufen wie
Lebensmittel als Charge (Einheit, Packung, MHD, Gruppe, Chargen-Ansicht,
Mindestbestand mit Einheit) - foodLike = kein Gegenstand ODER bulk. Menge-
Gegenstaende koennen jetzt ebenfalls einer Gruppe zugeordnet werden.
ProductDetailView entsprechend auf foodLike umgestellt; NewProductRequest
und ProductUpdateRequest um bulk/individual/base_unit erweitert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-27 20:51:07 +02:00
parent f287f8f768
commit 72fa2ce37d
3 changed files with 116 additions and 52 deletions

View File

@@ -50,16 +50,19 @@ struct ProductDetailView: View {
if name != current.name { return true }
if brand != (current.brand ?? "") { return true }
if categoryId != current.categoryId { return true }
if current.isObject {
if !current.isIndividual, shopId != current.shopId { return true }
if productUrl != (current.productUrl ?? "") { return true }
if minStock != (current.minStockDisplay.map { formatAmount($0) } ?? "") { return true }
if fieldValues != Self.stringValues(current.fieldValues) { return true }
} else {
// Gruppe gibt es für alles außer Einzelstücke.
if !current.isIndividual, groupId != current.groupId { return true }
// Packung/MHD nur bei Lebensmitteln und Verbrauchsgegenständen.
if current.foodLike {
if packageSize != (current.packageSize.map { formatAmount($0) } ?? "") { return true }
if packageLabel != (current.packageLabel ?? "") { return true }
if datePrecision != (current.datePrecision == "month" ? "month" : "day") { return true }
if groupId != current.groupId { return true }
}
if current.isObject {
if !current.isIndividual && !current.isBulk, shopId != current.shopId { return true }
if !current.isBulk, productUrl != (current.productUrl ?? "") { return true }
if minStock != (current.minStockDisplay.map { formatAmount($0) } ?? "") { return true }
if fieldValues != Self.stringValues(current.fieldValues) { return true }
}
return false
}
@@ -82,10 +85,12 @@ struct ProductDetailView: View {
if current.isObject && current.isIndividual {
ProductItemsSection(product: current, onChanged: { await reload() },
onFinish: { dismiss() })
} else if current.isObject {
} else if current.isObject && !current.isBulk {
// Menge je Lagerort (Verbrauchsgegenstände laufen als Charge, s.u.).
ObjectStockSection(product: current, locations: locations, lots: lots,
onChanged: { await reload() })
} else {
// Lebensmittel und Verbrauchsgegenstände: Vorrat aus den Chargen.
Section("Bestand") {
LabeledContent("Vorrat",
value: "\(formatAmount(current.stockInArticleUnits)) \(current.articleUnitLabel)")
@@ -101,7 +106,7 @@ struct ProductDetailView: View {
Section("Artikel") {
LabeledField(label: "Name", text: $name)
LabeledField(label: "Marke", text: $brand)
if !current.isObject {
if current.foodLike {
QuantityField(label: "Packungsgröße", text: $packageSize,
suffix: display.baseUnitLabel(current.baseUnit))
Picker("Bezeichnung", selection: $packageLabel) {
@@ -123,7 +128,9 @@ struct ProductDetailView: View {
: "Nur Lebensmittel-Kategorien.")
}
if current.isObject {
// Bezugsquelle/Link nur für Menge je Lagerort und Einzelstücke
// Verbrauchsgegenstände laufen wie Lebensmittel und lassen das weg.
if current.isObject && !current.isBulk {
// Gekauft bei" gehört bei Einzelstücken zum einzelnen Stück
// (jedes kann woanders gekauft sein), nicht ans Modell wie im
// Web wird der Shop hier dann ausgeblendet. Der Produktlink
@@ -140,14 +147,10 @@ struct ProductDetailView: View {
Link("Im Onlineshop öffnen", destination: url)
}
}
if !fieldDefs.isEmpty {
Section("Eigene Felder") {
ForEach(fieldDefs) { def in
ObjectFieldRow(def: def, value: fieldBinding(def))
}
}
}
} else {
}
// Gruppe für alles außer Einzelstücke (Lebensmittel, Verbrauchs-
// gegenstand, Menge je Lagerort).
if !current.isIndividual {
Section {
Picker("Gruppe", selection: $groupId) {
Text(" keine ").tag(Int?.none)
@@ -156,14 +159,23 @@ struct ProductDetailView: View {
}
}
} footer: {
Text("Gruppe: zählt Bestände mehrerer Marken zusammen. Der EAN-Code "
Text("Gruppe: zählt Bestände mehrerer Artikel zusammen. Der EAN-Code "
+ "wandert bei einem Wechsel mit.")
}
}
// Eigene Felder für alle Gegenstände mit Kategorie-Feldern.
if current.isObject && !fieldDefs.isEmpty {
Section("Eigene Felder") {
ForEach(fieldDefs) { def in
ObjectFieldRow(def: def, value: fieldBinding(def))
}
}
}
if current.isObject {
Section("Mindestbestand") {
QuantityField(label: "Gesamt (Stück)", text: $minStock)
QuantityField(label: "Gesamt (\(current.isBulk ? current.articleUnitLabel : "Stück"))",
text: $minStock)
}
}
@@ -191,7 +203,7 @@ struct ProductDetailView: View {
.disabled(busy || name.isEmpty)
}
if !current.isObject {
if current.foodLike {
Section("Chargen") {
ForEach(lots) { lot in
Button {
@@ -314,8 +326,35 @@ struct ProductDetailView: View {
busy = true
defer { busy = false }
do {
if current.isObject {
// Gegenstände: keine Lebensmittel-Felder, dafür Shop, Link und eigene Felder.
let minBase = Double(minStock.replacingOccurrences(of: ",", with: "."))
.map { $0 * current.articleUnitFactor }
if current.foodLike {
// Lebensmittel ODER Verbrauchsgegenstand: als Charge mit Einheit/
// Packung/MHD/Gruppe. Verbrauchsgegenstände (isObject) tragen
// zusätzlich eigene Felder und einen Mindestbestand.
var req = ProductUpdateRequest(
name: name,
brand: brand.isEmpty ? nil : brand,
packageSize: Double(packageSize.replacingOccurrences(of: ",", with: ".")),
packageLabel: packageLabel.isEmpty ? nil : packageLabel,
datePrecision: datePrecision,
groupId: groupId,
categoryId: categoryId
)
if current.isObject {
var fv: [String: String?] = [:]
for def in fieldDefs { fv[String(def.id)] = fieldValues[String(def.id)] ?? "" }
req.fieldValues = fv
req.minStock = minBase
req.minStockInPackages = (current.packageSize ?? 0) > 0
req.minStockUnitId = nil
req.sendMinStock = true
}
current = try await APIClient.shared.updateProduct(id: current.id, req)
if current.isObject { fieldValues = Self.stringValues(current.fieldValues) }
} else {
// Gegenstand: Menge je Lagerort oder Einzelstücke. Menge-
// Gegenstände dürfen jetzt ebenfalls in eine Gruppe.
var fv: [String: String?] = [:]
for def in fieldDefs { fv[String(def.id)] = fieldValues[String(def.id)] ?? "" }
current = try await APIClient.shared.updateProduct(
@@ -324,32 +363,18 @@ struct ProductDetailView: View {
name: name,
brand: brand.isEmpty ? nil : brand,
packageSize: nil, packageLabel: nil, datePrecision: nil,
groupId: nil, categoryId: categoryId,
groupId: current.isIndividual ? nil : groupId, categoryId: categoryId,
// Einzelstücke tragen den Shop je Stück nichts ans Modell.
shopId: current.isIndividual ? nil : shopId,
productUrl: productUrl.isEmpty ? nil : productUrl,
fieldValues: fv,
minStock: Double(minStock.replacingOccurrences(of: ",", with: "."))
.map { $0 * current.articleUnitFactor },
minStock: minBase,
minStockInPackages: (current.packageSize ?? 0) > 0,
minStockUnitId: nil,
sendMinStock: true
)
)
fieldValues = Self.stringValues(current.fieldValues)
} else {
current = try await APIClient.shared.updateProduct(
id: current.id,
ProductUpdateRequest(
name: name,
brand: brand.isEmpty ? nil : brand,
packageSize: Double(packageSize.replacingOccurrences(of: ",", with: ".")),
packageLabel: packageLabel.isEmpty ? nil : packageLabel,
datePrecision: datePrecision,
groupId: groupId,
categoryId: categoryId
)
)
}
status = "Gespeichert."
} catch {