From 5c589c4b93ce495c3ede65aa2e2a6b353b44b5aa Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 12 Feb 2026 21:26:45 +0100 Subject: [PATCH] feat: add move-to-list functionality and stats dashboard cards to lists overview --- MindDump/ViewModels/ListStore.swift | 32 ++++++++++++++++++++- MindDump/Views/ListDetailView.swift | 12 ++++++++ MindDump/Views/ListsOverviewView.swift | 10 +++++++ MindDump/Views/MoveToListView.swift | 39 ++++++++++++++++++++++++++ MindDump/Views/StatsCardView.swift | 24 ++++++++++++++++ 5 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 MindDump/Views/MoveToListView.swift create mode 100644 MindDump/Views/StatsCardView.swift diff --git a/MindDump/ViewModels/ListStore.swift b/MindDump/ViewModels/ListStore.swift index 0f87f10..feaf7d0 100644 --- a/MindDump/ViewModels/ListStore.swift +++ b/MindDump/ViewModels/ListStore.swift @@ -12,6 +12,26 @@ class ListStore { lists.first { $0.isInbox }!.id } + var allOpenItems: [TodoItem] { + lists.flatMap { $0.items }.filter { !$0.isCompleted } + } + + var openCount: Int { allOpenItems.count } + + var urgentCount: Int { + let threeDaysFromNow = Calendar.current.date(byAdding: .day, value: 3, to: Date())! + return allOpenItems.filter { item in + item.priority == .high || (item.deadline != nil && item.deadline! <= threeDaysFromNow) + }.count + } + + var dueCount: Int { + let startOfTomorrow = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: Date())!) + return allOpenItems.filter { item in + item.deadline != nil && item.deadline! < startOfTomorrow + }.count + } + init(modelContext: ModelContext) { self.modelContext = modelContext ensureInbox() @@ -86,7 +106,17 @@ class ListStore { fetchLists() } - // MARK: - Private + func moveItem(_ itemID: UUID, from sourceListID: UUID, to targetListID: UUID) { + guard sourceListID != targetListID, + let sourceList = lists.first(where: { $0.id == sourceListID }), + let targetList = lists.first(where: { $0.id == targetListID }), + let item = sourceList.items.first(where: { $0.id == itemID }) else { return } + sourceList.items.removeAll { $0.id == itemID } + targetList.items.append(item) + item.modifiedAt = Date() + save() + fetchLists() + } private func ensureInbox() { let descriptor = FetchDescriptor(predicate: #Predicate { $0.isInbox }) diff --git a/MindDump/Views/ListDetailView.swift b/MindDump/Views/ListDetailView.swift index a4b4816..57d3a06 100644 --- a/MindDump/Views/ListDetailView.swift +++ b/MindDump/Views/ListDetailView.swift @@ -4,6 +4,7 @@ struct ListDetailView: View { @Environment(ListStore.self) private var store let listID: UUID @State private var editorItem: TodoItem? + @State private var moveItem: TodoItem? private var todoList: TodoList? { store.lists.first { $0.id == listID } @@ -22,6 +23,14 @@ struct ListDetailView: View { }, onTap: { editorItem = item }) + .swipeActions(edge: .leading) { + Button { + moveItem = item + } label: { + Label("Verschieben", systemImage: "folder") + } + .tint(.blue) + } } .onDelete { offsets in let sorted = todoList.sortedItems @@ -37,5 +46,8 @@ struct ListDetailView: View { .sheet(item: $editorItem) { item in TodoEditorView(listID: listID, item: item) } + .sheet(item: $moveItem) { item in + MoveToListView(itemID: item.id, currentListID: listID) + } } } diff --git a/MindDump/Views/ListsOverviewView.swift b/MindDump/Views/ListsOverviewView.swift index 7492e44..97ae023 100644 --- a/MindDump/Views/ListsOverviewView.swift +++ b/MindDump/Views/ListsOverviewView.swift @@ -7,6 +7,16 @@ struct ListsOverviewView: View { var body: some View { List { + Section { + HStack(spacing: 12) { + StatsCardView(icon: "checklist", count: store.openCount, label: "Offen", color: .blue) + StatsCardView(icon: "exclamationmark.triangle", count: store.urgentCount, label: "Dringend", color: .orange) + StatsCardView(icon: "calendar.badge.clock", count: store.dueCount, label: "Fällig", color: .red) + } + .listRowInsets(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0)) + .listRowBackground(Color.clear) + } + ForEach(store.lists) { list in NavigationLink(value: list.id) { HStack { diff --git a/MindDump/Views/MoveToListView.swift b/MindDump/Views/MoveToListView.swift new file mode 100644 index 0000000..7bcdbc1 --- /dev/null +++ b/MindDump/Views/MoveToListView.swift @@ -0,0 +1,39 @@ +import SwiftUI + +struct MoveToListView: View { + @Environment(ListStore.self) private var store + @Environment(\.dismiss) private var dismiss + let itemID: UUID + let currentListID: UUID + + var body: some View { + NavigationStack { + List { + ForEach(store.lists) { list in + Button { + store.moveItem(itemID, from: currentListID, to: list.id) + dismiss() + } label: { + HStack { + Text(list.name) + .foregroundStyle(.primary) + Spacer() + if list.id == currentListID { + Image(systemName: "checkmark") + .foregroundStyle(.secondary) + } + } + } + .disabled(list.id == currentListID) + } + } + .navigationTitle("Verschieben nach") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Abbrechen") { dismiss() } + } + } + } + } +} diff --git a/MindDump/Views/StatsCardView.swift b/MindDump/Views/StatsCardView.swift new file mode 100644 index 0000000..9a3ff74 --- /dev/null +++ b/MindDump/Views/StatsCardView.swift @@ -0,0 +1,24 @@ +import SwiftUI + +struct StatsCardView: View { + let icon: String + let count: Int + let label: String + let color: Color + + var body: some View { + VStack(spacing: 6) { + Image(systemName: icon) + .font(.title2) + .foregroundStyle(color) + Text("\(count)") + .font(.title.bold()) + Text(label) + .font(.caption) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background(color.opacity(0.12), in: RoundedRectangle(cornerRadius: 12)) + } +}