Tap body → marks complete + navigates to tasks. Swipe action 'Mark Complete' → marks complete without opening app. TaskViewModel.reload() called on foreground to sync UserDefaults changes made while app was backgrounded. Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
196 lines
6.9 KiB
Swift
196 lines
6.9 KiB
Swift
import SwiftUI
|
|
|
|
// MARK: - Task Model
|
|
struct TaskItem: Identifiable, Codable, Equatable {
|
|
var id = UUID()
|
|
var title: String
|
|
var dueDate: Date?
|
|
var hasTime: Bool = false
|
|
var isComplete: Bool = false
|
|
var completedAt: Date? = nil
|
|
var quadrant: Quadrant = .urgent
|
|
var category: TaskCategory = .reminder
|
|
var taskColor: TaskColor = .orange
|
|
var isRecurring: Bool = false
|
|
var isPinned: Bool = false
|
|
var endDate: Date? = nil
|
|
var isAllDay: Bool = false
|
|
}
|
|
|
|
enum Quadrant: String, CaseIterable, Identifiable, Codable {
|
|
var id: String { rawValue }
|
|
case urgent = "Urgent + Important"
|
|
case schedule = "Schedule It"
|
|
case delegate_ = "Delegate"
|
|
case eliminate = "Eliminate"
|
|
}
|
|
|
|
enum TaskCategory: String, Codable {
|
|
case reminder = "Reminder"
|
|
case birthday = "Birthday"
|
|
case domain = "Domain"
|
|
case annual = "Annual"
|
|
case custom = "Custom"
|
|
}
|
|
|
|
enum TaskColor: String, Codable {
|
|
case orange, green, blue, yellow
|
|
|
|
func color() -> Color {
|
|
switch self {
|
|
case .orange: return AppColors.accent
|
|
case .green: return AppColors.green
|
|
case .blue: return AppColors.blue
|
|
case .yellow: return AppColors.yellow
|
|
}
|
|
}
|
|
func soft() -> Color {
|
|
switch self {
|
|
case .orange: return AppColors.accentSoft
|
|
case .green: return AppColors.greenSoft
|
|
case .blue: return AppColors.blueSoft
|
|
case .yellow: return AppColors.yellowSoft
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Task View Model
|
|
@MainActor
|
|
class TaskViewModel: ObservableObject {
|
|
@Published var tasks: [TaskItem]
|
|
|
|
private let persistKey: String
|
|
|
|
init() {
|
|
let uid = AuthManager.shared.currentUser?.id ?? "shared"
|
|
self.persistKey = "kisani.tasks.v2.\(uid)"
|
|
if let data = UserDefaults.standard.data(forKey: persistKey),
|
|
let saved = try? JSONDecoder().decode([TaskItem].self, from: data), !saved.isEmpty {
|
|
tasks = saved
|
|
} else {
|
|
tasks = []
|
|
}
|
|
}
|
|
|
|
func reload() {
|
|
guard let data = UserDefaults.standard.data(forKey: persistKey),
|
|
let saved = try? JSONDecoder().decode([TaskItem].self, from: data)
|
|
else { return }
|
|
tasks = saved
|
|
}
|
|
|
|
private func save() {
|
|
if let data = try? JSONEncoder().encode(tasks) {
|
|
UserDefaults.standard.set(data, forKey: persistKey)
|
|
}
|
|
}
|
|
|
|
var overdueTasks: [TaskItem] {
|
|
let startOfDay = Calendar.current.startOfDay(for: Date())
|
|
return tasks.filter { !$0.isComplete && ($0.dueDate ?? .distantFuture) < startOfDay }
|
|
}
|
|
var todayTasks: [TaskItem] {
|
|
let start = Calendar.current.startOfDay(for: Date())
|
|
let end = Calendar.current.date(byAdding: .day, value: 1, to: start)!
|
|
return tasks
|
|
.filter { !$0.isComplete && ($0.dueDate ?? .distantFuture) >= start && ($0.dueDate ?? .distantFuture) < end }
|
|
.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
|
|
}
|
|
var upcomingTasks: [TaskItem] {
|
|
let cal = Calendar.current
|
|
let fourDays = cal.date(byAdding: .day, value: 4, to: cal.startOfDay(for: Date()))!
|
|
return tasks
|
|
.filter { !$0.isComplete && ($0.dueDate ?? .distantFuture) >= fourDays }
|
|
.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
|
|
}
|
|
var next3DaysTasks: [(date: Date, tasks: [TaskItem])] {
|
|
let cal = Calendar.current
|
|
let today = cal.startOfDay(for: Date())
|
|
return (1...3).compactMap { offset -> (Date, [TaskItem])? in
|
|
guard let day = cal.date(byAdding: .day, value: offset, to: today),
|
|
let nextDay = cal.date(byAdding: .day, value: 1, to: day) else { return nil }
|
|
let dayTasks = tasks.filter {
|
|
!$0.isComplete &&
|
|
($0.dueDate ?? .distantFuture) >= day &&
|
|
($0.dueDate ?? .distantFuture) < nextDay
|
|
}.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
|
|
return dayTasks.isEmpty ? nil : (day, dayTasks)
|
|
}
|
|
}
|
|
var completedTodayTasks: [TaskItem] {
|
|
let startOfDay = Calendar.current.startOfDay(for: Date())
|
|
return tasks
|
|
.filter { $0.isComplete && ($0.completedAt ?? .distantPast) >= startOfDay }
|
|
.sorted { ($0.completedAt ?? .distantPast) > ($1.completedAt ?? .distantPast) }
|
|
}
|
|
|
|
func toggle(_ task: TaskItem) {
|
|
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
|
tasks[idx].isComplete.toggle()
|
|
tasks[idx].completedAt = tasks[idx].isComplete ? Date() : nil
|
|
save()
|
|
}
|
|
func tasks(for quadrant: Quadrant) -> [TaskItem] {
|
|
tasks
|
|
.filter { $0.quadrant == quadrant }
|
|
.sorted { $0.isPinned && !$1.isPinned }
|
|
}
|
|
|
|
func pin(_ task: TaskItem) {
|
|
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
|
tasks[idx].isPinned.toggle()
|
|
save()
|
|
}
|
|
|
|
func setDate(_ task: TaskItem, date: Date?) {
|
|
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
|
tasks[idx].dueDate = date
|
|
save()
|
|
}
|
|
|
|
func setCategory(_ task: TaskItem, category: TaskCategory) {
|
|
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
|
tasks[idx].category = category
|
|
save()
|
|
}
|
|
func moveToQuadrant(taskID: UUID, quadrant: Quadrant) {
|
|
guard let idx = tasks.firstIndex(where: { $0.id == taskID }) else { return }
|
|
tasks[idx].quadrant = quadrant
|
|
save()
|
|
}
|
|
|
|
func addTask(title: String, dueDate: Date?, hasTime: Bool = false,
|
|
quadrant: Quadrant = .urgent, category: TaskCategory = .reminder,
|
|
taskColor: TaskColor = .orange, isRecurring: Bool = false,
|
|
endDate: Date? = nil, isAllDay: Bool = false) {
|
|
tasks.append(TaskItem(title: title, dueDate: dueDate, hasTime: hasTime,
|
|
quadrant: quadrant, category: category,
|
|
taskColor: taskColor, isRecurring: isRecurring,
|
|
endDate: endDate, isAllDay: isAllDay))
|
|
save()
|
|
}
|
|
|
|
func postpone(_ task: TaskItem, days: Int = 1) {
|
|
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
|
let base = tasks[idx].dueDate ?? Date()
|
|
tasks[idx].dueDate = Calendar.current.date(byAdding: .day, value: days, to: base)
|
|
save()
|
|
}
|
|
|
|
func postponeAllOverdue(days: Int = 1) {
|
|
let ids = overdueTasks.map { $0.id }
|
|
for id in ids {
|
|
guard let idx = tasks.firstIndex(where: { $0.id == id }) else { continue }
|
|
let base = tasks[idx].dueDate ?? Date()
|
|
tasks[idx].dueDate = Calendar.current.date(byAdding: .day, value: days, to: base)
|
|
}
|
|
save()
|
|
}
|
|
|
|
func delete(_ task: TaskItem) {
|
|
tasks.removeAll { $0.id == task.id }
|
|
save()
|
|
}
|
|
}
|
|
|