diff --git a/ios/Sources/ListViews.swift b/ios/Sources/ListViews.swift index 675b9b3..5d4a3b9 100644 --- a/ios/Sources/ListViews.swift +++ b/ios/Sources/ListViews.swift @@ -201,15 +201,42 @@ struct ProductListView: View { @State private var categories: [CategoryItem] = [] /// nil = alle, 0 = ohne Kategorie, sonst die ID (Unterkategorien zaehlen mit). @State private var categoryId: Int? + @State private var sortMode: SortMode = .nameAsc @State private var busy = false @State private var showNew = false + enum SortMode: String, CaseIterable, Identifiable { + case nameAsc = "Name A–Z" + case nameDesc = "Name Z–A" + case updated = "Zuletzt geändert" + var id: String { rawValue } + } + /// Auf den festen Typ gefiltert (clientseitig, wie im Web). private var sichtbar: [Product] { guard let fixedType else { return products } return products.filter { ($0.isObject ? "object" : "food") == fixedType } } + /// Sichtbare Produkte in der gewählten Reihenfolge. + private var sortiert: [Product] { + switch sortMode { + case .nameAsc: + return sichtbar.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } + case .nameDesc: + return sichtbar.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedDescending } + case .updated: + // ISO-Strings gleichen Formats sind chronologisch vergleichbar; neueste zuerst. + return sichtbar.sorted { ($0.updatedAt ?? "") > ($1.updatedAt ?? "") } + } + } + + /// Nur Kategorien des Listentyps (Lebensmittel-Liste zeigt nur Food-Kategorien usw.). + private var typKategorien: [CategoryItem] { + guard let fixedType else { return categories } + return categories.filter { ($0.tracking ?? "food") == fixedType } + } + private var titel: String { switch fixedType { case "food": return "Lebensmittel" @@ -243,10 +270,16 @@ struct ProductListView: View { Text(filterLabel).foregroundStyle(.secondary) } } + Picker(selection: $sortMode) { + ForEach(SortMode.allCases) { Text($0.rawValue).tag($0) } + } label: { + Label("Sortierung", systemImage: "arrow.up.arrow.down") + } + .pickerStyle(.menu) } Section { - ForEach(sichtbar) { product in + ForEach(sortiert) { product in NavigationLink { ProductDetailView(product: product) } label: { @@ -308,17 +341,18 @@ struct ProductListView: View { } private func categoryTree() -> [TreeEntry] { - let ids = Set(categories.map(\.id)) + let liste = typKategorien + 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 categories.filter({ $0.parentId == node.id }) { + for child in liste.filter({ $0.parentId == node.id }) { walk(child, depth + 1) } } // Waisen (Oberkategorie geloescht) gelten als oberste Ebene. - for root in categories.filter({ $0.parentId == nil || !ids.contains($0.parentId!) }) { + for root in liste.filter({ $0.parentId == nil || !ids.contains($0.parentId!) }) { walk(root, 0) } return result diff --git a/ios/Sources/Models.swift b/ios/Sources/Models.swift index 605dae2..2abf5b3 100644 --- a/ios/Sources/Models.swift +++ b/ios/Sources/Models.swift @@ -82,10 +82,13 @@ struct Product: Codable, Identifiable, Hashable { let bulk: Bool? /// Mindestbestand je Lagerort (zusaetzlich zum globalen Mindestbestand). let locationMinStocks: [LocationMinStock]? + /// Zeitpunkt der letzten Änderung (ISO-String), zum Sortieren „Zuletzt geändert". + let updatedAt: String? enum CodingKeys: String, CodingKey { case id, barcode, name, brand, stock, kind, barcodes, tracking, individual, bulk case locationMinStocks = "location_min_stocks" + case updatedAt = "updated_at" case datePrecision = "date_precision" case categoryId = "category_id" case categoryName = "category_name"