feat: add Follow Team Mode (Scenario D) for road trip planning

Adds a new planning mode that lets users follow a team's schedule
(home + away games) and builds multi-city routes accordingly.

Key changes:
- New ScenarioDPlanner with team filtering and route generation
- Team picker UI with sport grouping and search
- Fix TravelEstimator 5-day limit (was 2-day) for cross-country routes
- Fix DateInterval end boundary to include games on last day
- Comprehensive test suite covering edge cases:
  - Multi-city routes with adequate/insufficient time
  - Optimal game selection per city for feasibility
  - 5-day driving segment limits
  - Multiple driver scenarios

Enables trips like Houston → Chicago → Anaheim following the Astros.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-11 12:42:43 -06:00
parent e7fb3cfbbe
commit f7faec01b1
9 changed files with 1744 additions and 8 deletions

View File

@@ -12,6 +12,7 @@ enum PlanningMode: String, Codable, CaseIterable, Identifiable {
case dateRange // Start date + end date, find games in range
case gameFirst // Pick games first, trip around those games
case locations // Start/end locations, optional games along route
case followTeam // Follow one team's schedule (home + away)
var id: String { rawValue }
@@ -20,6 +21,7 @@ enum PlanningMode: String, Codable, CaseIterable, Identifiable {
case .dateRange: return "By Dates"
case .gameFirst: return "By Games"
case .locations: return "By Route"
case .followTeam: return "Follow Team"
}
}
@@ -28,6 +30,7 @@ enum PlanningMode: String, Codable, CaseIterable, Identifiable {
case .dateRange: return "Shows a curated sample of possible routes — use filters to find your ideal trip"
case .gameFirst: return "Build trip around specific games"
case .locations: return "Plan route between locations"
case .followTeam: return "Follow your team on the road"
}
}
@@ -36,6 +39,7 @@ enum PlanningMode: String, Codable, CaseIterable, Identifiable {
case .dateRange: return "calendar"
case .gameFirst: return "sportscourt"
case .locations: return "map"
case .followTeam: return "person.3.fill"
}
}
}
@@ -230,6 +234,12 @@ struct TripPreferences: Codable, Hashable {
var allowRepeatCities: Bool
var selectedRegions: Set<Region>
/// Team to follow (for Follow Team mode)
var followTeamId: UUID?
/// Whether to start/end from a home location (vs fly-in/fly-out)
var useHomeLocation: Bool
init(
planningMode: PlanningMode = .dateRange,
startLocation: LocationInput? = nil,
@@ -250,7 +260,9 @@ struct TripPreferences: Codable, Hashable {
numberOfDrivers: Int = 1,
maxDrivingHoursPerDriver: Double? = nil,
allowRepeatCities: Bool = true,
selectedRegions: Set<Region> = [.east, .central, .west]
selectedRegions: Set<Region> = [.east, .central, .west],
followTeamId: UUID? = nil,
useHomeLocation: Bool = true
) {
self.planningMode = planningMode
self.startLocation = startLocation
@@ -272,6 +284,8 @@ struct TripPreferences: Codable, Hashable {
self.maxDrivingHoursPerDriver = maxDrivingHoursPerDriver
self.allowRepeatCities = allowRepeatCities
self.selectedRegions = selectedRegions
self.followTeamId = followTeamId
self.useHomeLocation = useHomeLocation
}
var totalDriverHoursPerDay: Double {