From eeee1af5704f97e6ca18d7042cc6834ce5dac257 Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 12 Feb 2026 23:18:50 +0100 Subject: [PATCH] feat: add visible filter chips and sort menu to list detail view --- MindDump/Models/ListDetailConfig.swift | 23 +++++ MindDump/ViewModels/ListStore.swift | 44 +++++++++ MindDump/Views/ListDetailView.swift | 132 ++++++++++++++++++++++--- 3 files changed, 185 insertions(+), 14 deletions(-) create mode 100644 MindDump/Models/ListDetailConfig.swift diff --git a/MindDump/Models/ListDetailConfig.swift b/MindDump/Models/ListDetailConfig.swift new file mode 100644 index 0000000..a1b8f7b --- /dev/null +++ b/MindDump/Models/ListDetailConfig.swift @@ -0,0 +1,23 @@ +import Foundation + +enum ListDetailSort: CaseIterable { + case oldest, newest, dueDate, priority + + var label: String { + switch self { + case .oldest: "Älteste zuerst" + case .newest: "Neueste zuerst" + case .dueDate: "Deadline zuerst" + case .priority: "Priorität zuerst" + } + } + + var shortLabel: String { + switch self { + case .oldest: "Älteste" + case .newest: "Neueste" + case .dueDate: "Deadline" + case .priority: "Priorität" + } + } +} diff --git a/MindDump/ViewModels/ListStore.swift b/MindDump/ViewModels/ListStore.swift index 299273c..ccdfce0 100644 --- a/MindDump/ViewModels/ListStore.swift +++ b/MindDump/ViewModels/ListStore.swift @@ -128,6 +128,50 @@ class ListStore { deleteItem(item.id, from: listID) } + func filteredOpenItems(for list: TodoList, urgent: Bool, due: Bool, sort: ListDetailSort) -> [TodoItem] { + var items = list.openItems + + if urgent || due { + let now = Date() + let threeDaysFromNow = Calendar.current.date(byAdding: .day, value: 3, to: now)! + let startOfTomorrow = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: now)!) + + items = items.filter { item in + let isUrgent = item.priority == .high || (item.deadline != nil && item.deadline! <= threeDaysFromNow) + let isDue = item.deadline != nil && item.deadline! < startOfTomorrow + + if urgent && due { return isUrgent && isDue } + if urgent { return isUrgent } + return isDue + } + } + + switch sort { + case .oldest: + items.sort { $0.createdAt < $1.createdAt } + case .newest: + items.sort { $0.createdAt > $1.createdAt } + case .dueDate: + items.sort { lhs, rhs in + switch (lhs.deadline, rhs.deadline) { + case let (l?, r?): l < r + case (_?, nil): true + case (nil, _?): false + case (nil, nil): lhs.createdAt < rhs.createdAt + } + } + case .priority: + items.sort { lhs, rhs in + let lp = lhs.priority?.rawValue ?? -1 + let rp = rhs.priority?.rawValue ?? -1 + if lp != rp { return lp > rp } + return lhs.createdAt < rhs.createdAt + } + } + + return items + } + func moveItem(_ itemID: UUID, from sourceListID: UUID, to targetListID: UUID) { guard sourceListID != targetListID, let sourceList = lists.first(where: { $0.id == sourceListID }), diff --git a/MindDump/Views/ListDetailView.swift b/MindDump/Views/ListDetailView.swift index f61c019..c6173ae 100644 --- a/MindDump/Views/ListDetailView.swift +++ b/MindDump/Views/ListDetailView.swift @@ -6,6 +6,9 @@ struct ListDetailView: View { @State private var editorItem: TodoItem? @State private var moveItem: TodoItem? @State private var showCompleted = true + @State private var filterUrgent = false + @State private var filterDue = false + @State private var selectedSort: ListDetailSort = .oldest private var todoList: TodoList? { store.lists.first { $0.id == listID } @@ -17,23 +20,46 @@ struct ListDetailView: View { if todoList.items.isEmpty { ContentUnavailableView("Keine Einträge", systemImage: "tray") } else { + let filteredItems = store.filteredOpenItems( + for: todoList, urgent: filterUrgent, due: filterDue, sort: selectedSort + ) + List { Section { - TodoListView( - items: todoList.openItems, - onToggle: { item in - store.toggleItemCompleted(item.id, in: listID) - }, - onTap: { item in - editorItem = item - }, - onDelete: { item in - store.deleteItem(item.id, from: listID) - }, - onMove: { item in - moveItem = item + HStack(spacing: 12) { + chipButton("Dringend", icon: "exclamationmark.triangle", isActive: filterUrgent, color: .orange) { + withAnimation { filterUrgent.toggle() } } - ) + chipButton("Fällig", icon: "calendar.badge.clock", isActive: filterDue, color: .red) { + withAnimation { filterDue.toggle() } + } + sortMenu + } + .frame(maxWidth: .infinity) + .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) + .listRowBackground(Color.clear) + } + + Section { + if filteredItems.isEmpty { + emptyState + } else { + TodoListView( + items: filteredItems, + onToggle: { item in + store.toggleItemCompleted(item.id, in: listID) + }, + onTap: { item in + editorItem = item + }, + onDelete: { item in + store.deleteItem(item.id, from: listID) + }, + onMove: { item in + moveItem = item + } + ) + } } if !todoList.completedItems.isEmpty { @@ -81,4 +107,82 @@ struct ListDetailView: View { MoveToListView(itemID: item.id, currentListID: listID) } } + + // MARK: - Filter Chip + + @ViewBuilder + private func chipButton(_ label: String, icon: String, isActive: Bool, color: Color, action: @escaping () -> Void) -> some View { + Button(action: action) { + Image(systemName: icon) + .font(.subheadline.weight(.medium)) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(isActive ? color.opacity(0.15) : Color(.systemGray6)) + .foregroundStyle(isActive ? color : .secondary) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + } + + // MARK: - Sort Menu + + private var sortMenu: some View { + Menu { + Picker("Sortierung", selection: $selectedSort) { + ForEach(ListDetailSort.allCases, id: \.self) { sort in + Text(sort.label).tag(sort) + } + } + } label: { + HStack(spacing: 4) { + Image(systemName: "arrow.up.arrow.down") + ZStack(alignment: .leading) { + ForEach(ListDetailSort.allCases, id: \.self) { sort in + Text(sort.shortLabel) + .opacity(sort == selectedSort ? 1 : 0) + } + } + } + .font(.subheadline) + .foregroundStyle(.secondary) + } + } + + // MARK: - Empty State + + private var emptyState: some View { + VStack(spacing: 8) { + Image(systemName: emptyIcon) + .font(.title2) + .foregroundStyle(.secondary) + Text(emptyMessage) + .font(.subheadline) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 24) + .listRowBackground(Color.clear) + } + + private var emptyMessage: String { + if filterUrgent && filterDue { + "Keine dringenden oder fälligen Aufgaben" + } else if filterUrgent { + "Keine dringenden Aufgaben" + } else if filterDue { + "Keine fälligen Aufgaben" + } else { + "Keine offenen Aufgaben" + } + } + + private var emptyIcon: String { + if filterUrgent { + "exclamationmark.triangle" + } else if filterDue { + "calendar.badge.checkmark" + } else { + "checkmark.circle" + } + } }