// // ProgressViewModel.swift // SportsTime // // ViewModel for stadium progress tracking and visualization. // import Foundation import SwiftUI import SwiftData @MainActor @Observable final class ProgressViewModel { // MARK: - State var selectedSport: Sport = .mlb var isLoading = true var error: Error? var errorMessage: String? // MARK: - Data private(set) var visits: [StadiumVisit] = [] private(set) var stadiums: [Stadium] = [] private(set) var teams: [Team] = [] // MARK: - Dependencies private var modelContainer: ModelContainer? private var modelContext: ModelContext? private let dataProvider = AppDataProvider.shared // MARK: - Derived State (recomputed after mutations) /// Overall progress for the selected sport private(set) var leagueProgress: LeagueProgress = LeagueProgress(sport: .mlb, totalStadiums: 0, visitedStadiums: 0, stadiumsVisited: [], stadiumsRemaining: []) /// Stadium visit status indexed by stadium ID private(set) var stadiumVisitStatus: [String: StadiumVisitStatus] = [:] /// Visited stadiums for the selected sport private(set) var visitedStadiums: [Stadium] = [] /// Unvisited stadiums for the selected sport private(set) var unvisitedStadiums: [Stadium] = [] /// Stadiums for the selected sport var sportStadiums: [Stadium] { stadiums.filter { $0.sport == selectedSport } } /// Count of trips for the selected sport (stub - can be enhanced) var tripCount: Int { // TODO: Fetch saved trips count from SwiftData 0 } /// Recent visits sorted by date var recentVisits: [VisitSummary] { visits .sorted { $0.visitDate > $1.visitDate } .prefix(10) .compactMap { visit -> VisitSummary? in guard let stadium = dataProvider.stadium(for: visit.stadiumId), let sport = visit.sportEnum else { return nil } return VisitSummary( id: visit.id, stadium: stadium, visitDate: visit.visitDate, visitType: visit.visitType, sport: sport, homeTeamName: visit.homeTeamName, awayTeamName: visit.awayTeamName, score: visit.finalScore, photoCount: visit.photoMetadata?.count ?? 0, notes: visit.notes ) } } // MARK: - Configuration func configure(with container: ModelContainer) { self.modelContainer = container self.modelContext = ModelContext(container) } // MARK: - Actions func loadData() async { isLoading = true error = nil errorMessage = nil do { // Load stadiums and teams from data provider if dataProvider.stadiums.isEmpty { await dataProvider.loadInitialData() } stadiums = dataProvider.stadiums teams = dataProvider.teams // Load visits from SwiftData if let context = modelContext { let descriptor = FetchDescriptor( sortBy: [SortDescriptor(\.visitDate, order: .reverse)] ) visits = try context.fetch(descriptor) } } catch { self.error = error self.errorMessage = error.localizedDescription } recomputeDerivedState() isLoading = false } func selectSport(_ sport: Sport) { selectedSport = sport recomputeDerivedState() AnalyticsManager.shared.track(.sportSwitched(sport: sport.rawValue)) } func clearError() { error = nil errorMessage = nil } // MARK: - Visit Management func deleteVisit(_ visit: StadiumVisit) async throws { guard let context = modelContext else { return } if let sport = visit.sportEnum { AnalyticsManager.shared.track(.stadiumVisitDeleted(stadiumId: visit.stadiumId, sport: sport.rawValue)) } context.delete(visit) try context.save() // Reload data await loadData() } // MARK: - Derived State Recomputation private func recomputeDerivedState() { // Compute league progress once let sportStadiums = stadiums.filter { $0.sport == selectedSport } let visitedStadiumIds = Set( visits .filter { $0.sportEnum == selectedSport } .compactMap { visit -> String? in dataProvider.stadium(for: visit.stadiumId)?.id } ) let visited = sportStadiums.filter { visitedStadiumIds.contains($0.id) } let remaining = sportStadiums.filter { !visitedStadiumIds.contains($0.id) } leagueProgress = LeagueProgress( sport: selectedSport, totalStadiums: sportStadiums.count, visitedStadiums: visited.count, stadiumsVisited: visited, stadiumsRemaining: remaining ) visitedStadiums = visited unvisitedStadiums = remaining // Compute stadium visit status once var statusMap: [String: StadiumVisitStatus] = [:] let visitsByStadium = Dictionary(grouping: visits.filter { $0.sportEnum == selectedSport }) { $0.stadiumId } for stadium in stadiums { if let stadiumVisits = visitsByStadium[stadium.id], !stadiumVisits.isEmpty { let summaries = stadiumVisits.map { visit in VisitSummary( id: visit.id, stadium: stadium, visitDate: visit.visitDate, visitType: visit.visitType, sport: selectedSport, homeTeamName: visit.homeTeamName, awayTeamName: visit.awayTeamName, score: visit.finalScore, photoCount: visit.photoMetadata?.count ?? 0, notes: visit.notes ) } statusMap[stadium.id] = .visited(visits: summaries) } else { statusMap[stadium.id] = .notVisited } } stadiumVisitStatus = statusMap } // MARK: - Progress Card Generation func progressCardData(includeUsername: Bool = false) -> ProgressCardData { ProgressCardData( sport: selectedSport, progress: leagueProgress, username: nil, includeMap: true, showDetailedStats: false ) } }