Files
timetracker/ios/TimeTracker/TimeTracker/Shared/Components/StatCard.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

69 lines
2.0 KiB
Swift

import SwiftUI
struct StatCard: View {
let title: String
let value: String
let icon: String
var color: Color = .accentColor
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: icon)
.foregroundStyle(color)
Text(title)
.font(.subheadline)
.foregroundStyle(.secondary)
}
Text(value)
.font(.title2)
.fontWeight(.semibold)
.foregroundStyle(.primary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color(.systemGray6))
.cornerRadius(12)
}
}
extension TimeInterval {
/// Formats as a clock string used for the live timer display: "1:23:45" or "05:30".
var formattedDuration: String {
let totalSeconds = Int(self)
let hours = totalSeconds / 3600
let minutes = (totalSeconds % 3600) / 60
let seconds = totalSeconds % 60
if hours > 0 {
return String(format: "%d:%02d:%02d", hours, minutes, seconds)
} else {
return String(format: "%02d:%02d", minutes, seconds)
}
}
/// Human-readable duration used in lists and cards: "3h 48min", "45min", "< 1min".
var formattedShortDuration: String {
let totalSeconds = Int(self)
let hours = totalSeconds / 3600
let minutes = (totalSeconds % 3600) / 60
if hours > 0 && minutes > 0 {
return "\(hours)h \(minutes)min"
} else if hours > 0 {
return "\(hours)h"
} else if minutes > 0 {
return "\(minutes)min"
} else {
return "< 1min"
}
}
/// Formats as hours with one decimal place, e.g. "3.8h". Used by the widget.
var formattedHours: String {
let hours = self / 3600
return String(format: "%.1fh", hours)
}
}