feat(wizard): add mode-specific trip wizard inputs
- Add GamePickerStep with sheet-based Sport → Team → Game selection - Add TeamPickerStep with sheet-based Sport → Team selection - Add LocationsStep for start/end location selection with round trip toggle - Update TripWizardViewModel with mode-specific fields and validation - Update TripWizardView with conditional step rendering per mode - Update ReviewStep with mode-aware validation display - Fix gameFirst mode to derive date range from selected games Each planning mode now shows only relevant steps: - By Dates: Dates → Sports → Regions → Route → Repeat → Must Stops - By Games: Game Picker → Route → Repeat → Must Stops - By Route: Locations → Dates → Sports → Route → Repeat → Must Stops - Follow Team: Team Picker → Dates → Route → Repeat → Must Stops Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,12 @@ struct TripWizardView: View {
|
||||
|
||||
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 {
|
||||
@@ -29,26 +35,57 @@ struct TripWizardView: View {
|
||||
// All other steps appear together after planning mode selected
|
||||
if viewModel.areStepsVisible {
|
||||
Group {
|
||||
DatesStep(
|
||||
startDate: $viewModel.startDate,
|
||||
endDate: $viewModel.endDate,
|
||||
hasSetDates: $viewModel.hasSetDates,
|
||||
onDatesChanged: {
|
||||
Task {
|
||||
await viewModel.fetchSportAvailability()
|
||||
// Mode-specific steps
|
||||
if viewModel.showGamePickerStep {
|
||||
GamePickerStep(
|
||||
selectedSports: $viewModel.gamePickerSports,
|
||||
selectedTeamIds: $viewModel.gamePickerTeamIds,
|
||||
selectedGameIds: $viewModel.selectedGameIds
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
SportsStep(
|
||||
selectedSports: $viewModel.selectedSports,
|
||||
sportAvailability: viewModel.sportAvailability,
|
||||
isLoading: viewModel.isLoadingSportAvailability,
|
||||
canSelectSport: viewModel.canSelectSport
|
||||
)
|
||||
if viewModel.showSportsStep {
|
||||
SportsStep(
|
||||
selectedSports: $viewModel.selectedSports,
|
||||
sportAvailability: viewModel.sportAvailability,
|
||||
isLoading: viewModel.isLoadingSportAvailability,
|
||||
canSelectSport: viewModel.canSelectSport
|
||||
)
|
||||
}
|
||||
|
||||
RegionsStep(selectedRegions: $viewModel.selectedRegions)
|
||||
if viewModel.showRegionsStep {
|
||||
RegionsStep(selectedRegions: $viewModel.selectedRegions)
|
||||
}
|
||||
|
||||
// Always shown steps
|
||||
RoutePreferenceStep(
|
||||
routePreference: $viewModel.routePreference,
|
||||
hasSetRoutePreference: $viewModel.hasSetRoutePreference
|
||||
@@ -73,7 +110,11 @@ struct TripWizardView: View {
|
||||
isPlanning: viewModel.isPlanning,
|
||||
canPlanTrip: viewModel.canPlanTrip,
|
||||
fieldValidation: viewModel.fieldValidation,
|
||||
onPlan: { Task { await planTrip() } }
|
||||
onPlan: { Task { await planTrip() } },
|
||||
selectedGameCount: viewModel.selectedGameIds.count,
|
||||
selectedTeamName: selectedTeamName,
|
||||
startLocationName: viewModel.startLocation?.name,
|
||||
endLocationName: viewModel.endLocation?.name
|
||||
)
|
||||
}
|
||||
.transition(.opacity)
|
||||
@@ -115,19 +156,61 @@ struct TripWizardView: View {
|
||||
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
|
||||
)
|
||||
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, derive date range from selected games
|
||||
var games: [Game]
|
||||
if viewModel.planningMode == .gameFirst && !viewModel.selectedGameIds.isEmpty {
|
||||
// Fetch all games for the selected sports to find the must-see games
|
||||
let allGames = try await AppDataProvider.shared.allGames(for: preferences.sports)
|
||||
|
||||
// Find the selected must-see games
|
||||
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
|
||||
}
|
||||
|
||||
// Derive date range from must-see games (with buffer)
|
||||
let gameDates = mustSeeGames.map { $0.dateTime }
|
||||
let minDate = gameDates.min() ?? Date()
|
||||
let maxDate = gameDates.max() ?? Date()
|
||||
|
||||
// Update preferences with derived date range
|
||||
preferences = TripPreferences(
|
||||
planningMode: preferences.planningMode,
|
||||
startLocation: preferences.startLocation,
|
||||
endLocation: preferences.endLocation,
|
||||
sports: preferences.sports,
|
||||
mustSeeGameIds: preferences.mustSeeGameIds,
|
||||
startDate: Calendar.current.startOfDay(for: minDate),
|
||||
endDate: Calendar.current.date(byAdding: .day, value: 1, to: maxDate) ?? maxDate,
|
||||
mustStopLocations: preferences.mustStopLocations,
|
||||
routePreference: preferences.routePreference,
|
||||
allowRepeatCities: preferences.allowRepeatCities,
|
||||
selectedRegions: preferences.selectedRegions,
|
||||
followTeamId: preferences.followTeamId
|
||||
)
|
||||
|
||||
// Use all games within the derived date range
|
||||
games = allGames.filter {
|
||||
$0.dateTime >= preferences.startDate && $0.dateTime <= preferences.endDate
|
||||
}
|
||||
} 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 {
|
||||
@@ -171,15 +254,29 @@ struct TripWizardView: View {
|
||||
}
|
||||
|
||||
private func buildPreferences() -> TripPreferences {
|
||||
TripPreferences(
|
||||
// Determine which sports to use based on mode
|
||||
let sports: Set<Sport>
|
||||
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,
|
||||
sports: viewModel.selectedSports,
|
||||
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
|
||||
selectedRegions: viewModel.selectedRegions,
|
||||
followTeamId: viewModel.selectedTeamId
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user