Files
KisaniCal/KisaniCal/Models/TaskItem.swift
Robin Kutesa 2f08e4a006 Fix build errors: Self.kXxx refs and MainActor isolation
- Remove leftover Self. prefix on instance storage keys in save()
- Mark TaskViewModel and WorkoutViewModel @MainActor so init() can
  safely access AuthManager.shared without isolation violations

Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
2026-06-01 01:38:19 +03:00

189 lines
6.7 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 = []
}
}
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()
}
}