App: Obergruppen zuordnen, Mindestbestaende je Ort bearbeiten
Die App konnte bei Gruppen bisher nur umbenennen und Mindestbestaende lesen.
Jetzt gibt es einen vollen Gruppen-Editor (Sources/GroupViews.swift): Name,
Obergruppen als Mehrfachauswahl, Einheit und Mindestbestaende je Ort. Bewusst
eine flache Liste mit "unter: ..." statt TreeMasterView - das Protokoll TreeItem
kennt genau einen Elternteil und wuerde eine Gruppe mit zwei Obergruppen doppelt
zeigen. Die eigene Gruppe und ihre Untergruppen sind in der Auswahl
ausgeschlossen, damit kein Ring entsteht.
Mindestbestaende-Uebersicht ist nach Lagerort gegliedert ("Ueberall" zuerst) und
nicht mehr nur lesend - Gruppenzeilen oeffnen den neuen Editor. Am Artikel
entfaellt das separate Feld "Gesamt": der Bedarf haengt immer an einem Ort, und
"Ueberall" ist einer davon. Das Stammdaten-Speichern schickt min_stock nicht
mehr mit, sonst wuerde es die dort gesetzte Ueberall-Zeile ueberschreiben.
APIClient kann jetzt updateGroup und setGroupLocationMinStock. GroupUpdateRequest
bekommt bewusst KEINEN eigenen Encoder: der synthetisierte laesst nil-Felder weg,
ein handgeschriebener wuerde "name": null senden und den Namen loeschen.
Neue Datei -> xcodegen generate; Projektdatei ist mit committet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,7 +31,6 @@ struct ProductDetailView: View {
|
||||
@State private var shopId: Int?
|
||||
@State private var productUrl = ""
|
||||
// Gesamt-Mindestbestand (nur Gegenstände; Menge in Stück/Artikeleinheit).
|
||||
@State private var minStock = ""
|
||||
@State private var shops: [ShopItem] = []
|
||||
@State private var locations: [StorageLocation] = []
|
||||
@State private var fieldDefs: [FieldDefinition] = []
|
||||
@@ -63,8 +62,6 @@ struct ProductDetailView: View {
|
||||
if packageSize != (current.packageSize.map { formatAmount($0) } ?? "") { return true }
|
||||
if packageLabel != (current.packageLabel ?? "") { return true }
|
||||
if datePrecision != (current.datePrecision == "month" ? "month" : "day") { return true }
|
||||
// Mindestbestand gibt es bei Lebensmitteln und Verbrauchsgegenständen.
|
||||
if minStock != minStockFeld { return true }
|
||||
}
|
||||
if current.isObject {
|
||||
if objMode != currentObjMode { return true }
|
||||
@@ -197,26 +194,31 @@ struct ProductDetailView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// Mindestbestand bei Lebensmitteln UND Verbrauchsgegenständen (Charge) –
|
||||
// in der Anzeigeeinheit (z.B. Gramm/Milliliter), nicht in Packungen.
|
||||
// Mindestbestand bei Lebensmitteln UND Verbrauchsgegenständen (Charge).
|
||||
// Menge je Lagerort und Einzelstücke haben keinen Mindestbestand.
|
||||
if current.foodLike {
|
||||
Section("Mindestbestand") {
|
||||
QuantityField(label: "Gesamt (\(current.unitName))", text: $minStock)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Es gibt kein separates „Gesamt"-Feld mehr: der Bedarf hängt immer
|
||||
// an einem Ort, und „Überall" ist einer davon (der oberste).
|
||||
if current.foodLike {
|
||||
Section {
|
||||
NavigationLink {
|
||||
ProductLocationMinView(product: current) { await reload() }
|
||||
} label: {
|
||||
let n = (current.locationMinStocks ?? []).count
|
||||
Label(n > 0 ? "Mindestbestand je Lagerort (\(n))" : "Mindestbestand je Lagerort",
|
||||
systemImage: "mappin.and.ellipse")
|
||||
let zeilen = current.locationMinStocks ?? []
|
||||
let ueberall = zeilen.first { $0.locationId == nil }
|
||||
Label {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Mindestbestand")
|
||||
Text(minStockUntertitel(zeilen: zeilen, ueberall: ueberall))
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
} icon: {
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
}
|
||||
}
|
||||
} footer: {
|
||||
Text("Bedarf je Lagerort (z.B. Ferienhaus, Zuhause), zusätzlich zum Gesamt-Mindestbestand.")
|
||||
Text("„Überall“ heißt: egal wo, Hauptsache im Haus – Käufe für einen "
|
||||
+ "Lagerort decken das mit ab.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,10 +331,17 @@ struct ProductDetailView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Gesamt-Mindestbestand in der Anzeigeeinheit (Gramm/Milliliter/Stück) – aus
|
||||
/// den Basiseinheiten umgerechnet, unabhängig davon, wie er erfasst wurde.
|
||||
private var minStockFeld: String {
|
||||
current.minStock.map { formatAmount($0 / max(current.unitFactor, 1)) } ?? ""
|
||||
/// Kurzfassung der hinterlegten Mindestbestände für die Übersichtszeile.
|
||||
private func minStockUntertitel(zeilen: [LocationMinStock], ueberall: LocationMinStock?) -> String {
|
||||
if zeilen.isEmpty { return "keiner hinterlegt" }
|
||||
let faktor = max(current.unitFactor, 1)
|
||||
var teile: [String] = []
|
||||
if let u = ueberall {
|
||||
teile.append("Überall \(formatAmount(u.minStock / faktor)) \(current.unitName)")
|
||||
}
|
||||
let orte = zeilen.count - (ueberall == nil ? 0 : 1)
|
||||
if orte > 0 { teile.append("\(orte) \(orte == 1 ? "Lagerort" : "Lagerorte")") }
|
||||
return teile.joined(separator: " · ")
|
||||
}
|
||||
|
||||
/// Aktuelle Verwaltungsart des geladenen Artikels.
|
||||
@@ -350,7 +359,6 @@ struct ProductDetailView: View {
|
||||
categoryId = current.categoryId
|
||||
shopId = current.shopId
|
||||
productUrl = current.productUrl ?? ""
|
||||
minStock = minStockFeld
|
||||
objMode = currentObjMode
|
||||
fieldValues = Self.stringValues(current.fieldValues)
|
||||
}
|
||||
@@ -411,9 +419,6 @@ struct ProductDetailView: View {
|
||||
busy = true
|
||||
defer { busy = false }
|
||||
do {
|
||||
// Mindestbestand in der Anzeigeeinheit erfasst → Basiseinheiten.
|
||||
let minBase = Double(minStock.replacingOccurrences(of: ",", with: "."))
|
||||
.map { $0 * current.unitFactor }
|
||||
if current.foodLike {
|
||||
// Lebensmittel ODER Verbrauchsgegenstand: als Charge mit Einheit/
|
||||
// Packung/MHD/Gruppe. Verbrauchsgegenstände (isObject) tragen
|
||||
@@ -427,11 +432,9 @@ struct ProductDetailView: View {
|
||||
groupId: groupId,
|
||||
categoryId: categoryId
|
||||
)
|
||||
// Mindestbestand für Lebensmittel UND Verbrauchsgegenstände.
|
||||
req.minStock = minBase
|
||||
req.minStockInPackages = false
|
||||
req.minStockUnitId = nil
|
||||
req.sendMinStock = true
|
||||
// Der Mindestbestand wird in ProductLocationMinView gepflegt und
|
||||
// hier bewusst NICHT mitgeschickt – sonst überschriebe das
|
||||
// Stammdaten-Speichern die dort gesetzte „Überall"-Zeile.
|
||||
if current.isObject {
|
||||
var fv: [String: String?] = [:]
|
||||
for def in fieldDefs { fv[String(def.id)] = fieldValues[String(def.id)] ?? "" }
|
||||
@@ -660,22 +663,21 @@ struct ProductLocationMinView: View {
|
||||
@State private var busy = false
|
||||
@State private var error: String?
|
||||
|
||||
// Speicherung in Artikeleinheiten, Anzeige in der Anzeigeeinheit (Gramm/ml/Stück).
|
||||
private var artToDisp: Double {
|
||||
let disp = product.unitFactor == 0 ? 1 : product.unitFactor
|
||||
return product.articleUnitFactor / disp
|
||||
}
|
||||
// Gespeichert wird in Basiseinheiten, erfasst in der Anzeigeeinheit (g/ml/Stück).
|
||||
private var dispFaktor: Double { product.unitFactor == 0 ? 1 : product.unitFactor }
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Text("Bedarf je Lagerort (z.B. Ferienhaus, Zuhause). Menge in \(product.unitName).")
|
||||
Text("Was soll wo immer da sein? „Überall“ heißt: egal wo, Hauptsache "
|
||||
+ "im Haus – Käufe für einen Lagerort decken das mit ab. "
|
||||
+ "Menge in \(product.unitName).")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
ForEach($rows) { $row in
|
||||
HStack {
|
||||
Picker("Lagerort", selection: $row.locationId) {
|
||||
Text("– wählen –").tag(String?.none)
|
||||
Picker("Ort", selection: $row.locationId) {
|
||||
Text(UEBERALL_NAME).tag(String?.some(UEBERALL_ID))
|
||||
ForEach(locations) { l in Text(l.path(in: locations)).tag(String?.some(l.id)) }
|
||||
}
|
||||
TextField("Menge", text: $row.amount)
|
||||
@@ -691,15 +693,15 @@ struct ProductLocationMinView: View {
|
||||
}
|
||||
}
|
||||
Button {
|
||||
rows.append(MinRow(locationId: nil, amount: ""))
|
||||
rows.append(MinRow(locationId: UEBERALL_ID, amount: ""))
|
||||
} label: {
|
||||
Label("Lagerort hinzufügen", systemImage: "plus")
|
||||
Label("Mindestbestand hinzufügen", systemImage: "plus")
|
||||
}
|
||||
if let error {
|
||||
Section { Text(error).foregroundStyle(.red).font(.callout) }
|
||||
}
|
||||
}
|
||||
.navigationTitle("Bedarf je Lagerort")
|
||||
.navigationTitle("Mindestbestand")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
@@ -712,8 +714,10 @@ struct ProductLocationMinView: View {
|
||||
|
||||
private func load() async {
|
||||
locations = (try? await APIClient.shared.locations()) ?? []
|
||||
// „Überall" ist der Ort nil – im Picker braucht es einen Platzhalter.
|
||||
rows = (product.locationMinStocks ?? []).map {
|
||||
MinRow(locationId: $0.locationId, amount: formatAmount($0.minStock * artToDisp))
|
||||
MinRow(locationId: $0.locationId ?? UEBERALL_ID,
|
||||
amount: formatAmount($0.minStock / dispFaktor))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -727,8 +731,10 @@ struct ProductLocationMinView: View {
|
||||
let wert = Double(row.amount.replacingOccurrences(of: ",", with: ".")) ?? 0
|
||||
if wert > 0 {
|
||||
gesehen.insert(loc)
|
||||
// Eingabe in Anzeigeeinheit → Artikeleinheiten (so gespeichert).
|
||||
list.append(LocationMinStockIn(locationId: loc, minStock: wert / artToDisp))
|
||||
// Eingabe in Anzeigeeinheit → Basiseinheiten (so gespeichert).
|
||||
list.append(LocationMinStockIn(
|
||||
locationId: loc == UEBERALL_ID ? nil : loc,
|
||||
minStock: wert * dispFaktor))
|
||||
}
|
||||
}
|
||||
do {
|
||||
|
||||
Reference in New Issue
Block a user