import Combine import SwiftUI struct SessionDetailView: View { let sessionId: UUID @EnvironmentObject var dataManager: ClimbingDataManager @Environment(\.dismiss) private var dismiss @State private var showingDeleteAlert = false @State private var showingAddAttempt = false @State private var editingAttempt: Attempt? @State private var attemptToDelete: Attempt? @State private var currentTime = Date() private var session: ClimbSession? { dataManager.session(withId: sessionId) } private func startTimer() { // Update every 5 seconds instead of 1 second for better performance timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in currentTime = Date() } } private func stopTimer() { timer?.invalidate() timer = nil } private var gym: Gym? { guard let session = session else { return nil } return dataManager.gym(withId: session.gymId) } private var attempts: [Attempt] { dataManager.attempts(forSession: sessionId) } private var attemptsWithProblems: [(Attempt, Problem)] { attempts.compactMap { attempt in guard let problem = dataManager.problem(withId: attempt.problemId) else { return nil } return (attempt, problem) }.sorted { $0.0.timestamp < $1.0.timestamp } } private var sessionStats: SessionStats { calculateSessionStats() } @State private var timer: Timer? var body: some View { ScrollView { LazyVStack(spacing: 20) { if let session = session, let gym = gym { SessionHeaderCard( session: session, gym: gym, stats: sessionStats, currentTime: currentTime) SessionStatsCard(stats: sessionStats) AttemptsSection( attemptsWithProblems: attemptsWithProblems, attemptToDelete: $attemptToDelete, editingAttempt: $editingAttempt) } else { Text("Session not found") .foregroundColor(.secondary) } } .padding() } .onAppear { startTimer() } .onDisappear { stopTimer() } .navigationTitle("Session Details") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { if let session = session { if session.status == .active { Button("End Session") { dataManager.endSession(session.id) dismiss() } .foregroundColor(.orange) } else { Menu { Button(role: .destructive) { showingDeleteAlert = true } label: { Label("Delete Session", systemImage: "trash") } } label: { Image(systemName: "ellipsis.circle") } } } } } .alert( "Delete Attempt", isPresented: Binding( get: { attemptToDelete != nil }, set: { if !$0 { attemptToDelete = nil } } ) ) { Button("Cancel", role: .cancel) { attemptToDelete = nil } Button("Delete", role: .destructive) { if let attempt = attemptToDelete { dataManager.deleteAttempt(attempt) attemptToDelete = nil } } } message: { if let attempt = attemptToDelete, let problem = dataManager.problem(withId: attempt.problemId) { Text( "Are you sure you want to delete this attempt on \"\(problem.name ?? "Unknown Problem")\"? This action cannot be undone." ) } else { Text("Are you sure you want to delete this attempt? This action cannot be undone.") } } .overlay(alignment: .bottomTrailing) { if session?.status == .active { Button(action: { showingAddAttempt = true }) { Image(systemName: "plus") .font(.title2) .foregroundColor(.white) .frame(width: 56, height: 56) .background(Circle().fill(.blue)) .shadow(radius: 4) } .padding() } } .alert("Delete Session", isPresented: $showingDeleteAlert) { Button("Cancel", role: .cancel) {} Button("Delete", role: .destructive) { if let session = session { dataManager.deleteSession(session) dismiss() } } } message: { Text( "Are you sure you want to delete this session? This will also delete all attempts associated with this session." ) } .sheet(isPresented: $showingAddAttempt) { if let session = session, let gym = gym { AddAttemptView(session: session, gym: gym) } } .sheet(item: $editingAttempt) { attempt in EditAttemptView(attempt: attempt) } } private func calculateSessionStats() -> SessionStats { let successfulAttempts = attempts.filter { $0.result.isSuccessful } let uniqueProblems = Set(attempts.map { $0.problemId }) let completedProblems = Set(successfulAttempts.map { $0.problemId }) return SessionStats( totalAttempts: attempts.count, successfulAttempts: successfulAttempts.count, uniqueProblemsAttempted: uniqueProblems.count, uniqueProblemsCompleted: completedProblems.count ) } } struct SessionHeaderCard: View { let session: ClimbSession let gym: Gym let stats: SessionStats let currentTime: Date var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) { Text(gym.name) .font(.title) .fontWeight(.bold) Text(formatDate(session.date)) .font(.title2) .foregroundColor(.blue) if session.status == .active { if let startTime = session.startTime { Text("Duration: \(formatDuration(from: startTime, to: currentTime))") .font(.subheadline) .foregroundColor(.secondary) } } else if let duration = session.duration { Text("Duration: \(duration) minutes") .font(.subheadline) .foregroundColor(.secondary) } if let notes = session.notes, !notes.isEmpty { Text(notes) .font(.body) .padding(.top, 4) } } // Status indicator HStack { Image(systemName: session.status == .active ? "play.fill" : "checkmark.circle.fill") .foregroundColor(session.status == .active ? .green : .blue) Text(session.status == .active ? "In Progress" : "Completed") .font(.subheadline) .fontWeight(.medium) .foregroundColor(session.status == .active ? .green : .blue) Spacer() } .padding(.horizontal, 12) .padding(.vertical, 6) .background( RoundedRectangle(cornerRadius: 8) .fill((session.status == .active ? Color.green : Color.blue).opacity(0.1)) ) } .padding() .background( RoundedRectangle(cornerRadius: 16) .fill(.regularMaterial) ) } private func formatDate(_ date: Date) -> String { let formatter = DateFormatter() formatter.dateStyle = .full return formatter.string(from: date) } private func formatDuration(from start: Date, to end: Date) -> String { let interval = end.timeIntervalSince(start) let hours = Int(interval) / 3600 let minutes = Int(interval) % 3600 / 60 let seconds = Int(interval) % 60 if hours > 0 { return String(format: "%dh %dm %ds", hours, minutes, seconds) } else if minutes > 0 { return String(format: "%dm %ds", minutes, seconds) } else { return String(format: "%ds", seconds) } } } struct SessionStatsCard: View { let stats: SessionStats var body: some View { VStack(alignment: .leading, spacing: 16) { Text("Session Stats") .font(.title2) .fontWeight(.bold) if stats.totalAttempts == 0 { Text("No attempts recorded yet") .foregroundColor(.secondary) .frame(maxWidth: .infinity, alignment: .center) .padding() } else { LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 16) { StatItem(label: "Total Attempts", value: "\(stats.totalAttempts)") StatItem(label: "Problems", value: "\(stats.uniqueProblemsAttempted)") StatItem(label: "Completed", value: "\(stats.uniqueProblemsCompleted)") } } } .padding() .background( RoundedRectangle(cornerRadius: 16) .fill(.regularMaterial) ) } } struct StatItem: View { let label: String let value: String var body: some View { VStack(spacing: 4) { Text(value) .font(.title2) .fontWeight(.bold) .foregroundColor(.blue) Text(label) .font(.caption) .foregroundColor(.secondary) } .frame(maxWidth: .infinity) } } struct AttemptsSection: View { let attemptsWithProblems: [(Attempt, Problem)] @Binding var attemptToDelete: Attempt? @Binding var editingAttempt: Attempt? @EnvironmentObject var dataManager: ClimbingDataManager var body: some View { VStack(alignment: .leading, spacing: 16) { Text("Attempts (\(attemptsWithProblems.count))") .font(.title2) .fontWeight(.bold) if attemptsWithProblems.isEmpty { VStack(spacing: 12) { Image(systemName: "hand.raised.slash") .font(.title) .foregroundColor(.secondary) Text("No attempts yet") .font(.headline) .foregroundColor(.secondary) Text("Start attempting problems to see your progress!") .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) } .frame(maxWidth: .infinity) .padding() .background( RoundedRectangle(cornerRadius: 16) .fill(.regularMaterial) ) } else { List { ForEach(attemptsWithProblems.indices, id: \.self) { index in let (attempt, problem) = attemptsWithProblems[index] AttemptCard(attempt: attempt, problem: problem) .listRowBackground(Color.clear) .listRowSeparator(.hidden) .listRowInsets(EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 0)) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button(role: .destructive) { // Add haptic feedback for delete action let impactFeedback = UIImpactFeedbackGenerator(style: .medium) impactFeedback.impactOccurred() attemptToDelete = attempt } label: { Label("Delete", systemImage: "trash") } .accessibilityLabel("Delete attempt") .accessibilityHint("Removes this attempt from the session") Button { editingAttempt = attempt } label: { Label("Edit", systemImage: "pencil") } .tint(.blue) .accessibilityLabel("Edit attempt") .accessibilityHint("Modify the details of this attempt") } .onTapGesture { editingAttempt = attempt } } } .listStyle(.plain) .scrollDisabled(true) .frame(height: CGFloat(attemptsWithProblems.count) * 120) } } } } struct AttemptCard: View { let attempt: Attempt let problem: Problem var body: some View { VStack(alignment: .leading, spacing: 12) { HStack { VStack(alignment: .leading, spacing: 4) { Text(problem.name ?? "Unknown Problem") .font(.headline) .fontWeight(.semibold) Text("\(problem.difficulty.system.displayName): \(problem.difficulty.grade)") .font(.subheadline) .foregroundColor(.blue) if let location = problem.location { Text(location) .font(.caption) .foregroundColor(.secondary) } } Spacer() VStack(alignment: .trailing, spacing: 8) { AttemptResultBadge(result: attempt.result) } } if let notes = attempt.notes, !notes.isEmpty { Text(notes) .font(.subheadline) .foregroundColor(.secondary) } if let highestHold = attempt.highestHold, !highestHold.isEmpty { Text("Highest hold: \(highestHold)") .font(.caption) .foregroundColor(.secondary) } } .padding() .background(.regularMaterial) .cornerRadius(12) .shadow(radius: 2) } } struct AttemptResultBadge: View { let result: AttemptResult private var badgeColor: Color { switch result { case .success, .flash: return .green case .fall: return .orange case .noProgress: return .red } } var body: some View { Text(result.displayName) .font(.caption) .fontWeight(.medium) .padding(.horizontal, 8) .padding(.vertical, 4) .background( RoundedRectangle(cornerRadius: 6) .fill(badgeColor.opacity(0.1)) ) .foregroundColor(badgeColor) .overlay( RoundedRectangle(cornerRadius: 6) .stroke(badgeColor.opacity(0.3), lineWidth: 1) ) } } struct SessionStats { let totalAttempts: Int let successfulAttempts: Int let uniqueProblemsAttempted: Int let uniqueProblemsCompleted: Int } #Preview { NavigationView { SessionDetailView(sessionId: UUID()) .environmentObject(ClimbingDataManager.preview) } }