iOS: Mindestbestaende-Uebersicht zeigt Gruppen und je-Lagerort
Bisher nur globale Produkt-Mindestbestaende - wer (wie im Screenshot) nur Gruppen- oder Lagerort-Bedarfe hatte, sah "keine gesetzt". Jetzt Produkte UND Gruppen, jeweils Gesamt und je Lagerort, mit Soll (Anzeige-/Gruppeneinheit) und "niedrig"- Hinweis. Produktzeilen oeffnen den Artikel zum Bearbeiten. GroupItem traegt dafuer die Mindestbestand-Felder. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -190,31 +190,84 @@ struct ExpiringView: View {
|
||||
|
||||
// MARK: - Mindestbestände
|
||||
|
||||
/// Übersicht aller Artikel mit gesetztem Mindestbestand (Soll vs. Bestand, in der
|
||||
/// Anzeigeeinheit). Tippen öffnet den Artikel zum Bearbeiten des Mindestbestands.
|
||||
/// Ü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.
|
||||
struct MinStockView: View {
|
||||
@State private var products: [Product] = []
|
||||
@State private var groups: [GroupItem] = []
|
||||
@State private var busy = true
|
||||
@State private var error: String?
|
||||
|
||||
private var mitBestand: [Product] {
|
||||
products.filter { ($0.minStock ?? 0) > 0 }
|
||||
.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
|
||||
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 unter: Bool
|
||||
}
|
||||
|
||||
private var rows: [MinRow] {
|
||||
var out: [MinRow] = []
|
||||
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))
|
||||
}
|
||||
}
|
||||
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))
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
if !busy && mitBestand.isEmpty {
|
||||
Text("Noch keine Mindestbestände gesetzt. Im Artikel beim Feld Mindestbestand festlegen.")
|
||||
.foregroundStyle(.secondary)
|
||||
if !busy && rows.isEmpty {
|
||||
Text("Noch keine Mindestbestände gesetzt.").foregroundStyle(.secondary)
|
||||
}
|
||||
ForEach(mitBestand) { p in
|
||||
NavigationLink { ProductDetailView(product: p) } label: { row(p) }
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Mindestbestände")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.overlay { if busy && products.isEmpty { ProgressView() } }
|
||||
.overlay { if busy && rows.isEmpty { ProgressView() } }
|
||||
.refreshable { await load() }
|
||||
.onAppear { Task { await load() } }
|
||||
.alert("Fehler", isPresented: Binding(get: { error != nil }, set: { if !$0 { error = nil } })) {
|
||||
@@ -223,20 +276,22 @@ struct MinStockView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func row(_ p: Product) -> some View {
|
||||
let faktor = p.unitFactor == 0 ? 1 : p.unitFactor
|
||||
let soll = (p.minStock ?? 0) / faktor
|
||||
let bestand = p.stock / faktor
|
||||
let unter = p.stock < (p.minStock ?? 0)
|
||||
private func zeile(_ r: MinRow) -> some View {
|
||||
HStack(spacing: 12) {
|
||||
ProductThumb(productId: p.id)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(p.name)
|
||||
Text("Soll \(formatAmount(soll)) · Bestand \(formatAmount(bestand)) \(p.unitName)")
|
||||
HStack(spacing: 6) {
|
||||
Text(r.title)
|
||||
if r.isGroup {
|
||||
Text("Gruppe").font(.caption2)
|
||||
.padding(.horizontal, 6).padding(.vertical, 1)
|
||||
.background(Color.accentColor.opacity(0.2), in: Capsule())
|
||||
}
|
||||
}
|
||||
Text("\(r.ort) · Soll \(r.soll)" + (r.bestand.map { " · Bestand \($0)" } ?? ""))
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
if unter {
|
||||
if r.unter {
|
||||
Text("niedrig").font(.caption2)
|
||||
.padding(.horizontal, 8).padding(.vertical, 3)
|
||||
.background(Color.orange.opacity(0.2), in: Capsule())
|
||||
@@ -248,9 +303,11 @@ struct MinStockView: View {
|
||||
busy = true
|
||||
defer { busy = false }
|
||||
do {
|
||||
let geladen = try await APIClient.shared.searchProducts("")
|
||||
products = geladen
|
||||
Task { await ProductImageCache.shared.preload(geladen) }
|
||||
async let ps = APIClient.shared.searchProducts("")
|
||||
async let gs = APIClient.shared.groups()
|
||||
products = try await ps
|
||||
groups = try await gs
|
||||
Task { await ProductImageCache.shared.preload(products) }
|
||||
} catch {
|
||||
self.error = error.localizedDescription
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user