refactor: unify filter chips and sections across list and global views
This commit is contained in:
@@ -2,28 +2,4 @@ import Foundation
|
||||
|
||||
enum TodoFilter: Hashable {
|
||||
case open, urgent, due
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .open: "Offene Aufgaben"
|
||||
case .urgent: "Dringende Aufgaben"
|
||||
case .due: "Fällige Aufgaben"
|
||||
}
|
||||
}
|
||||
|
||||
var emptyMessage: String {
|
||||
switch self {
|
||||
case .open: "Keine offenen Aufgaben"
|
||||
case .urgent: "Keine dringenden Aufgaben"
|
||||
case .due: "Keine fälligen Aufgaben"
|
||||
}
|
||||
}
|
||||
|
||||
var emptyIcon: String {
|
||||
switch self {
|
||||
case .open: "checkmark.circle"
|
||||
case .urgent: "exclamationmark.triangle"
|
||||
case .due: "calendar.badge.checkmark"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,29 +19,21 @@ class ListStore {
|
||||
var openCount: Int { allOpenItems.count }
|
||||
|
||||
var urgentItems: [TodoItem] {
|
||||
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)
|
||||
}
|
||||
allOpenItems.filter { isUrgent($0) }
|
||||
}
|
||||
|
||||
var urgentCount: Int { urgentItems.count }
|
||||
|
||||
var dueItems: [TodoItem] {
|
||||
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
|
||||
}
|
||||
allOpenItems.filter { isDue($0) }
|
||||
}
|
||||
|
||||
var dueCount: Int { dueItems.count }
|
||||
|
||||
func items(for filter: TodoFilter) -> [TodoItem] {
|
||||
switch filter {
|
||||
case .open: allOpenItems
|
||||
case .urgent: urgentItems
|
||||
case .due: dueItems
|
||||
}
|
||||
var allCompletedItems: [TodoItem] {
|
||||
lists.flatMap { $0.items }
|
||||
.filter { $0.isCompleted }
|
||||
.sorted { ($0.modifiedAt ?? $0.createdAt) > ($1.modifiedAt ?? $1.createdAt) }
|
||||
}
|
||||
|
||||
init(modelContext: ModelContext) {
|
||||
@@ -128,21 +120,14 @@ class ListStore {
|
||||
deleteItem(item.id, from: listID)
|
||||
}
|
||||
|
||||
func filteredOpenItems(for list: TodoList, urgent: Bool, due: Bool, sort: ListDetailSort) -> [TodoItem] {
|
||||
var items = list.openItems
|
||||
func filteredOpenItems(for list: TodoList? = nil, urgent: Bool, due: Bool, sort: ListDetailSort) -> [TodoItem] {
|
||||
var items = list?.openItems ?? allOpenItems
|
||||
|
||||
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
|
||||
if urgent && due { return isUrgent(item) && isDue(item) }
|
||||
if urgent { return isUrgent(item) }
|
||||
return isDue(item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +188,16 @@ class ListStore {
|
||||
lists = inbox + rest
|
||||
}
|
||||
|
||||
private func isUrgent(_ item: TodoItem) -> Bool {
|
||||
let threeDaysFromNow = Calendar.current.date(byAdding: .day, value: 3, to: Date())!
|
||||
return item.priority == .high || (item.deadline != nil && item.deadline! <= threeDaysFromNow)
|
||||
}
|
||||
|
||||
private func isDue(_ item: TodoItem) -> Bool {
|
||||
let startOfTomorrow = Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: 1, to: Date())!)
|
||||
return item.deadline != nil && item.deadline! < startOfTomorrow
|
||||
}
|
||||
|
||||
private func save() {
|
||||
try? modelContext.save()
|
||||
}
|
||||
|
||||
@@ -26,18 +26,11 @@ struct ListDetailView: View {
|
||||
|
||||
List {
|
||||
Section {
|
||||
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)
|
||||
FilterChipBar(
|
||||
filterUrgent: $filterUrgent,
|
||||
filterDue: $filterDue,
|
||||
selectedSort: $selectedSort
|
||||
)
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -108,46 +101,6 @@ struct ListDetailView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -5,37 +5,96 @@ struct TodoListScreen: View {
|
||||
let filter: TodoFilter
|
||||
@State private var editorItem: TodoItem?
|
||||
@State private var moveItem: TodoItem?
|
||||
@State private var showCompleted = true
|
||||
@State private var filterUrgent: Bool
|
||||
@State private var filterDue: Bool
|
||||
@State private var selectedSort: ListDetailSort = .oldest
|
||||
|
||||
private var items: [TodoItem] {
|
||||
store.items(for: filter)
|
||||
init(filter: TodoFilter) {
|
||||
self.filter = filter
|
||||
_filterUrgent = State(initialValue: filter == .urgent)
|
||||
_filterDue = State(initialValue: filter == .due)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if items.isEmpty {
|
||||
ContentUnavailableView(filter.emptyMessage, systemImage: filter.emptyIcon)
|
||||
let filteredItems = store.filteredOpenItems(
|
||||
urgent: filterUrgent, due: filterDue, sort: selectedSort
|
||||
)
|
||||
|
||||
if store.allOpenItems.isEmpty && store.allCompletedItems.isEmpty {
|
||||
ContentUnavailableView("Keine Einträge", systemImage: "tray")
|
||||
} else {
|
||||
List {
|
||||
TodoListView(
|
||||
items: items,
|
||||
showListName: true,
|
||||
onToggle: { item in
|
||||
store.toggleItemCompleted(item)
|
||||
},
|
||||
onTap: { item in
|
||||
editorItem = item
|
||||
},
|
||||
onDelete: { item in
|
||||
store.deleteItem(item)
|
||||
},
|
||||
onMove: { item in
|
||||
moveItem = item
|
||||
Section {
|
||||
FilterChipBar(
|
||||
filterUrgent: $filterUrgent,
|
||||
filterDue: $filterDue,
|
||||
selectedSort: $selectedSort
|
||||
)
|
||||
}
|
||||
|
||||
Section {
|
||||
if filteredItems.isEmpty {
|
||||
emptyState
|
||||
} else {
|
||||
TodoListView(
|
||||
items: filteredItems,
|
||||
showListName: true,
|
||||
onToggle: { item in
|
||||
store.toggleItemCompleted(item)
|
||||
},
|
||||
onTap: { item in
|
||||
editorItem = item
|
||||
},
|
||||
onDelete: { item in
|
||||
store.deleteItem(item)
|
||||
},
|
||||
onMove: { item in
|
||||
moveItem = item
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if !store.allCompletedItems.isEmpty {
|
||||
Section {
|
||||
if showCompleted {
|
||||
TodoListView(
|
||||
items: store.allCompletedItems,
|
||||
showListName: true,
|
||||
onToggle: { item in
|
||||
store.toggleItemCompleted(item)
|
||||
},
|
||||
onTap: { item in
|
||||
editorItem = item
|
||||
},
|
||||
onDelete: { item in
|
||||
store.deleteItem(item)
|
||||
},
|
||||
onMove: { item in
|
||||
moveItem = item
|
||||
}
|
||||
)
|
||||
}
|
||||
} header: {
|
||||
Button {
|
||||
withAnimation {
|
||||
showCompleted.toggle()
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Erledigt (\(store.allCompletedItems.count))")
|
||||
Spacer()
|
||||
Image(systemName: showCompleted ? "chevron.down" : "chevron.right")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(filter.title)
|
||||
.navigationTitle("Aufgaben")
|
||||
.sheet(item: $editorItem) { item in
|
||||
if let listID = item.list?.id {
|
||||
TodoEditorView(listID: listID, item: item)
|
||||
@@ -47,4 +106,42 @@ struct TodoListScreen: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user