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:
@@ -76,7 +76,10 @@ struct ShoppingListView: View {
|
||||
ListRow(
|
||||
systemImage: "square.stack.3d.up",
|
||||
title: group.name,
|
||||
subtitle: "fehlt \(formatAmount(group.deficit)) \(group.unitName) · \(group.productCount) Artikel"
|
||||
subtitle: "fehlt \(formatAmount(group.deficit)) \(group.unitName)"
|
||||
+ " · \(group.productCount) Artikel"
|
||||
+ ((group.subgroupCount ?? 0) > 0
|
||||
? " · inkl. \(group.subgroupCount ?? 0) Untergruppen" : "")
|
||||
) { GroupBadge() }
|
||||
}
|
||||
}
|
||||
@@ -185,10 +188,15 @@ struct ExpiringView: View {
|
||||
|
||||
// MARK: - Mindestbestände
|
||||
|
||||
/// Übersicht aller Mindestbestände: Produkte und Gruppen, jeweils Gesamt UND je
|
||||
/// Lagerort (Soll in der Anzeige-/Gruppeneinheit). Produktzeilen öffnen den
|
||||
/// Artikel zum Bearbeiten; das Setzen von Gruppen-/Lagerort-Bedarfen läuft übers
|
||||
/// Web.
|
||||
/// Übersicht aller Mindestbestände – nach LAGERORT gegliedert.
|
||||
///
|
||||
/// Ein Mindestbestand ist immer ein Bedarf an einem Ort. „Überall" (egal wo,
|
||||
/// Hauptsache im Haus) ist dabei der oberste Ort; Käufe für einen einzelnen
|
||||
/// Lagerort decken ihn mit ab. Mengen kommen in Basiseinheiten und werden hier
|
||||
/// in die Erfassungseinheit des Ziels umgerechnet.
|
||||
///
|
||||
/// Artikelzeilen öffnen den Artikel, Gruppenzeilen die Gruppe – beide lassen
|
||||
/// sich dort bearbeiten.
|
||||
struct MinStockView: View {
|
||||
@State private var products: [Product] = []
|
||||
@State private var groups: [GroupItem] = []
|
||||
@@ -198,71 +206,92 @@ struct MinStockView: View {
|
||||
private struct MinRow: Identifiable {
|
||||
let id: String
|
||||
let title: String
|
||||
let ort: String // "Gesamt" oder Lagerort-Name
|
||||
let soll: String
|
||||
let bestand: String?
|
||||
let productId: Int? // Tippziel (nur Produkte)
|
||||
let isGroup: Bool
|
||||
let productId: Int? // Tippziel (Artikel)
|
||||
let groupId: Int? // Tippziel (Gruppe)
|
||||
let unter: Bool
|
||||
let untergruppen: Int
|
||||
}
|
||||
|
||||
private var rows: [MinRow] {
|
||||
var out: [MinRow] = []
|
||||
private struct OrtBlock: Identifiable {
|
||||
let id: String
|
||||
let name: String
|
||||
let zeilen: [MinRow]
|
||||
}
|
||||
|
||||
/// Alle Zeilen, gebündelt je Ort. „Überall" (Ort nil) steht zuerst.
|
||||
private var bloecke: [OrtBlock] {
|
||||
var jeOrt: [String: [MinRow]] = [:]
|
||||
var namen: [String: String] = [UEBERALL_ID: UEBERALL_NAME]
|
||||
|
||||
func merken(_ locationId: String?, _ locationName: String?, _ row: MinRow) {
|
||||
let key = locationId ?? UEBERALL_ID
|
||||
if let n = locationName, locationId != nil { namen[key] = n }
|
||||
jeOrt[key, default: []].append(row)
|
||||
}
|
||||
|
||||
for p in products.sorted(by: { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }) {
|
||||
let disp = p.unitFactor == 0 ? 1 : p.unitFactor // Basis -> Anzeigeeinheit
|
||||
let art = p.articleUnitFactor == 0 ? 1 : p.articleUnitFactor
|
||||
if let ms = p.minStock, ms > 0 {
|
||||
out.append(MinRow(
|
||||
id: "p\(p.id)g", title: p.name, ort: "Gesamt",
|
||||
soll: "\(formatAmount(ms / disp)) \(p.unitName)",
|
||||
bestand: "\(formatAmount(p.stock / disp)) \(p.unitName)",
|
||||
productId: p.id, isGroup: false, unter: p.stock < ms))
|
||||
}
|
||||
for l in (p.locationMinStocks ?? []) {
|
||||
// je Lagerort in Artikeleinheiten gespeichert -> Anzeigeeinheit.
|
||||
out.append(MinRow(
|
||||
id: "p\(p.id)l\(l.locationId)", title: p.name,
|
||||
ort: l.locationName ?? "Lagerort",
|
||||
soll: "\(formatAmount(l.minStock * art / disp)) \(p.unitName)",
|
||||
bestand: nil, productId: p.id, isGroup: false, unter: false))
|
||||
merken(l.locationId, l.locationName, MinRow(
|
||||
id: "p\(p.id)l\(l.id)", title: p.name,
|
||||
soll: "\(formatAmount(l.minStock / disp)) \(p.unitName)",
|
||||
bestand: l.stock.map { "\(formatAmount($0 / disp)) \(p.unitName)" },
|
||||
productId: p.id, groupId: nil,
|
||||
unter: (l.stock ?? 0) < l.minStock, untergruppen: 0))
|
||||
}
|
||||
}
|
||||
for g in groups.sorted(by: { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }) {
|
||||
let unit = g.minStockUnitName ?? ""
|
||||
if let ms = g.minStock, ms > 0 {
|
||||
out.append(MinRow(
|
||||
id: "g\(g.id)g", title: g.name, ort: "Gesamt",
|
||||
soll: "\(formatAmount(ms)) \(unit)",
|
||||
bestand: g.stock.map { "\(formatAmount($0)) \(unit)" },
|
||||
productId: nil, isGroup: true, unter: (g.stock ?? 0) < ms))
|
||||
}
|
||||
let faktor = g.minFaktor == 0 ? 1 : g.minFaktor
|
||||
let einheit = g.minEinheit
|
||||
for l in (g.locationMinStocks ?? []) {
|
||||
out.append(MinRow(
|
||||
id: "g\(g.id)l\(l.locationId)", title: g.name,
|
||||
ort: l.locationName ?? "Lagerort",
|
||||
soll: "\(formatAmount(l.minStock)) \(unit)",
|
||||
bestand: nil, productId: nil, isGroup: true, unter: false))
|
||||
merken(l.locationId, l.locationName, MinRow(
|
||||
id: "g\(g.id)l\(l.id)", title: g.name,
|
||||
soll: "\(formatAmount(l.minStock / faktor)) \(einheit)",
|
||||
bestand: l.stock.map { "\(formatAmount($0 / faktor)) \(einheit)" },
|
||||
productId: nil, groupId: g.id,
|
||||
unter: (l.stock ?? 0) < l.minStock,
|
||||
untergruppen: g.childIds?.count ?? 0))
|
||||
}
|
||||
}
|
||||
return out
|
||||
|
||||
// „Überall" zuerst, danach die Lagerorte alphabetisch.
|
||||
let schluessel = jeOrt.keys.sorted { a, b in
|
||||
if a == UEBERALL_ID { return true }
|
||||
if b == UEBERALL_ID { return false }
|
||||
return (namen[a] ?? "").localizedCaseInsensitiveCompare(namen[b] ?? "") == .orderedAscending
|
||||
}
|
||||
return schluessel.map {
|
||||
OrtBlock(id: $0, name: namen[$0] ?? "Lagerort",
|
||||
zeilen: jeOrt[$0] ?? [])
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
if !busy && rows.isEmpty {
|
||||
if !busy && bloecke.isEmpty {
|
||||
Text("Noch keine Mindestbestände gesetzt.").foregroundStyle(.secondary)
|
||||
}
|
||||
ForEach(rows) { r in
|
||||
if let pid = r.productId, let p = products.first(where: { $0.id == pid }) {
|
||||
NavigationLink { ProductDetailView(product: p) } label: { zeile(r) }
|
||||
} else {
|
||||
zeile(r)
|
||||
ForEach(bloecke) { block in
|
||||
Section(block.id == UEBERALL_ID ? "Überall (egal wo)" : block.name) {
|
||||
ForEach(block.zeilen) { r in
|
||||
if let pid = r.productId, let p = products.first(where: { $0.id == pid }) {
|
||||
NavigationLink { ProductDetailView(product: p) } label: { zeile(r) }
|
||||
} else if let gid = r.groupId, let g = groups.first(where: { $0.id == gid }) {
|
||||
NavigationLink {
|
||||
GroupEditor(item: g, all: groups) { Task { await load() } }
|
||||
} label: { zeile(r) }
|
||||
} else {
|
||||
zeile(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Mindestbestände")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.overlay { if busy && rows.isEmpty { ProgressView() } }
|
||||
.overlay { if busy && bloecke.isEmpty { ProgressView() } }
|
||||
.refreshable { await load() }
|
||||
.onAppear { Task { await load() } }
|
||||
.alert("Fehler", isPresented: Binding(get: { error != nil }, set: { if !$0 { error = nil } })) {
|
||||
@@ -276,13 +305,18 @@ struct MinStockView: View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack(spacing: 6) {
|
||||
Text(r.title)
|
||||
if r.isGroup {
|
||||
if r.groupId != nil {
|
||||
Text("Gruppe").font(.caption2)
|
||||
.padding(.horizontal, 6).padding(.vertical, 1)
|
||||
.background(Color.marke.opacity(0.2), in: Capsule())
|
||||
}
|
||||
if r.untergruppen > 0 {
|
||||
Text("inkl. \(r.untergruppen)").font(.caption2)
|
||||
.padding(.horizontal, 6).padding(.vertical, 1)
|
||||
.background(Color.secondary.opacity(0.15), in: Capsule())
|
||||
}
|
||||
}
|
||||
Text("\(r.ort) · Soll \(r.soll)" + (r.bestand.map { " · Bestand \($0)" } ?? ""))
|
||||
Text("Soll \(r.soll)" + (r.bestand.map { " · Bestand \($0)" } ?? ""))
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
|
||||
Reference in New Issue
Block a user