// // 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 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: Sports (after mode selected) 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 3: Dates (after sport 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 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.3), value: viewModel.revealState) } .onChange(of: viewModel.revealState) { _, _ in // Auto-scroll to newly revealed section DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { withAnimation { 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: { _ in nil } ) } .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.isDatesStepVisible { proxy.scrollTo("dates", anchor: .top) } else if viewModel.isSportsStepVisible { proxy.scrollTo("sports", 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 planning request let request = PlanningRequest( preferences: preferences, availableGames: games, teams: AppDataProvider.shared.teams, stadiums: AppDataProvider.shared.stadiums ) // Run planning engine let result = await TripPlanningEngine.shared.plan(request: request) await MainActor.run { 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 { await MainActor.run { 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 ) } } // MARK: - Preview #Preview { TripWizardView() }