// // TripWizardView.swift // SportsTime // // Progressive-reveal 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 planningError: String? @State private var showError = false private let planningEngine = TripPlanningEngine() var body: some View { NavigationStack { ScrollViewReader { proxy in ScrollView { VStack(spacing: Theme.Spacing.lg) { // Step 1: Planning Mode (always visible) PlanningModeStep(selection: $viewModel.planningMode) .id("planningMode") // Step 2: Dates (after mode selected) if viewModel.isDatesStepVisible { DatesStep( startDate: $viewModel.startDate, endDate: $viewModel.endDate, hasSetDates: $viewModel.hasSetDates, onDatesChanged: { Task { await viewModel.fetchSportAvailability() } } ) .id("dates") .transition(.move(edge: .bottom).combined(with: .opacity)) } // Step 3: Sports (after dates set) if viewModel.isSportsStepVisible { SportsStep( selectedSports: $viewModel.selectedSports, sportAvailability: viewModel.sportAvailability, isLoading: viewModel.isLoadingSportAvailability, canSelectSport: viewModel.canSelectSport ) .id("sports") .transition(.move(edge: .bottom).combined(with: .opacity)) } // Step 4: Regions (after dates set) if viewModel.isRegionsStepVisible { RegionsStep(selectedRegions: $viewModel.selectedRegions) .id("regions") .transition(.move(edge: .bottom).combined(with: .opacity)) } // Step 5: Route Preference (after regions selected) if viewModel.isRoutePreferenceStepVisible { RoutePreferenceStep( routePreference: $viewModel.routePreference, hasSetRoutePreference: $viewModel.hasSetRoutePreference ) .id("routePreference") .transition(.move(edge: .bottom).combined(with: .opacity)) } // Step 6: Repeat Cities (after route preference) if viewModel.isRepeatCitiesStepVisible { RepeatCitiesStep( allowRepeatCities: $viewModel.allowRepeatCities, hasSetRepeatCities: $viewModel.hasSetRepeatCities ) .id("repeatCities") .transition(.move(edge: .bottom).combined(with: .opacity)) } // Step 7: Must Stops (after repeat cities) if viewModel.isMustStopsStepVisible { MustStopsStep(mustStopLocations: $viewModel.mustStopLocations) .id("mustStops") .transition(.move(edge: .bottom).combined(with: .opacity)) } // Step 8: Review (after must stops visible) if viewModel.isReviewStepVisible { 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, onPlan: { Task { await planTrip() } } ) .id("review") .transition(.move(edge: .bottom).combined(with: .opacity)) } } .padding(Theme.Spacing.md) .animation(.easeInOut(duration: 0.15), value: viewModel.revealState) } .onChange(of: viewModel.revealState) { _, newState in // Auto-scroll to newly revealed section after a delay // to avoid interrupting user interactions DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { // Only scroll if state hasn't changed (user stopped interacting) guard viewModel.revealState == newState else { return } withAnimation(.easeInOut(duration: 0.25)) { scrollToLatestStep(proxy: proxy) } } } } .themedBackground() .navigationTitle("Plan a Trip") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } .navigationDestination(isPresented: $showTripOptions) { TripOptionsView( options: tripOptions, games: [:], preferences: buildPreferences(), convertToTrip: { option in convertOptionToTrip(option) } ) } .alert("Planning Error", isPresented: $showError) { Button("OK") { showError = false } } message: { Text(planningError ?? "An unknown error occurred") } } } // MARK: - Auto-scroll private func scrollToLatestStep(proxy: ScrollViewProxy) { if viewModel.isReviewStepVisible { proxy.scrollTo("review", anchor: .top) } else if viewModel.isMustStopsStepVisible { proxy.scrollTo("mustStops", anchor: .top) } else if viewModel.isRepeatCitiesStepVisible { proxy.scrollTo("repeatCities", anchor: .top) } else if viewModel.isRoutePreferenceStepVisible { proxy.scrollTo("routePreference", anchor: .top) } else if viewModel.isRegionsStepVisible { proxy.scrollTo("regions", anchor: .top) } else if viewModel.isSportsStepVisible { proxy.scrollTo("sports", anchor: .top) } else if viewModel.isDatesStepVisible { proxy.scrollTo("dates", anchor: .top) } } // MARK: - Planning private func planTrip() async { viewModel.isPlanning = true defer { viewModel.isPlanning = false } do { let preferences = buildPreferences() // Fetch games for selected sports and date range let games = try await AppDataProvider.shared.filterGames( sports: preferences.sports, startDate: preferences.startDate, endDate: preferences.endDate ) // 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) }) // 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 showTripOptions = true } case .failure(let failure): planningError = failure.message showError = true } } catch { planningError = error.localizedDescription showError = true } } private func buildPreferences() -> TripPreferences { TripPreferences( planningMode: viewModel.planningMode ?? .dateRange, sports: viewModel.selectedSports, startDate: viewModel.startDate, endDate: viewModel.endDate, mustStopLocations: viewModel.mustStopLocations, routePreference: viewModel.routePreference, allowRepeatCities: viewModel.allowRepeatCities, selectedRegions: viewModel.selectedRegions ) } 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() }