import Foundation import Observation import SwiftData enum GoalTargetType: String, CaseIterable { case task case category } struct GoalProgress: Identifiable { var id: UUID { goal.id } let goal: Goal let current: Double let target: Double var ratio: Double { target > 0 ? min(current / target, 1.0) : 0.0 } var isCompleted: Bool { current >= target } var displayName: String { if let task = goal.task { return "\(task.icon) \(task.name)" } if let category = goal.category { return category.name } return "—" } var percentText: String { "\(Int(ratio * 100))%" } } @MainActor @Observable final class GoalViewModel { private(set) var goals: [Goal] = [] private(set) var goalProgress: [GoalProgress] = [] private var context: ModelContext? func setup(context: ModelContext) { self.context = context fetchGoals() } func fetchGoals() { guard let context else { return } let descriptor = FetchDescriptor() goals = (try? context.fetch(descriptor)) ?? [] computeProgress() } func addGoal(targetType: String, conditions: Double, frequency: GoalFrequency, task: TaskItem?, category: Category?) { guard let context else { return } let goal = Goal(targetType: targetType, conditions: conditions, frequency: frequency, task: task, category: category) context.insert(goal) try? context.save() fetchGoals() } func deleteGoal(_ goal: Goal) { guard let context else { return } context.delete(goal) try? context.save() fetchGoals() } private func computeProgress() { let now = Date.now let cal = Calendar.current goalProgress = goals.map { goal in let logs = relevantLogs(for: goal, reference: now, calendar: cal) let current: Double = goal.targetType == "count" ? Double(logs.reduce(0) { $0 + $1.count }) : logs.reduce(0.0) { $0 + $1.duration } return GoalProgress(goal: goal, current: current, target: goal.conditions) } } private func relevantLogs(for goal: Goal, reference: Date, calendar: Calendar) -> [TaskLog] { let allLogs: [TaskLog] if let task = goal.task { allLogs = task.logs } else if let category = goal.category { allLogs = category.tasks.flatMap { $0.logs } } else { return [] } return allLogs.filter { inPeriod($0.date, frequency: goal.frequency, reference: reference, calendar: calendar) } } private func inPeriod(_ date: Date, frequency: GoalFrequency, reference: Date, calendar: Calendar) -> Bool { switch frequency { case .daily: return calendar.isDate(date, inSameDayAs: reference) case .weekly: return calendar.isDate(date, equalTo: reference, toGranularity: .weekOfYear) case .monthly: return calendar.isDate(date, equalTo: reference, toGranularity: .month) } } }