// ── History View ─────────────────────────────────────────────── // Past timers, stats, streaks import SwiftUI struct HistoryView: View { @EnvironmentObject var store: TimerStore @State private var selectedSegment: HistorySegment = .recent enum HistorySegment: String, CaseIterable { case recent = "Recent" case stats = "Stats" case badges = "Badges" } private var completedTimers: [CMTimer] { store.timers .filter { [.completed, .dismissed].contains($0.state) } .sorted { ($0.completedAt ?? $0.dismissedAt ?? .distantPast) > ($1.completedAt ?? $1.dismissedAt ?? .distantPast) } } var body: some View { NavigationStack { ZStack { CMColors.bg.ignoresSafeArea() VStack(spacing: 0) { // Segment picker Picker("View", selection: $selectedSegment) { ForEach(HistorySegment.allCases, id: \.self) { segment in Text(segment.rawValue).tag(segment) } } .pickerStyle(.segmented) .padding(.horizontal, CMSpacing.lg) .padding(.vertical, CMSpacing.md) switch selectedSegment { case .recent: recentList case .stats: statsView case .badges: badgesView } } } .navigationTitle("History") .navigationBarTitleDisplayMode(.inline) .toolbarBackground(CMColors.surface, for: .navigationBar) .toolbarColorScheme(.dark, for: .navigationBar) } } // MARK: - Recent List private var recentList: some View { Group { if completedTimers.isEmpty { VStack(spacing: CMSpacing.lg) { Spacer() Image(systemName: "clock.arrow.circlepath") .font(.system(size: 48)) .foregroundStyle(CMColors.textMuted) Text("No timer history yet") .font(CMFonts.body(size: 18, weight: .semibold)) .foregroundStyle(CMColors.textSecondary) Text("Completed and dismissed timers\nwill appear here") .font(CMFonts.body(size: 14)) .foregroundStyle(CMColors.textMuted) .multilineTextAlignment(.center) Spacer() } } else { ScrollView { LazyVStack(spacing: CMSpacing.md) { ForEach(completedTimers) { timer in HistoryCard(timer: timer) } } .padding(.horizontal, CMSpacing.lg) .padding(.bottom, CMSpacing.xxl) } } } } // MARK: - Stats View private var statsView: some View { ScrollView { VStack(spacing: CMSpacing.lg) { // Streak card StreakCard() // Focus score let focusScore = GamificationEngine.calculateFocusScore(timers: store.timers) FocusScoreCard(score: focusScore) // Weekly summary card (shareable) let summary = GamificationEngine.generateWeeklySummary( timers: store.timers, streak: GamificationStore.shared.streak ) WeeklySummaryCard(summary: summary) // Summary cards let allTimers = store.timers let completed = allTimers.filter { $0.state == .completed }.count let dismissed = allTimers.filter { $0.state == .dismissed }.count let active = store.activeTimers.count let totalSnoozes = allTimers.reduce(0) { $0 + $1.snoozeCount } LazyVGrid(columns: [ GridItem(.flexible(), spacing: CMSpacing.md), GridItem(.flexible(), spacing: CMSpacing.md), ], spacing: CMSpacing.md) { StatCard(title: "Completed", value: "\(completed)", icon: "checkmark.circle.fill", color: CMColors.success) StatCard(title: "Dismissed", value: "\(dismissed)", icon: "xmark.circle.fill", color: CMColors.textMuted) StatCard(title: "Active", value: "\(active)", icon: "play.circle.fill", color: CMColors.accent) StatCard(title: "Snoozes", value: "\(totalSnoozes)", icon: "moon.zzz.fill", color: CMColors.important) } // On-time rate if completed + dismissed > 0 { let onTimeRate = Double(completed) / Double(completed + dismissed) * 100 VStack(alignment: .leading, spacing: CMSpacing.sm) { HStack { Text("Completion Rate") .font(CMFonts.body(size: 14, weight: .medium)) .foregroundStyle(CMColors.textSecondary) Spacer() Text(String(format: "%.0f%%", onTimeRate)) .font(CMFonts.mono(size: 20, weight: .bold)) .foregroundStyle(onTimeRate >= 70 ? CMColors.success : CMColors.important) } GeometryReader { geo in ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 4) .fill(CMColors.border) .frame(height: 8) RoundedRectangle(cornerRadius: 4) .fill(onTimeRate >= 70 ? CMColors.success : CMColors.important) .frame(width: geo.size.width * (onTimeRate / 100), height: 8) } } .frame(height: 8) } .padding(CMSpacing.lg) .background(CMColors.surface) .clipShape(RoundedRectangle(cornerRadius: CMRadius.md)) } // Timer type breakdown let typeBreakdown = Dictionary(grouping: allTimers, by: { $0.type }) VStack(alignment: .leading, spacing: CMSpacing.md) { Text("By Type") .font(CMFonts.body(size: 14, weight: .medium)) .foregroundStyle(CMColors.textSecondary) ForEach(CMTimerType.allCases) { type in let count = typeBreakdown[type]?.count ?? 0 if count > 0 { HStack { Text(type.label) .font(CMFonts.body(size: 14)) .foregroundStyle(CMColors.text) Spacer() Text("\(count)") .font(CMFonts.mono(size: 14, weight: .semibold)) .foregroundStyle(CMColors.accent) } } } } .padding(CMSpacing.lg) .background(CMColors.surface) .clipShape(RoundedRectangle(cornerRadius: CMRadius.md)) } .padding(.horizontal, CMSpacing.lg) .padding(.bottom, CMSpacing.xxl) } } // MARK: - Badges View private var badgesView: some View { ScrollView { VStack(spacing: CMSpacing.lg) { // Streak card (compact) StreakCard() // Badge grid BadgeGridView() } .padding(.horizontal, CMSpacing.lg) .padding(.bottom, CMSpacing.xxl) } } } // MARK: - Stat Card struct StatCard: View { let title: String let value: String let icon: String let color: Color var body: some View { VStack(spacing: CMSpacing.sm) { Image(systemName: icon) .font(.title2) .foregroundStyle(color) Text(value) .font(CMFonts.mono(size: 28, weight: .bold)) .foregroundStyle(CMColors.text) Text(title) .font(CMFonts.body(size: 12, weight: .medium)) .foregroundStyle(CMColors.textMuted) } .frame(maxWidth: .infinity) .padding(CMSpacing.lg) .background(CMColors.surface) .clipShape(RoundedRectangle(cornerRadius: CMRadius.md)) .overlay( RoundedRectangle(cornerRadius: CMRadius.md) .stroke(CMColors.border, lineWidth: 1) ) } } // MARK: - History Card struct HistoryCard: View { let timer: CMTimer var body: some View { HStack(spacing: CMSpacing.md) { // Urgency dot Circle() .fill(CMColors.urgencyColor(timer.urgency)) .frame(width: 8, height: 8) VStack(alignment: .leading, spacing: CMSpacing.xxs) { Text(timer.label) .font(CMFonts.body(size: 14, weight: .medium)) .foregroundStyle(CMColors.text) let endDate = timer.completedAt ?? timer.dismissedAt ?? timer.createdAt Text(formatDateTime(endDate)) .font(CMFonts.body(size: 12)) .foregroundStyle(CMColors.textMuted) } Spacer() // State badge if timer.state == .completed { Image(systemName: "checkmark.circle.fill") .foregroundStyle(CMColors.success) } else { Image(systemName: "xmark.circle") .foregroundStyle(CMColors.textMuted) } } .padding(CMSpacing.md) .background(CMColors.surface) .clipShape(RoundedRectangle(cornerRadius: CMRadius.sm)) } }