import SwiftUI /// Gemeinsame Zahlenformatierung der Listen. func formatAmount(_ value: Double) -> String { value == value.rounded() ? String(Int(value)) : String(format: "%.2f", value) } /// MHD lesbar: Monatsangaben ohne Tag ("09/2026"). func formatBestBefore(_ raw: String?, precision: String?) -> String { guard let raw else { return "ohne MHD" } guard precision == "month" else { return raw } let teile = raw.split(separator: "-") guard teile.count >= 2 else { return raw } return "\(teile[1])/\(teile[0])" } // MARK: - Einkaufsliste struct ShoppingListView: View { @State private var items: [ShoppingItem] = [] @State private var groups: [GroupShoppingItem] = [] @State private var busy = true @State private var error: String? var body: some View { List { if let error { Section { Text(error).foregroundStyle(.red).font(.callout) } } if !items.isEmpty { Section("Artikel unter Mindestbestand") { ForEach(items) { item in VStack(alignment: .leading, spacing: 2) { Text(item.name) Text("fehlt \(formatAmount(item.deficit)) · Bestand \(formatAmount(item.stock)) von \(formatAmount(item.minStock))") .font(.caption).foregroundStyle(.secondary) } } } } if !groups.isEmpty { Section("Gruppen unter Mindestbestand") { ForEach(groups) { group in VStack(alignment: .leading, spacing: 2) { Text(group.name) Text("fehlt \(formatAmount(group.deficit)) \(group.unitName) · \(group.productCount) Artikel") .font(.caption).foregroundStyle(.secondary) } } } } if items.isEmpty && groups.isEmpty && !busy { Text("Nichts einzukaufen – alle Mindestbestände sind gedeckt.") .foregroundStyle(.secondary) } } .navigationTitle("Einkaufsliste") .navigationBarTitleDisplayMode(.inline) .refreshable { await load() } .task { await load() } } private func load() async { busy = true defer { busy = false } do { items = try await APIClient.shared.shoppingList() groups = try await APIClient.shared.shoppingGroups() error = nil } catch { self.error = error.localizedDescription } } } // MARK: - Bald ablaufend struct ExpiringView: View { @State private var items: [ExpiringItem] = [] @State private var busy = true @State private var error: String? var body: some View { List { if let error { Section { Text(error).foregroundStyle(.red).font(.callout) } } ForEach(items) { item in HStack { VStack(alignment: .leading, spacing: 2) { Text(item.productName) Text("MHD \(formatBestBefore(item.bestBefore, precision: item.bestBeforePrecision)) · \(articleAmount(item))") .font(.caption).foregroundStyle(.secondary) } Spacer() Text(restText(item.daysLeft)) .font(.caption).bold() .foregroundStyle(item.daysLeft < 0 ? .red : .orange) } } if items.isEmpty && !busy { Text("Nichts läuft demnächst ab.").foregroundStyle(.secondary) } } .navigationTitle("Bald ablaufend") .navigationBarTitleDisplayMode(.inline) .refreshable { await load() } .task { await load() } } private func load() async { busy = true defer { busy = false } do { items = try await APIClient.shared.expiring() error = nil } catch { self.error = error.localizedDescription } } /// Gebinde ist die Leitangabe, sonst die Produkteinheit. private func articleAmount(_ item: ExpiringItem) -> String { if let size = item.packageSize, size > 0 { return "\(formatAmount(item.quantity / size)) \(item.packageLabel ?? "Packung")" } let faktor = item.unitFactor == 0 ? 1 : item.unitFactor return "\(formatAmount(item.quantity / faktor)) \(item.unitName)" } private func restText(_ days: Int) -> String { if days < 0 { return "abgelaufen" } if days == 0 { return "heute" } return days == 1 ? "1 Tag" : "\(days) Tage" } } // MARK: - Produktliste struct ProductListView: View { @State private var query = "" @State private var products: [Product] = [] @State private var busy = false var body: some View { List { ForEach(products) { product in NavigationLink { ProductDetailView(product: product) } label: { VStack(alignment: .leading, spacing: 2) { Text(product.name) Text("\(formatAmount(product.stockInArticleUnits)) \(product.articleUnitLabel)") .font(.caption).foregroundStyle(.secondary) } } } if products.isEmpty && !busy { Text("Keine Treffer").foregroundStyle(.secondary) } } .searchable(text: $query, prompt: "Artikel suchen") .onChange(of: query) { _ in Task { await load() } } .navigationTitle("Produkte") .navigationBarTitleDisplayMode(.inline) .refreshable { await load() } .task { await load() } } private func load() async { busy = true defer { busy = false } products = (try? await APIClient.shared.searchProducts(query)) ?? [] } }