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 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-28 13:45:41 +02:00
parent 4f8e6e3185
commit 532695c2ee

View File

@@ -6,20 +6,72 @@ import SwiftUI
struct ItemListView: View { struct ItemListView: View {
@State private var items: [Item] = [] @State private var items: [Item] = []
@State private var locations: [StorageLocation] = [] @State private var locations: [StorageLocation] = []
@State private var categories: [CategoryItem] = []
@State private var query = "" @State private var query = ""
@State private var locationFilter: String? // Lagerort-ID @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 busy = true
@State private var error: String? @State private var error: String?
private var categories: [String] { private struct TreeEntry: Identifiable {
Array(Set(items.compactMap { $0.categoryName })).sorted() 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<Int>()
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] { private var gefiltert: [Item] {
let q = query.trimmingCharacters(in: .whitespaces).lowercased() let q = query.trimmingCharacters(in: .whitespaces).lowercased()
return items.filter { it in 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 let loc = locationFilter, it.locationId != loc { return false }
if !q.isEmpty { if !q.isEmpty {
let heu = [it.productName, it.uid, it.note, it.productBrand] 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 } check(loc.path(in: locations), locationFilter == loc.id) { locationFilter = loc.id }
} }
} }
if !categories.isEmpty { if !filterKategorien.isEmpty {
Menu("Kategorie") { Menu("Kategorie") {
check("Alle", categoryFilter == nil) { categoryFilter = nil } check("Alle", categoryFilter == nil) { categoryFilter = nil }
ForEach(categories, id: \.self) { c in ForEach(filterKategorien) { e in
check(c, categoryFilter == c) { categoryFilter = c } check(e.label, categoryFilter == e.item.id) { categoryFilter = e.item.id }
} }
} }
} }
@@ -98,6 +150,7 @@ struct ItemListView: View {
do { do {
items = try await APIClient.shared.allItems() items = try await APIClient.shared.allItems()
locations = (try? await APIClient.shared.locations()) ?? [] locations = (try? await APIClient.shared.locations()) ?? []
categories = (try? await APIClient.shared.categories()) ?? []
} catch { } catch {
self.error = error.localizedDescription self.error = error.localizedDescription
} }