diff --git a/ios/Sources/ListViews.swift b/ios/Sources/ListViews.swift index 31e4cf9..074dcb2 100644 --- a/ios/Sources/ListViews.swift +++ b/ios/Sources/ListViews.swift @@ -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 } diff --git a/ios/Sources/Models.swift b/ios/Sources/Models.swift index 2abf5b3..d2da64f 100644 --- a/ios/Sources/Models.swift +++ b/ios/Sources/Models.swift @@ -586,6 +586,18 @@ struct SettingEntry: Codable { 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 minStockUnitName: String? = nil + var stock: Double? = nil + var locationMinStocks: [LocationMinStock]? = nil + + enum CodingKeys: String, CodingKey { + case id, name, stock + case minStock = "min_stock" + case minStockUnitName = "min_stock_unit_name" + case locationMinStocks = "location_min_stocks" + } } /// Kategorie: ordnet die Artikelliste und bestimmt die Verwaltungsart