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
|
||||
}
|
||||
|
||||
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<TodoList>(predicate: #Predicate { $0.isInbox })
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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