- 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
69 lines
2.0 KiB
Swift
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)
|
|
}
|
|
}
|