// // TripWizardViewModel.swift // SportsTime // // ViewModel for trip wizard. // import Foundation import SwiftUI @Observable final class TripWizardViewModel { // MARK: - Planning Mode var planningMode: PlanningMode? = nil { didSet { if oldValue != nil && oldValue != planningMode { resetAllSelections() } } } // MARK: - Sports Selection var selectedSports: Set = [] // MARK: - Dates var startDate: Date = Date() var endDate: Date = Date().addingTimeInterval(86400 * 7) var hasSetDates: Bool = false // MARK: - Regions var selectedRegions: Set = [] // MARK: - Route Preferences var routePreference: RoutePreference = .balanced var hasSetRoutePreference: Bool = false // MARK: - Repeat Cities var allowRepeatCities: Bool = false var hasSetRepeatCities: Bool = false // MARK: - Must Stops var mustStopLocations: [LocationInput] = [] // MARK: - Mode-Specific: gameFirst (cascading selection) var gamePickerSports: Set = [] var gamePickerTeamIds: Set = [] var selectedGameIds: Set = [] // MARK: - Mode-Specific: followTeam var selectedTeamId: String? = nil var teamPickerSport: Sport? = nil // MARK: - Mode-Specific: locations var startLocation: LocationInput? = nil var endLocation: LocationInput? = nil // MARK: - Mode-Specific: teamFirst var teamFirstSport: Sport? = nil var teamFirstSelectedTeamIds: Set = [] // MARK: - Planning State var isPlanning: Bool = false // MARK: - Sport Availability var sportAvailability: [Sport: Bool] = [:] var isLoadingSportAvailability: Bool = false // MARK: - Visibility /// All steps visible once planning mode is selected var areStepsVisible: Bool { planningMode != nil } /// Mode-specific step visibility var showDatesStep: Bool { planningMode == .dateRange || planningMode == .followTeam || planningMode == .locations } var showSportsStep: Bool { planningMode == .dateRange || planningMode == .locations } var showRegionsStep: Bool { planningMode == .dateRange } var showGamePickerStep: Bool { planningMode == .gameFirst } var showTeamPickerStep: Bool { planningMode == .followTeam } var showLocationsStep: Bool { planningMode == .locations } var showTeamFirstStep: Bool { planningMode == .teamFirst } // MARK: - Validation /// All required fields must be set before planning var canPlanTrip: Bool { guard let mode = planningMode else { return false } // Common requirements for all modes guard hasSetRoutePreference && hasSetRepeatCities else { return false } switch mode { case .dateRange: return hasSetDates && !selectedSports.isEmpty && !selectedRegions.isEmpty case .gameFirst: return !selectedGameIds.isEmpty case .locations: return startLocation != nil && endLocation != nil && hasSetDates && !selectedSports.isEmpty case .followTeam: return selectedTeamId != nil && hasSetDates case .teamFirst: return teamFirstSport != nil && teamFirstSelectedTeamIds.count >= 2 } } /// Field validation for the review step - shows which fields are missing var fieldValidation: FieldValidation { FieldValidation( planningMode: planningMode, sports: selectedSports.isEmpty ? .missing : .valid, dates: hasSetDates ? .valid : .missing, regions: selectedRegions.isEmpty ? .missing : .valid, routePreference: hasSetRoutePreference ? .valid : .missing, repeatCities: hasSetRepeatCities ? .valid : .missing, selectedGames: selectedGameIds.isEmpty ? .missing : .valid, selectedTeam: selectedTeamId == nil ? .missing : .valid, startLocation: startLocation == nil ? .missing : .valid, endLocation: endLocation == nil ? .missing : .valid, teamFirstTeams: teamFirstSelectedTeamIds.count >= 2 ? .valid : .missing, teamFirstTeamCount: teamFirstSelectedTeamIds.count ) } // MARK: - Sport Availability func canSelectSport(_ sport: Sport) -> Bool { sportAvailability[sport] ?? true // Default to available if not checked } func fetchSportAvailability() async { guard hasSetDates else { return } isLoadingSportAvailability = true defer { isLoadingSportAvailability = false } var availability: [Sport: Bool] = [:] for sport in Sport.supported { do { let games = try await AppDataProvider.shared.filterGames( sports: [sport], startDate: startDate, endDate: endDate ) availability[sport] = !games.isEmpty } catch { availability[sport] = true // Default to available on error } } await MainActor.run { self.sportAvailability = availability } } // MARK: - Reset Logic private func resetAllSelections() { // Common fields selectedSports = [] hasSetDates = false selectedRegions = [] hasSetRoutePreference = false hasSetRepeatCities = false mustStopLocations = [] // gameFirst mode fields gamePickerSports = [] gamePickerTeamIds = [] selectedGameIds = [] // followTeam mode fields selectedTeamId = nil teamPickerSport = nil // locations mode fields startLocation = nil endLocation = nil // teamFirst mode fields teamFirstSport = nil teamFirstSelectedTeamIds = [] } } // MARK: - Field Validation struct FieldValidation { enum Status { case valid case missing } let planningMode: PlanningMode? // Common fields let sports: Status let dates: Status let regions: Status let routePreference: Status let repeatCities: Status // Mode-specific fields let selectedGames: Status let selectedTeam: Status let startLocation: Status let endLocation: Status let teamFirstTeams: Status let teamFirstTeamCount: Int /// Returns only the fields that are required for the current planning mode var requiredFields: [(name: String, status: Status)] { var fields: [(String, Status)] = [] guard let mode = planningMode else { return fields } switch mode { case .dateRange: fields = [ ("Dates", dates), ("Sports", sports), ("Regions", regions), ("Route Preference", routePreference), ("Repeat Cities", repeatCities) ] case .gameFirst: fields = [ ("Games", selectedGames), ("Route Preference", routePreference), ("Repeat Cities", repeatCities) ] case .locations: fields = [ ("Start Location", startLocation), ("End Location", endLocation), ("Dates", dates), ("Sports", sports), ("Route Preference", routePreference), ("Repeat Cities", repeatCities) ] case .followTeam: fields = [ ("Team", selectedTeam), ("Dates", dates), ("Route Preference", routePreference), ("Repeat Cities", repeatCities) ] case .teamFirst: fields = [ ("Teams", teamFirstTeams), ("Route Preference", routePreference), ("Repeat Cities", repeatCities) ] } return fields } /// Returns only the missing required fields for the current mode var missingFields: [String] { requiredFields.filter { $0.status == .missing }.map { $0.name } } /// Whether all required fields for the current mode are valid var allRequiredFieldsValid: Bool { requiredFields.allSatisfy { $0.status == .valid } } }