From 532695c2ee3a379f49b5973090e5fa21d5f800d6 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Tue, 28 Jul 2026 13:45:41 +0200 Subject: [PATCH] iOS: Einzelstueck-Kategoriefilter hierarchisch + mit Oberkategorien Der Kategoriefilter der Einzelstueckliste zeigt jetzt einen eingerueckten Baum und enthaelt auch die Oberkategorien (z.B. "Klamotten"), nicht nur die Blaetter mit Exemplaren. Nach einer Oberkategorie zu filtern schliesst alle Unter- kategorien ein (ueber category_id statt Namensgleichheit). Co-Authored-By: Claude Opus 4.8 --- ios/Sources/ItemListView.swift | 67 ++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 7 deletions(-) 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 }