feat: add move-to-list functionality and stats dashboard cards to lists overview
This commit is contained in:
@@ -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 })
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
39
MindDump/Views/MoveToListView.swift
Normal file
39
MindDump/Views/MoveToListView.swift
Normal 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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
MindDump/Views/StatsCardView.swift
Normal file
24
MindDump/Views/StatsCardView.swift
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user