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:
Scarriffle
2026-08-16 00:02:43 +02:00
parent 0537a2b53f
commit f33054c534
8 changed files with 576 additions and 133 deletions

View File

@@ -146,30 +146,50 @@ struct Product: Codable, Identifiable, Hashable {
}
/// 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 locationId: String?
let locationName: String?
let minStock: Double
var id: String { locationId }
/// 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 Artikeleinheiten).
/// Ein Mindestbestand-Eintrag zum Speichern (Menge in Basiseinheiten).
/// ``locationId`` nil = Ueberall".
struct LocationMinStockIn: Codable {
let locationId: String
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
@@ -390,6 +410,8 @@ struct GroupShoppingItem: Codable, Identifiable {
let deficit: Double
let unitName: String
let productCount: Int
/// Wie viele Untergruppen mitgezaehlt werden (0 = keine).
var subgroupCount: Int? = nil
var id: Int { groupId }
@@ -399,6 +421,7 @@ struct GroupShoppingItem: Codable, Identifiable {
case minStock = "min_stock"
case unitName = "unit_name"
case productCount = "product_count"
case subgroupCount = "subgroup_count"
}
}
@@ -637,20 +660,86 @@ struct SettingEntry: Codable {
}
/// 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
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"
}
}
@@ -1283,10 +1372,13 @@ 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"
}
}