diff --git a/ios/Sources/ItemListView.swift b/ios/Sources/ItemListView.swift index 7e77020..47b01bd 100644 --- a/ios/Sources/ItemListView.swift +++ b/ios/Sources/ItemListView.swift @@ -6,20 +6,72 @@ import SwiftUI struct ItemListView: View { @State private var items: [Item] = [] @State private var locations: [StorageLocation] = [] + @State private var categories: [CategoryItem] = [] @State private var query = "" @State private var locationFilter: String? // Lagerort-ID - @State private var categoryFilter: String? // Kategoriename + @State private var categoryFilter: Int? // Kategorie-ID (Unterkategorien zählen mit) @State private var busy = true @State private var error: String? - private var categories: [String] { - Array(Set(items.compactMap { $0.categoryName })).sorted() + private struct TreeEntry: Identifiable { + let item: CategoryItem + let depth: Int + var id: Int { item.id } + var label: String { String(repeating: " ", count: depth) + item.name } + } + + private var catById: [Int: CategoryItem] { + Dictionary(categories.map { ($0.id, $0) }, uniquingKeysWith: { a, _ in a }) + } + + /// Ist `catId` die Kategorie `target` oder eine ihrer Unterkategorien? + private func imTeilbaum(_ catId: Int?, target: Int) -> Bool { + let map = catById + var cur = catId + var g = 0 + while let c = cur, g < 50 { + if c == target { return true } + cur = map[c]?.parentId + g += 1 + } + return false + } + + /// Kategorien fürs Filter-Menü: alle, die selbst oder als Oberkategorie zu einem + /// vorhandenen Einzelstück gehören – so ist z.B. auch „Klamotten" wählbar. Baum + /// mit eingerückter Beschriftung (hierarchisch). + private var filterKategorien: [TreeEntry] { + let map = catById + var relevant = Set() + for it in items { + var cur = it.categoryId + var g = 0 + while let c = cur, !relevant.contains(c), g < 50 { + relevant.insert(c) + cur = map[c]?.parentId + g += 1 + } + } + let liste = categories.filter { relevant.contains($0.id) } + let ids = Set(liste.map(\.id)) + var result: [TreeEntry] = [] + func walk(_ node: CategoryItem, _ depth: Int) { + result.append(TreeEntry(item: node, depth: depth)) + for child in liste.filter({ $0.parentId == node.id }) { walk(child, depth + 1) } + } + for root in liste.filter({ $0.parentId == nil || !ids.contains($0.parentId!) }) { walk(root, 0) } + return result + } + + private var filterKategorieName: String { + if let id = categoryFilter, let t = categories.first(where: { $0.id == id }) { return t.name } + return "Alle" } 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 cat = categoryFilter, !imTeilbaum(it.categoryId, target: 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] @@ -64,11 +116,11 @@ struct ItemListView: View { check(loc.path(in: locations), locationFilter == loc.id) { locationFilter = loc.id } } } - if !categories.isEmpty { + if !filterKategorien.isEmpty { Menu("Kategorie") { check("Alle", categoryFilter == nil) { categoryFilter = nil } - ForEach(categories, id: \.self) { c in - check(c, categoryFilter == c) { categoryFilter = c } + ForEach(filterKategorien) { e in + check(e.label, categoryFilter == e.item.id) { categoryFilter = e.item.id } } } } @@ -98,6 +150,7 @@ struct ItemListView: View { do { items = try await APIClient.shared.allItems() locations = (try? await APIClient.shared.locations()) ?? [] + categories = (try? await APIClient.shared.categories()) ?? [] } catch { self.error = error.localizedDescription }