Refactor trip planning: DAG router + trip options UI + simplified itinerary
- Replace O(2^n) GeographicRouteExplorer with O(n) GameDAGRouter using DAG + beam search - Add geographic diversity to route selection (returns routes from distinct regions) - Add trip options selector UI (TripOptionsView, TripOptionCard) to choose between routes - Simplify itinerary display: separate games and travel segments by date - Remove complex ItineraryDay bundling, query games/travel directly per day - Update ScenarioA/B/C planners to use GameDAGRouter - Add new test suites for planners and travel estimator 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ final class TripCreationViewModel {
|
||||
enum ViewState: Equatable {
|
||||
case editing
|
||||
case planning
|
||||
case selectingOption([ItineraryOption]) // Multiple options to choose from
|
||||
case completed(Trip)
|
||||
case error(String)
|
||||
|
||||
@@ -23,6 +24,7 @@ final class TripCreationViewModel {
|
||||
switch (lhs, rhs) {
|
||||
case (.editing, .editing): return true
|
||||
case (.planning, .planning): return true
|
||||
case (.selectingOption(let o1), .selectingOption(let o2)): return o1.count == o2.count
|
||||
case (.completed(let t1), .completed(let t2)): return t1.id == t2.id
|
||||
case (.error(let e1), .error(let e2)): return e1 == e2
|
||||
default: return false
|
||||
@@ -88,6 +90,7 @@ final class TripCreationViewModel {
|
||||
private var teams: [UUID: Team] = [:]
|
||||
private var stadiums: [UUID: Stadium] = [:]
|
||||
private var games: [Game] = []
|
||||
private(set) var currentPreferences: TripPreferences?
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
@@ -300,13 +303,21 @@ final class TripCreationViewModel {
|
||||
|
||||
switch result {
|
||||
case .success(let options):
|
||||
guard let bestOption = options.first else {
|
||||
guard !options.isEmpty else {
|
||||
viewState = .error("No valid itinerary found")
|
||||
return
|
||||
}
|
||||
// Convert ItineraryOption to Trip
|
||||
let trip = convertToTrip(option: bestOption, preferences: preferences)
|
||||
viewState = .completed(trip)
|
||||
// Store preferences for later conversion
|
||||
currentPreferences = preferences
|
||||
|
||||
if options.count == 1 {
|
||||
// Only one option - go directly to detail
|
||||
let trip = convertToTrip(option: options[0], preferences: preferences)
|
||||
viewState = .completed(trip)
|
||||
} else {
|
||||
// Multiple options - show selection view
|
||||
viewState = .selectingOption(options)
|
||||
}
|
||||
|
||||
case .failure(let failure):
|
||||
viewState = .error(failureMessage(for: failure))
|
||||
@@ -423,6 +434,51 @@ final class TripCreationViewModel {
|
||||
preferredCities = []
|
||||
availableGames = []
|
||||
isLoadingGames = false
|
||||
currentPreferences = nil
|
||||
}
|
||||
|
||||
/// Select a specific itinerary option and navigate to its detail
|
||||
func selectOption(_ option: ItineraryOption) {
|
||||
guard let preferences = currentPreferences else {
|
||||
viewState = .error("Unable to load trip preferences")
|
||||
return
|
||||
}
|
||||
let trip = convertToTrip(option: option, preferences: preferences)
|
||||
viewState = .completed(trip)
|
||||
}
|
||||
|
||||
/// Convert an itinerary option to a Trip (public for use by TripOptionsView)
|
||||
func convertOptionToTrip(_ option: ItineraryOption) -> Trip {
|
||||
let preferences = currentPreferences ?? TripPreferences(
|
||||
planningMode: planningMode,
|
||||
startLocation: nil,
|
||||
endLocation: nil,
|
||||
sports: selectedSports,
|
||||
mustSeeGameIds: mustSeeGameIds,
|
||||
travelMode: travelMode,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
numberOfStops: useStopCount ? numberOfStops : nil,
|
||||
tripDuration: useStopCount ? nil : tripDurationDays,
|
||||
leisureLevel: leisureLevel,
|
||||
mustStopLocations: mustStopLocations,
|
||||
preferredCities: preferredCities,
|
||||
routePreference: routePreference,
|
||||
needsEVCharging: needsEVCharging,
|
||||
lodgingType: lodgingType,
|
||||
numberOfDrivers: numberOfDrivers,
|
||||
maxDrivingHoursPerDriver: maxDrivingHoursPerDriver,
|
||||
catchOtherSports: catchOtherSports
|
||||
)
|
||||
return convertToTrip(option: option, preferences: preferences)
|
||||
}
|
||||
|
||||
/// Go back to option selection from trip detail
|
||||
func backToOptions() {
|
||||
if case .completed = viewState {
|
||||
// We'd need to store options to go back - for now, restart planning
|
||||
viewState = .editing
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Conversion Helpers
|
||||
|
||||
Reference in New Issue
Block a user