// // ExerciseTrendsView.swift // Workouts // // Copyright 2026 Rouslan Zenetl. All Rights Reserved. // import SwiftUI import SwiftData /// Per-exercise drill-down: every weighted exercise with completed history, most /// recently trained first, each opening its weight-progression detail. This promotes /// the progression chart to a first-class Progress surface (it also remains on the /// library's reference pages). struct ExerciseTrendsListView: View { @AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb /// Completed weighted logs, newest first. @Query private var logs: [WorkoutLog] init() { let completedRaw = WorkoutStatus.completed.rawValue let weightRaw = LoadType.weight.rawValue _logs = Query( filter: #Predicate { $0.statusRaw == completedRaw && $0.loadType == weightRaw }, sort: \WorkoutLog.date, order: .reverse) } private struct ExerciseSummary: Identifiable { var name: String var sessions: Int var best: Double var last: Date var id: String { name } } /// One row per exercise name: session count, best top-set weight, last trained. /// `logs` is newest-first, so the first log seen per name fixes `last`. private var summaries: [ExerciseSummary] { var byName: [String: ExerciseSummary] = [:] for log in logs { let top = log.setEntries?.compactMap(\.weight).max() ?? log.weight if var summary = byName[log.exerciseName] { summary.sessions += 1 summary.best = max(summary.best, top) byName[log.exerciseName] = summary } else { byName[log.exerciseName] = ExerciseSummary( name: log.exerciseName, sessions: 1, best: top, last: log.date) } } return byName.values.sorted { $0.last > $1.last } } var body: some View { List(summaries) { summary in NavigationLink { ExerciseTrendDetailView(exerciseName: summary.name) } label: { HStack { VStack(alignment: .leading, spacing: 2) { Text(summary.name) Text("\(summary.sessions) session\(summary.sessions == 1 ? "" : "s") · last \(summary.last.daysAgoLabel())") .font(.caption) .foregroundStyle(.secondary) } Spacer() Text(weightUnit.format(summary.best)) .font(.subheadline.weight(.semibold)) .monospacedDigit() .foregroundStyle(.secondary) } } } .overlay { if summaries.isEmpty { ContentUnavailableView( "No Weighted Exercises Yet", systemImage: "dumbbell", description: Text("Complete weighted sets and each exercise's progression shows up here.")) } } .navigationTitle("Exercise Trends") } } /// One exercise's progression: headline stats over its completed history plus the /// weight-progression chart. struct ExerciseTrendDetailView: View { let exerciseName: String @AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb /// Completed logs for this exercise, oldest first (matching the chart's order). @Query private var logs: [WorkoutLog] init(exerciseName: String) { self.exerciseName = exerciseName let name = exerciseName let completedRaw = WorkoutStatus.completed.rawValue _logs = Query( filter: #Predicate { $0.exerciseName == name && $0.statusRaw == completedRaw }, sort: \WorkoutLog.date, order: .forward) } private var bestWeight: Double { logs.map { $0.setEntries?.compactMap(\.weight).max() ?? $0.weight }.max() ?? 0 } var body: some View { ScrollView { VStack(alignment: .leading, spacing: 16) { HStack(spacing: 12) { ProgressStatTile( title: "Best", value: weightUnit.format(bestWeight), unit: nil) ProgressStatTile( title: "Sessions", value: "\(logs.count)", unit: nil) ProgressStatTile( title: "Last", value: logs.last?.date.daysAgoLabel() ?? "—", unit: nil) } WeightProgressionChartView(exerciseName: exerciseName) .progressCard() } .padding(.horizontal) } .background(Color(.systemGroupedBackground)) .navigationTitle(exerciseName) .navigationBarTitleDisplayMode(.inline) } }