94 lines
3.0 KiB
Swift
94 lines
3.0 KiB
Swift
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<Goal>()
|
|
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)
|
|
}
|
|
}
|
|
}
|