import SwiftUI /// Liste aller Einzelstücke (physische Exemplare) über alle Produkte – anders als /// die Produktliste, die nur die Katalog-Typen zeigt. Such- und Filterfunktion, /// v.a. nach Lagerort. Tippen öffnet das Einzelstück. struct ItemListView: View { @State private var items: [Item] = [] @State private var locations: [StorageLocation] = [] @State private var query = "" @State private var locationFilter: String? // Lagerort-ID @State private var categoryFilter: String? // Kategoriename @State private var busy = true @State private var error: String? private var categories: [String] { Array(Set(items.compactMap { $0.categoryName })).sorted() } private var gefiltert: [Item] { let q = query.trimmingCharacters(in: .whitespaces).lowercased() return items.filter { it in if let cat = categoryFilter, it.categoryName != cat { return false } if let loc = locationFilter, it.locationId != loc { return false } if !q.isEmpty { let heu = [it.productName, it.uid, it.note, it.productBrand] .compactMap { $0 }.joined(separator: " ").lowercased() if !heu.contains(q) { return false } } return true } } private var hatFilter: Bool { locationFilter != nil || categoryFilter != nil } var body: some View { List { if !busy && gefiltert.isEmpty { Text(items.isEmpty ? "Noch keine Einzelstücke." : "Nichts gefunden.") .foregroundStyle(.secondary) } ForEach(gefiltert) { it in NavigationLink { ItemEditView(item: it) } label: { ItemRow(item: it, locations: locations) } } } .searchable(text: $query, prompt: "Produkt, UID oder Notiz") .navigationTitle("Einzelstücke") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarTrailing) { filterMenu } } .overlay { if busy && items.isEmpty { ProgressView() } } .refreshable { await load() } .task { await load() } .alert("Fehler", isPresented: Binding(get: { error != nil }, set: { if !$0 { error = nil } })) { Button("OK", role: .cancel) {} } message: { Text(error ?? "") } } private var filterMenu: some View { Menu { Menu("Lagerort") { check("Alle", locationFilter == nil) { locationFilter = nil } ForEach(locations) { loc in check(loc.path(in: locations), locationFilter == loc.id) { locationFilter = loc.id } } } if !categories.isEmpty { Menu("Kategorie") { check("Alle", categoryFilter == nil) { categoryFilter = nil } ForEach(categories, id: \.self) { c in check(c, categoryFilter == c) { categoryFilter = c } } } } if hatFilter { Divider() Button(role: .destructive) { locationFilter = nil; categoryFilter = nil } label: { Label("Filter zurücksetzen", systemImage: "xmark.circle") } } } label: { Image(systemName: hatFilter ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle") } } @ViewBuilder private func check(_ title: String, _ selected: Bool, _ action: @escaping () -> Void) -> some View { Button(action: action) { if selected { Label(title, systemImage: "checkmark") } else { Text(title) } } } private func load() async { busy = true defer { busy = false } do { items = try await APIClient.shared.allItems() locations = (try? await APIClient.shared.locations()) ?? [] } catch { self.error = error.localizedDescription } } } /// Eine Einzelstück-Zeile: Produktbild, Produktname, UID und Lagerort-Pfad. struct ItemRow: View { let item: Item let locations: [StorageLocation] var body: some View { HStack(spacing: 12) { ProductThumb(productId: item.productId) VStack(alignment: .leading, spacing: 2) { Text(item.productName ?? "—") let pfad = locationPath(item.locationId, in: locations) HStack(spacing: 6) { Text(item.uid).font(.caption).foregroundStyle(.secondary) if !pfad.isEmpty { Text("· \(pfad)").font(.caption).foregroundStyle(.secondary).lineLimit(1) } } } Spacer() } } }