Files
timetracker/ios/TimeTracker/TimeTracker/Features/Timer/TimerViewModel.swift
Simon Franken 39d6ea00d9 Fix iOS list response decoding to match plain-array backend responses
GET /clients and GET /projects return bare arrays, not wrapped objects.
Remove ClientListResponse and ProjectListResponse wrapper structs and
update ClientsViewModel, ProjectsViewModel, and TimerViewModel to decode
[Client] and [Project] directly.
2026-02-19 19:05:43 +01:00

160 lines
4.5 KiB
Swift

import Foundation
import SwiftUI
@MainActor
final class TimerViewModel: ObservableObject {
@Published var activeTimer: OngoingTimer?
@Published var projects: [Project] = []
@Published var selectedProject: Project?
@Published var isLoading = false
@Published var error: String?
@Published var elapsedTime: TimeInterval = 0
private let apiClient = APIClient()
private let database = DatabaseService.shared
private var timerTask: Task<Void, Never>?
init() {
startElapsedTimeUpdater()
}
deinit {
timerTask?.cancel()
}
func loadData() async {
isLoading = true
error = nil
do {
// Fetch active timer
activeTimer = try await apiClient.request(
endpoint: APIEndpoint.timer,
authenticated: true
)
// Cache timer for widget
try await database.cacheTimer(activeTimer)
// Fetch projects
projects = try await apiClient.request(
endpoint: APIEndpoint.projects,
authenticated: true
)
// Set selected project if timer has one
if let timerProject = activeTimer?.project {
selectedProject = projects.first { $0.id == timerProject.id }
}
// Calculate elapsed time
if let timer = activeTimer {
elapsedTime = timer.elapsedTime
}
isLoading = false
} catch {
isLoading = false
self.error = error.localizedDescription
// Try to load cached data
if let cachedTimer = try? await database.getCachedTimer() {
activeTimer = cachedTimer
elapsedTime = cachedTimer.elapsedTime
}
}
}
func startTimer() async {
isLoading = true
error = nil
do {
let input = StartTimerInput(projectId: selectedProject?.id)
activeTimer = try await apiClient.request(
endpoint: APIEndpoint.timerStart,
method: .post,
body: input,
authenticated: true
)
try await database.cacheTimer(activeTimer)
if let timer = activeTimer {
elapsedTime = timer.elapsedTime
}
isLoading = false
} catch {
isLoading = false
self.error = error.localizedDescription
}
}
func stopTimer() async {
guard let timer = activeTimer else { return }
isLoading = true
error = nil
let projectId = selectedProject?.id ?? timer.projectId ?? ""
do {
let input = StopTimerInput(projectId: projectId)
try await apiClient.requestVoid(
endpoint: APIEndpoint.timerStop,
method: .post,
body: input,
authenticated: true
)
activeTimer = nil
selectedProject = nil
elapsedTime = 0
try await database.cacheTimer(nil)
isLoading = false
} catch {
isLoading = false
self.error = error.localizedDescription
}
}
func updateProject(_ project: Project?) async {
selectedProject = project
guard let timer = activeTimer else { return }
do {
guard let projectId = project?.id else { return }
let input = UpdateTimerInput(projectId: projectId)
activeTimer = try await apiClient.request(
endpoint: APIEndpoint.timer,
method: .put,
body: input,
authenticated: true
)
try await database.cacheTimer(activeTimer)
} catch {
self.error = error.localizedDescription
}
}
private func startElapsedTimeUpdater() {
timerTask = Task { [weak self] in
while !Task.isCancelled {
try? await Task.sleep(nanoseconds: 1_000_000_000)
guard let self = self, self.activeTimer != nil else { continue }
await MainActor.run {
self.elapsedTime = self.activeTimer?.elapsedTime ?? 0
}
}
}
}
}