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:
Trey t
2026-01-14 22:21:57 -06:00
parent 1301442604
commit aa34c6585a
7 changed files with 1277 additions and 63 deletions

View File

@@ -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
)
}