diff --git a/ios/TimeTracker/TimeTracker/Features/Timer/TimerView.swift b/ios/TimeTracker/TimeTracker/Features/Timer/TimerView.swift index 2b59d03..60f94d6 100644 --- a/ios/TimeTracker/TimeTracker/Features/Timer/TimerView.swift +++ b/ios/TimeTracker/TimeTracker/Features/Timer/TimerView.swift @@ -44,6 +44,8 @@ struct TimerView: View { .font(.system(size: 64, weight: .light, design: .monospaced)) .foregroundStyle(viewModel.activeTimer != nil ? .primary : .secondary) + TimerUnitLabels(elapsed: viewModel.elapsedTime) + if let project = viewModel.selectedProject { ProjectColorBadge( color: project.color, @@ -123,7 +125,31 @@ struct TimerView: View { } } -struct ProjectPickerSheet: View { +/// Displays "h min sec" (or "min sec") column labels aligned under the +/// monospaced timer digits. +private struct TimerUnitLabels: View { + let elapsed: TimeInterval + + private var showHours: Bool { Int(elapsed) >= 3600 } + + var body: some View { + HStack(spacing: 0) { + if showHours { + Text("h") + .frame(width: 64) + Text("min") + .frame(width: 64) + } else { + Text("min") + .frame(width: 64) + } + Text("sec") + .frame(width: 64) + } + .font(.caption) + .foregroundStyle(.secondary) + } +} let projects: [Project] let selectedProject: Project? let onSelect: (Project?) -> Void diff --git a/ios/TimeTracker/TimeTracker/Models/OngoingTimer.swift b/ios/TimeTracker/TimeTracker/Models/OngoingTimer.swift index f4b8714..e8dd9b8 100644 --- a/ios/TimeTracker/TimeTracker/Models/OngoingTimer.swift +++ b/ios/TimeTracker/TimeTracker/Models/OngoingTimer.swift @@ -9,9 +9,7 @@ struct OngoingTimer: Codable, Identifiable, Equatable { let updatedAt: String var elapsedTime: TimeInterval { - guard let start = ISO8601DateFormatter().date(from: startTime) else { - return 0 - } + guard let start = Date.fromISO8601(startTime) else { return 0 } return Date().timeIntervalSince(start) } } diff --git a/ios/TimeTracker/TimeTracker/Models/TimeEntry.swift b/ios/TimeTracker/TimeTracker/Models/TimeEntry.swift index 38cd007..ac54d41 100644 --- a/ios/TimeTracker/TimeTracker/Models/TimeEntry.swift +++ b/ios/TimeTracker/TimeTracker/Models/TimeEntry.swift @@ -11,10 +11,8 @@ struct TimeEntry: Codable, Identifiable, Equatable { let updatedAt: String var duration: TimeInterval { - guard let start = ISO8601DateFormatter().date(from: startTime), - let end = ISO8601DateFormatter().date(from: endTime) else { - return 0 - } + guard let start = Date.fromISO8601(startTime), + let end = Date.fromISO8601(endTime) else { return 0 } return end.timeIntervalSince(start) } } diff --git a/ios/TimeTracker/TimeTracker/Shared/Extensions/Date+Extensions.swift b/ios/TimeTracker/TimeTracker/Shared/Extensions/Date+Extensions.swift index 21551a7..d7dec6b 100644 --- a/ios/TimeTracker/TimeTracker/Shared/Extensions/Date+Extensions.swift +++ b/ios/TimeTracker/TimeTracker/Shared/Extensions/Date+Extensions.swift @@ -59,8 +59,14 @@ extension Date { } static func fromISO8601(_ string: String) -> Date? { - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime] - return formatter.date(from: string) + // Try with fractional seconds first (e.g. "2026-02-20T09:00:00.000Z" from + // Prisma/Node.js JSON serialisation), then fall back to whole seconds. + let withFractional = ISO8601DateFormatter() + withFractional.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + if let date = withFractional.date(from: string) { return date } + + let wholeSec = ISO8601DateFormatter() + wholeSec.formatOptions = [.withInternetDateTime] + return wholeSec.date(from: string) } }