86 lines
2.8 KiB
Swift
86 lines
2.8 KiB
Swift
//
|
|
// TrackingViewModel.swift
|
|
// HaruDanim
|
|
//
|
|
// Drives real-time tracking of Actions: starting/stopping time sessions
|
|
// and recording count entries.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftData
|
|
import Observation
|
|
|
|
@Observable
|
|
final class TrackingViewModel {
|
|
/// Currently running time sessions (those without an `endTime`).
|
|
var activeSessions: [TimeSession] = []
|
|
|
|
// MARK: - Time tracking
|
|
|
|
/// Starts a new `TimeSession` for the action, or ends the running one if it is
|
|
/// already being tracked.
|
|
func toggleTimeTracking(for action: Action, context: ModelContext) {
|
|
if let running = runningSession(for: action, context: context) {
|
|
running.endTime = .now
|
|
} else {
|
|
let session = TimeSession(startTime: .now, action: action)
|
|
context.insert(session)
|
|
}
|
|
save(context)
|
|
refreshActiveSessions(context: context)
|
|
}
|
|
|
|
/// Ends a specific running session (used by the stop button in the UI).
|
|
func stopSession(_ session: TimeSession, context: ModelContext) {
|
|
guard session.endTime == nil else { return }
|
|
session.endTime = .now
|
|
save(context)
|
|
refreshActiveSessions(context: context)
|
|
}
|
|
|
|
// MARK: - Count tracking
|
|
|
|
/// Records a `CountEntry` for the action.
|
|
func incrementCount(for action: Action, amount: Int = 1, context: ModelContext) {
|
|
let entry = CountEntry(timestamp: .now, amount: amount, action: action)
|
|
context.insert(entry)
|
|
save(context)
|
|
}
|
|
|
|
// MARK: - Active session loading
|
|
|
|
/// Reloads `activeSessions` from the store (sessions with no `endTime`).
|
|
func refreshActiveSessions(context: ModelContext) {
|
|
let descriptor = FetchDescriptor<TimeSession>(
|
|
predicate: #Predicate { $0.endTime == nil },
|
|
sortBy: [SortDescriptor(\.startTime, order: .forward)]
|
|
)
|
|
activeSessions = (try? context.fetch(descriptor)) ?? []
|
|
}
|
|
|
|
/// Returns the running session belonging to `action`, if any.
|
|
func runningSession(for action: Action, context: ModelContext) -> TimeSession? {
|
|
let actionID = action.id
|
|
let descriptor = FetchDescriptor<TimeSession>(
|
|
predicate: #Predicate { $0.endTime == nil && $0.action?.id == actionID }
|
|
)
|
|
return try? context.fetch(descriptor).first
|
|
}
|
|
|
|
/// Whether the given action currently has a running session.
|
|
func isTracking(_ action: Action) -> Bool {
|
|
activeSessions.contains { $0.action?.id == action.id }
|
|
}
|
|
|
|
// MARK: - Persistence
|
|
|
|
private func save(_ context: ModelContext) {
|
|
do {
|
|
try context.save()
|
|
} catch {
|
|
// Persistence failures are non-fatal for the in-flight UI; surface in logs.
|
|
print("TrackingViewModel save failed: \(error)")
|
|
}
|
|
}
|
|
}
|