Files
timetracker/ios/TimeTracker/TimeTracker/Features/Dashboard/DashboardViewModel.swift
Simon Franken ba4765b8a2 Rebuild iOS app: calendar entries, overtime dashboard, settings tab, full CRUD
- Replace 5-tab layout with 4 tabs: Dashboard, Timer, Entries, Settings
- Dashboard: add Work Time Balance section using /client-targets API, showing
  per-client weekly progress bar, overtime/undertime label and expandable week breakdown
- Time Entries: replace flat list with UICalendarView month grid; tap a day to see
  that day's entries; add filter sheet (date range, project, client); new
  TimeEntryDetailSheet for creating and editing entries; duration shown as Xh Ymin
- Settings tab: user info header, navigation to Clients and Projects, logout button
- ClientsListView: list with NavigationLink to ClientDetailView
- ClientDetailView: inline client editing + full work time target CRUD (create,
  edit, delete target; add/delete balance corrections with date, hours, description)
- ProjectsListView: grouped by client, NavigationLink to ProjectDetailView
- ProjectDetailView: edit name, description, colour, client assignment
- Add ClientTarget, WeekBalance, BalanceCorrection models and APIEndpoints for
  /client-targets routes
- Update TimeInterval formatter: add formattedShortDuration (Xh Ymin / Xmin / <1min)
  used throughout app; keep formattedDuration for live timer display
2026-02-21 13:51:41 +01:00

97 lines
3.1 KiB
Swift

import Foundation
import SwiftUI
@MainActor
final class DashboardViewModel: ObservableObject {
@Published var activeTimer: OngoingTimer?
@Published var statistics: TimeStatistics?
@Published var recentEntries: [TimeEntry] = []
@Published var clientTargets: [ClientTarget] = []
@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
)
// Statistics for this week
let calendar = Calendar.current
let today = Date()
let startOfWeek = calendar.date(
from: calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: today)
)!
let endOfWeek = calendar.date(byAdding: .day, value: 6, to: startOfWeek)!
statistics = try await apiClient.request(
endpoint: APIEndpoint.timeEntriesStatistics,
queryItems: [
URLQueryItem(name: "startDate", value: startOfWeek.iso8601FullDate),
URLQueryItem(name: "endDate", value: endOfWeek.iso8601FullDate)
],
authenticated: true
)
// Recent entries (last 5)
let entriesResponse: TimeEntryListResponse = try await apiClient.request(
endpoint: APIEndpoint.timeEntries,
queryItems: [URLQueryItem(name: "limit", value: "5")],
authenticated: true
)
recentEntries = entriesResponse.entries
// Client targets (for overtime/undertime)
clientTargets = try await apiClient.request(
endpoint: APIEndpoint.clientTargets,
authenticated: true
)
if let timer = activeTimer {
elapsedTime = timer.elapsedTime
}
isLoading = false
} catch {
isLoading = false
self.error = error.localizedDescription
// Fallback to cached timer
if let cachedTimer = try? await database.getCachedTimer() {
activeTimer = cachedTimer
elapsedTime = cachedTimer.elapsedTime
}
}
}
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
}
}
}
}
}