learning_ai_clock/ios/ChronoMind/Views/History/HistoryView.swift

243 lines
9.3 KiB
Swift

// 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"
}
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
}
}
}
.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) {
// 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: - 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))
}
}