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.
160 lines
4.5 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|