feat: add move-to-list functionality and stats dashboard cards to lists overview

This commit is contained in:
2026-02-12 21:26:45 +01:00
parent d988cb53cf
commit 5c589c4b93
5 changed files with 116 additions and 1 deletions

View File

@@ -12,6 +12,26 @@ class ListStore {
lists.first { $0.isInbox }!.id 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) { init(modelContext: ModelContext) {
self.modelContext = modelContext self.modelContext = modelContext
ensureInbox() ensureInbox()
@@ -86,7 +106,17 @@ class ListStore {
fetchLists() 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() { private func ensureInbox() {
let descriptor = FetchDescriptor<TodoList>(predicate: #Predicate { $0.isInbox }) let descriptor = FetchDescriptor<TodoList>(predicate: #Predicate { $0.isInbox })

View File

@@ -4,6 +4,7 @@ struct ListDetailView: View {
@Environment(ListStore.self) private var store @Environment(ListStore.self) private var store
let listID: UUID let listID: UUID
@State private var editorItem: TodoItem? @State private var editorItem: TodoItem?
@State private var moveItem: TodoItem?
private var todoList: TodoList? { private var todoList: TodoList? {
store.lists.first { $0.id == listID } store.lists.first { $0.id == listID }
@@ -22,6 +23,14 @@ struct ListDetailView: View {
}, onTap: { }, onTap: {
editorItem = item editorItem = item
}) })
.swipeActions(edge: .leading) {
Button {
moveItem = item
} label: {
Label("Verschieben", systemImage: "folder")
}
.tint(.blue)
}
} }
.onDelete { offsets in .onDelete { offsets in
let sorted = todoList.sortedItems let sorted = todoList.sortedItems
@@ -37,5 +46,8 @@ struct ListDetailView: View {
.sheet(item: $editorItem) { item in .sheet(item: $editorItem) { item in
TodoEditorView(listID: listID, item: item) TodoEditorView(listID: listID, item: item)
} }
.sheet(item: $moveItem) { item in
MoveToListView(itemID: item.id, currentListID: listID)
}
} }
} }

View File

@@ -7,6 +7,16 @@ struct ListsOverviewView: View {
var body: some View { var body: some View {
List { 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 ForEach(store.lists) { list in
NavigationLink(value: list.id) { NavigationLink(value: list.id) {
HStack { HStack {

View File

@@ -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() }
}
}
}
}
}

View File

@@ -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))
}
}