// // TripWizardView.swift // SportsTime // // Wizard for trip creation. // import SwiftUI struct TripWizardView: View { @Environment(\.dismiss) private var dismiss @Environment(\.colorScheme) private var colorScheme @State private var viewModel = TripWizardViewModel() @State private var showTripOptions = false @State private var tripOptions: [ItineraryOption] = [] @State private var gamesForDisplay: [String: RichGame] = [:] @State private var planningError: String? @State private var showError = false private let planningEngine = TripPlanningEngine() /// Selected team name for display in ReviewStep private var selectedTeamName: String? { guard let teamId = viewModel.selectedTeamId else { return nil } return AppDataProvider.shared.teams.first { $0.id == teamId }?.fullName } var body: some View { NavigationStack { ScrollView { VStack(spacing: Theme.Spacing.lg) { // Step 1: Planning Mode (always visible) PlanningModeStep(selection: $viewModel.planningMode) // All other steps appear together after planning mode selected if viewModel.areStepsVisible { Group { // Mode-specific steps if viewModel.showGamePickerStep { GamePickerStep( selectedSports: $viewModel.gamePickerSports, selectedTeamIds: $viewModel.gamePickerTeamIds, selectedGameIds: $viewModel.selectedGameIds, startDate: $viewModel.startDate, endDate: $viewModel.endDate ) } if viewModel.showTeamPickerStep { TeamPickerStep( selectedSport: $viewModel.teamPickerSport, selectedTeamId: $viewModel.selectedTeamId ) } if viewModel.showLocationsStep { LocationsStep( startLocation: $viewModel.startLocation, endLocation: $viewModel.endLocation ) } // Common steps (conditionally shown) if viewModel.showDatesStep { DatesStep( startDate: $viewModel.startDate, endDate: $viewModel.endDate, hasSetDates: $viewModel.hasSetDates, onDatesChanged: { Task { await viewModel.fetchSportAvailability() } } ) } if viewModel.showSportsStep { SportsStep( selectedSports: $viewModel.selectedSports, sportAvailability: viewModel.sportAvailability, isLoading: viewModel.isLoadingSportAvailability, canSelectSport: viewModel.canSelectSport ) } if viewModel.showRegionsStep { RegionsStep(selectedRegions: $viewModel.selectedRegions) } // Always shown steps RoutePreferenceStep( routePreference: $viewModel.routePreference, hasSetRoutePreference: $viewModel.hasSetRoutePreference ) RepeatCitiesStep( allowRepeatCities: $viewModel.allowRepeatCities, hasSetRepeatCities: $viewModel.hasSetRepeatCities ) MustStopsStep(mustStopLocations: $viewModel.mustStopLocations) ReviewStep( planningMode: viewModel.planningMode ?? .dateRange, selectedSports: viewModel.selectedSports, startDate: viewModel.startDate, endDate: viewModel.endDate, selectedRegions: viewModel.selectedRegions, routePreference: viewModel.routePreference, allowRepeatCities: viewModel.allowRepeatCities, mustStopLocations: viewModel.mustStopLocations, isPlanning: viewModel.isPlanning, canPlanTrip: viewModel.canPlanTrip, fieldValidation: viewModel.fieldValidation, onPlan: { Task { await planTrip() } }, selectedGameCount: viewModel.selectedGameIds.count, selectedTeamName: selectedTeamName, startLocationName: viewModel.startLocation?.name, endLocationName: viewModel.endLocation?.name ) } .transition(.opacity) } } .padding(Theme.Spacing.md) .animation(.easeInOut(duration: 0.2), value: viewModel.areStepsVisible) } .themedBackground() .navigationTitle("Plan a Trip") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } .navigationDestination(isPresented: $showTripOptions) { TripOptionsView( options: tripOptions, games: gamesForDisplay, preferences: buildPreferences(), convertToTrip: { option in convertOptionToTrip(option) } ) } .alert("Planning Error", isPresented: $showError) { Button("OK") { showError = false } } message: { Text(planningError ?? "An unknown error occurred") } } } // MARK: - Planning private func planTrip() async { viewModel.isPlanning = true defer { viewModel.isPlanning = false } do { var preferences = buildPreferences() // Build dictionaries from arrays let teamsById = Dictionary(uniqueKeysWithValues: AppDataProvider.shared.teams.map { ($0.id, $0) }) let stadiumsById = Dictionary(uniqueKeysWithValues: AppDataProvider.shared.stadiums.map { ($0.id, $0) }) // For gameFirst mode, use the UI-selected date range (set by GamePickerStep) // The date range is a 7-day span centered on the selected game(s) var games: [Game] if viewModel.planningMode == .gameFirst && !viewModel.selectedGameIds.isEmpty { // Fetch all games for the selected sports within the UI date range // GamePickerStep already set viewModel.startDate/endDate to a 7-day span let allGames = try await AppDataProvider.shared.allGames(for: preferences.sports) // Validate that selected must-see games exist let mustSeeGames = allGames.filter { viewModel.selectedGameIds.contains($0.id) } if mustSeeGames.isEmpty { planningError = "Could not find the selected games. Please try again." showError = true return } // Use the UI-selected date range (already set by GamePickerStep to 7-day span) // Filter all games within this range - ScenarioBPlanner will use anchor games // as required stops and add bonus games that fit geographically let rangeStart = Calendar.current.startOfDay(for: preferences.startDate) let rangeEnd = Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: preferences.endDate) ?? preferences.endDate games = allGames.filter { $0.dateTime >= rangeStart && $0.dateTime <= rangeEnd } } else { // Standard mode: fetch games for date range games = try await AppDataProvider.shared.filterGames( sports: preferences.sports, startDate: preferences.startDate, endDate: preferences.endDate ) } // Build RichGame dictionary for display var richGamesDict: [String: RichGame] = [:] for game in games { if let homeTeam = teamsById[game.homeTeamId], let awayTeam = teamsById[game.awayTeamId], let stadium = stadiumsById[game.stadiumId] { let richGame = RichGame(game: game, homeTeam: homeTeam, awayTeam: awayTeam, stadium: stadium) richGamesDict[game.id] = richGame } } // Build planning request let request = PlanningRequest( preferences: preferences, availableGames: games, teams: teamsById, stadiums: stadiumsById ) // Run planning engine let result = planningEngine.planItineraries(request: request) switch result { case .success(let options): if options.isEmpty { planningError = "No valid trip options found for your criteria. Try expanding your date range or regions." showError = true } else { tripOptions = options gamesForDisplay = richGamesDict showTripOptions = true } case .failure(let failure): planningError = failure.message showError = true } } catch { planningError = error.localizedDescription showError = true } } private func buildPreferences() -> TripPreferences { // Determine which sports to use based on mode let sports: Set if viewModel.planningMode == .gameFirst { sports = viewModel.gamePickerSports } else if viewModel.planningMode == .followTeam, let sport = viewModel.teamPickerSport { sports = [sport] } else { sports = viewModel.selectedSports } return TripPreferences( planningMode: viewModel.planningMode ?? .dateRange, startLocation: viewModel.startLocation, endLocation: viewModel.endLocation, sports: sports, mustSeeGameIds: viewModel.selectedGameIds, startDate: viewModel.startDate, endDate: viewModel.endDate, mustStopLocations: viewModel.mustStopLocations, routePreference: viewModel.routePreference, allowRepeatCities: viewModel.allowRepeatCities, selectedRegions: viewModel.selectedRegions, followTeamId: viewModel.selectedTeamId ) } private func convertOptionToTrip(_ option: ItineraryOption) -> Trip { let preferences = buildPreferences() // Convert ItineraryStops to TripStops let tripStops = option.stops.enumerated().map { index, stop in TripStop( stopNumber: index + 1, city: stop.city, state: stop.state, coordinate: stop.coordinate, arrivalDate: stop.arrivalDate, departureDate: stop.departureDate, games: stop.games, isRestDay: stop.games.isEmpty ) } return Trip( name: generateTripName(from: tripStops), preferences: preferences, stops: tripStops, travelSegments: option.travelSegments, totalGames: option.totalGames, totalDistanceMeters: option.totalDistanceMiles * 1609.34, totalDrivingSeconds: option.totalDrivingHours * 3600 ) } private func generateTripName(from stops: [TripStop]) -> String { let cities = stops.compactMap { $0.city }.prefix(3) if cities.count <= 1 { return cities.first ?? "Road Trip" } return cities.joined(separator: " → ") } } // MARK: - Preview #Preview { TripWizardView() }