import SwiftUI struct StatisticsView: View { @Environment(AuthSession.self) private var auth @State private var routes: [RouteListItem] = [] @State private var isLoading = false @State private var errorMessage: String? var body: some View { NavigationStack { content .navigationTitle("Statistik") .task { await load() } .refreshable { await load() } } } @ViewBuilder private var content: some View { if isLoading && routes.isEmpty { ProgressView() } else if let errorMessage, routes.isEmpty { ContentUnavailableView( "Konnte Statistik nicht laden", systemImage: "exclamationmark.triangle", description: Text(errorMessage) ) } else if myRoutes.isEmpty { ContentUnavailableView( "Noch keine Touren", systemImage: "chart.bar.xaxis", description: Text("Sobald du Gassi-Touren aufnimmst, siehst du hier deine Zahlen.") ) } else { List { Section("Diese Woche") { stats(in: weekRoutes) } Section("Diesen Monat") { stats(in: monthRoutes) } Section("Allzeit") { stats(in: myRoutes) LabeledContent("Längste Tour", value: longestKm) LabeledContent("Aktuelle Serie", value: streakLabel) } } } } private func stats(in r: [RouteListItem]) -> some View { Group { LabeledContent("Distanz", value: String(format: "%.1f km", r.compactMap(\.distanzKm).reduce(0, +))) LabeledContent("Dauer", value: formatTotalMinutes(r.compactMap(\.dauerMin).reduce(0, +))) LabeledContent("Touren", value: "\(r.count)") } } private func load() async { isLoading = true errorMessage = nil defer { isLoading = false } do { routes = try await APIClient.shared.get("/api/routes") } catch { errorMessage = error.localizedDescription } } // MARK: - Filtering private var myId: Int? { auth.profile?.id } private var myRoutes: [RouteListItem] { guard let myId else { return [] } return routes.filter { $0.userId == myId } } private var weekRoutes: [RouteListItem] { let cal = Calendar.current let start = cal.dateInterval(of: .weekOfYear, for: .now)?.start ?? .now return myRoutes.filter { dateFromAPI($0.createdAt) >= start } } private var monthRoutes: [RouteListItem] { let cal = Calendar.current let start = cal.dateInterval(of: .month, for: .now)?.start ?? .now return myRoutes.filter { dateFromAPI($0.createdAt) >= start } } // MARK: - Derived private var longestKm: String { let max = myRoutes.compactMap(\.distanzKm).max() ?? 0 return String(format: "%.2f km", max) } private var streakLabel: String { let days = currentStreakDays() if days == 0 { return "—" } return days == 1 ? "1 Tag" : "\(days) Tage" } private func currentStreakDays() -> Int { let cal = Calendar.current let doneDays = Set(myRoutes.map { cal.startOfDay(for: dateFromAPI($0.createdAt)) }) guard !doneDays.isEmpty else { return 0 } var day = cal.startOfDay(for: .now) if !doneDays.contains(day) { day = cal.date(byAdding: .day, value: -1, to: day) ?? day } var streak = 0 while doneDays.contains(day) { streak += 1 day = cal.date(byAdding: .day, value: -1, to: day) ?? day } return streak } private func dateFromAPI(_ str: String?) -> Date { guard let str else { return .distantPast } let parser = DateFormatter() parser.locale = Locale(identifier: "en_US_POSIX") parser.timeZone = TimeZone(identifier: "UTC") for format in ["yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ssZ"] { parser.dateFormat = format if let d = parser.date(from: str) { return d } } return .distantPast } private func formatTotalMinutes(_ totalMin: Int) -> String { let h = totalMin / 60 let m = totalMin % 60 if h > 0 { return "\(h) h \(m) min" } return "\(m) min" } }