# Follow Team Mode Design ## Overview Add a 4th trip planning mode called "Follow Team" where users select a team, regions, and date range to build trips around that team's schedule (both home and away games). ## User Flow 1. Select "Follow Team" mode 2. Pick a team (grouped by sport for browsing) 3. Select regions (East/Central/West) 4. Pick date range 5. Choose whether to start/end from home location or fly-in/fly-out 6. Plan trip ## Scope **Included:** - Single team selection - Both home and away games - Region filtering (same as existing modes) - Date range filtering - Flexible start/end: home location OR first-game-to-last-game - Respect `allowRepeatCities` toggle **Not included (future):** - Multiple teams (rivalry trips) - Opponent filtering - Win/loss context display ## Data Model Changes ### PlanningMode Extension ```swift enum PlanningMode: String, Codable, CaseIterable, Identifiable { case dateRange // Scenario A - dates first case gameFirst // Scenario B - pick games case locations // Scenario C - start/end cities case followTeam // Scenario D - follow one team var displayName: String { switch self { case .dateRange: return "By Dates" case .gameFirst: return "By Games" case .locations: return "By Route" case .followTeam: return "Follow Team" } } var description: String { switch self { case .dateRange: return "Shows a curated sample of possible routes" case .gameFirst: return "Build trip around specific games" case .locations: return "Plan route between locations" case .followTeam: return "Follow your team on the road" } } var iconName: String { switch self { case .dateRange: return "calendar" case .gameFirst: return "sportscourt" case .locations: return "map" case .followTeam: return "person.3.fill" } } } ``` ### TripPreferences Addition ```swift struct TripPreferences { // ... existing properties ... /// 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 = true } ``` ### PlanningScenario Extension ```swift enum PlanningScenario: Equatable { case scenarioA // Date range case scenarioB // Selected games case scenarioC // Start/end locations case scenarioD // Follow team } ``` ## ScenarioDPlanner Logic ### Core Algorithm ```swift actor ScenarioDPlanner { func plan(request: PlanningRequest) async throws -> ItineraryResult { // 1. Filter games to selected team only let teamGames = filterToTeam( request.allGames, teamId: request.preferences.followTeamId ) // 2. Apply region filter let regionalGames = filterByRegion( teamGames, regions: request.preferences.selectedRegions, stadiums: request.stadiums ) // 3. Apply date range let dateFilteredGames = filterByDateRange( regionalGames, start: request.preferences.startDate, end: request.preferences.endDate ) // 4. Apply repeat city constraint let finalGames = applyRepeatCityFilter( dateFilteredGames, allowRepeat: request.preferences.allowRepeatCities, stadiums: request.stadiums ) // 5. Build route (with or without home location) if request.preferences.useHomeLocation, let startLocation = request.startLocation { // Round-trip from home return buildRouteFromHome(finalGames, start: startLocation) } else { // Fly-in / fly-out (first game to last game) return buildPointToPointRoute(finalGames) } } } ``` ### Team Game Filtering A game belongs to the followed team if they're home OR away: ```swift func filterToTeam(_ games: [Game], teamId: UUID?) -> [Game] { guard let teamId else { return [] } return games.filter { game in game.homeTeamId == teamId || game.awayTeamId == teamId } } ``` ### Repeat City Handling When `allowRepeatCities = false`, keep only one game per city: ```swift func applyRepeatCityFilter(_ games: [Game], allowRepeat: Bool, stadiums: [UUID: Stadium]) -> [Game] { guard !allowRepeat else { return games } var seenCities: Set = [] return games.filter { game in guard let stadium = stadiums[game.stadiumId] else { return false } if seenCities.contains(stadium.city) { return false } seenCities.insert(stadium.city) return true } } ``` ## UI Changes ### Mode Selector Expand to 4 modes in a 2x2 grid: ```swift var planningModeSection: some View { LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) { ForEach(PlanningMode.allCases) { mode in PlanningModeCard( mode: mode, isSelected: viewModel.planningMode == mode, action: { viewModel.planningMode = mode } ) } } } ``` ### Follow Team Mode Sections ```swift case .followTeam: teamPickerSection // Select team (grouped by sport) regionSection // East/Central/West datesSection // Start/end dates homeLocationToggle // "Start from home?" toggle locationSection // Conditional: Only if toggle is on ``` ### Team Picker Section ```swift var teamPickerSection: some View { Section("Select Team") { Picker("Team", selection: $viewModel.followTeamId) { Text("Choose a team").tag(nil as UUID?) ForEach(teamsByLeague) { league in Section(league.name) { ForEach(league.teams) { team in Text(team.name).tag(team.id as UUID?) } } } } } } ``` ### Home Location Toggle ```swift var homeLocationToggle: some View { Toggle("Start and end from home", isOn: $viewModel.useHomeLocation) .toggleStyle(.switch) } ``` ## Validation Rules ```swift var followTeamValidation: String? { guard viewModel.followTeamId != nil else { return "Select a team to follow" } guard viewModel.endDate > viewModel.startDate else { return "End date must be after start date" } if viewModel.useHomeLocation && viewModel.startLocation == nil { return "Enter your home location" } return nil } ``` ## Edge Cases | Case | Behavior | |------|----------| | No games in range | Show: "No [Team] games found in [Region] between [dates]" | | All games in one city | Valid single-stop trip | | Repeat city + toggle off | Keep first game in each city, skip duplicates | | Back-to-back different cities | Existing driving constraints handle feasibility | | No start location but toggle on | Block submission with validation message | ## Planning Failures Reuse existing `PlanningFailure` reasons: - `.noGamesInRange` — No team games in date/region - `.noValidRoutes` — Can't build feasible route - `.repeatCityViolation` — Would require repeat city but toggle is off ## Implementation Plan ### Files to Create ``` SportsTime/Planning/Engine/ScenarioDPlanner.swift SportsTimeTests/Planning/ScenarioDPlannerTests.swift ``` ### Files to Modify ``` SportsTime/Core/Models/Domain/TripPreferences.swift SportsTime/Planning/Models/PlanningModels.swift SportsTime/Planning/Engine/TripPlanningEngine.swift SportsTime/Features/Trip/Views/TripCreationView.swift SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift ``` ### Implementation Order 1. Data model — Add properties to `TripPreferences`, add `scenarioD` 2. Planner — Create `ScenarioDPlanner` 3. Engine routing — Update `TripPlanningEngine` to route to Scenario D 4. ViewModel — Add state and validation 5. UI — Add team picker, toggle, mode card 6. Tests — Cover all edge cases ### Test Cases - Happy path: Team with 5 games in region → valid route - No games: Team has no games in range → proper failure - Single city: All home games, repeat allowed → valid single-stop - Repeat city blocked: Multiple home games, repeat off → keeps one - With home location: Round-trip from user's city - Without home location: Point-to-point first to last game