chore: remove scraper, add docs, add marketing-videos gitignore
- Remove Scripts/ directory (scraper no longer needed) - Add themed background documentation to CLAUDE.md - Add .gitignore for marketing-videos to prevent node_modules tracking Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
444
docs/CUSTOMER_FEEDBACK_PLAN.md
Normal file
444
docs/CUSTOMER_FEEDBACK_PLAN.md
Normal file
@@ -0,0 +1,444 @@
|
||||
# Customer Feedback Implementation Plan
|
||||
|
||||
**Created:** January 20, 2026
|
||||
**Updated:** January 20, 2026
|
||||
**Status:** In Progress (3 of 4 implemented)
|
||||
**Priority:** High (User-reported issues)
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document outlines the implementation plan for four customer-reported issues:
|
||||
|
||||
| # | Issue | Type | Priority | Status |
|
||||
|---|-------|------|----------|--------|
|
||||
| 1 | Game row selection area too small | UX Bug | P1 | ✅ **DONE** |
|
||||
| 2 | NWSL team names duplicated | Data Bug | P1 | ⏸️ Deferred (per user request) |
|
||||
| 3 | "By Games" mode needs date selection UI | Feature | P2 | ✅ **DONE** |
|
||||
| 4 | Schedule view missing NBA games | Investigation | P1 | ✅ **DONE** (debugging added) |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Critical Bug Fixes (P1)
|
||||
|
||||
### 1.1 Game Row Selection Area
|
||||
|
||||
**Problem:** Users report they cannot click anywhere on the row to select a game.
|
||||
|
||||
**Investigation Findings:**
|
||||
- File: `Features/Trip/Views/Wizard/Steps/GamePickerStep.swift`
|
||||
- Lines 397-512: `GamesPickerSheet`
|
||||
- Current implementation wraps content in `Button` with `.buttonStyle(.plain)`
|
||||
- The entire row SHOULD be tappable
|
||||
|
||||
**Root Cause Hypothesis:**
|
||||
The issue may be in a DIFFERENT view than `GamesPickerSheet`. Need to verify which screen users are referring to. Possible locations:
|
||||
1. `ScheduleListView.swift` - GameRowView is display-only (no selection)
|
||||
2. Game selection in other wizard steps
|
||||
3. Possibly hitting the checkbox icon area feels required
|
||||
|
||||
**Tasks:**
|
||||
- [ ] **Task 1.1.1:** Reproduce the issue - identify EXACT screen where selection fails
|
||||
- [ ] **Task 1.1.2:** If `GamesPickerSheet` - Add `.contentShape(Rectangle())` to ensure full row hit testing
|
||||
- [ ] **Task 1.1.3:** If `ScheduleListView` - This is intentionally display-only; clarify requirements
|
||||
- [ ] **Task 1.1.4:** Add visual tap feedback (highlight on press) for better UX
|
||||
- [ ] **Task 1.1.5:** Write regression test for row selection
|
||||
|
||||
**Proposed Fix (if GamesPickerSheet):**
|
||||
```swift
|
||||
Button {
|
||||
// toggle selection
|
||||
} label: {
|
||||
HStack {
|
||||
// ... content
|
||||
}
|
||||
.padding(.vertical, Theme.Spacing.xs)
|
||||
.contentShape(Rectangle()) // ← ADD THIS
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
```
|
||||
|
||||
**Files to Modify:**
|
||||
- `SportsTime/Features/Trip/Views/Wizard/Steps/GamePickerStep.swift`
|
||||
|
||||
**Estimated Effort:** 1-2 hours (including investigation)
|
||||
|
||||
---
|
||||
|
||||
### 1.2 NWSL Team Name Duplication
|
||||
|
||||
**Problem:** NWSL teams display as "Houston Houston Dash" instead of "Houston Dash".
|
||||
|
||||
**Investigation Findings:**
|
||||
- File: `Core/Models/Domain/Team.swift:59`
|
||||
- The `fullName` computed property: `city.isEmpty ? name : "\(city) \(name)"`
|
||||
- NWSL teams in JSON have:
|
||||
- `city: "Houston"`
|
||||
- `name: "Houston Dash"` (already includes city)
|
||||
- Result: "Houston" + " " + "Houston Dash" = "Houston Houston Dash"
|
||||
|
||||
**Affected Teams:**
|
||||
| Team | city | name | Current fullName |
|
||||
|------|------|------|------------------|
|
||||
| Houston Dash | Houston | Houston Dash | Houston Houston Dash |
|
||||
| Portland Thorns | Portland | Portland Thorns | Portland Portland Thorns |
|
||||
| Seattle Reign | Seattle | Seattle Reign | Seattle Seattle Reign |
|
||||
| Orlando Pride | Orlando | Orlando Pride | Orlando Orlando Pride |
|
||||
| Kansas City Current | Kansas City | Kansas City Current | Kansas City Kansas City Current |
|
||||
| North Carolina Courage | North Carolina | North Carolina Courage | North Carolina North Carolina Courage |
|
||||
| + other NWSL teams... | | | |
|
||||
|
||||
**Solution Options:**
|
||||
|
||||
**Option A: Fix Data (Recommended)**
|
||||
Update `teams_canonical.json` and regenerate from scripts:
|
||||
```json
|
||||
// BEFORE
|
||||
{ "city": "Houston", "name": "Houston Dash" }
|
||||
|
||||
// AFTER
|
||||
{ "city": "Houston", "name": "Dash" }
|
||||
```
|
||||
|
||||
Pros:
|
||||
- Clean separation of city and nickname
|
||||
- Consistent with MLB/NFL/NHL conventions
|
||||
- `fullName` logic remains simple
|
||||
|
||||
Cons:
|
||||
- Requires data regeneration
|
||||
- Need to verify all NWSL teams
|
||||
- Must update CloudKit canonical data
|
||||
|
||||
**Option B: Fix Code**
|
||||
Detect city prefix in `fullName` and avoid duplication:
|
||||
```swift
|
||||
var fullName: String {
|
||||
if city.isEmpty { return name }
|
||||
if name.hasPrefix(city) { return name } // ← ADD THIS
|
||||
return "\(city) \(name)"
|
||||
}
|
||||
```
|
||||
|
||||
Pros:
|
||||
- No data migration needed
|
||||
- Handles edge cases automatically
|
||||
|
||||
Cons:
|
||||
- Hides data inconsistency
|
||||
- Fragile (what about "New York City FC" vs "NYC"?)
|
||||
- Adds runtime overhead
|
||||
|
||||
**Selected Approach: Option A (Data Fix)**
|
||||
|
||||
**Tasks:**
|
||||
- [ ] **Task 1.2.1:** Update `Scripts/output/teams_nwsl.json` - change `name` to nickname only
|
||||
- [ ] **Task 1.2.2:** Run data pipeline to regenerate `teams_canonical.json`
|
||||
- [ ] **Task 1.2.3:** Verify all NWSL teams display correctly
|
||||
- [ ] **Task 1.2.4:** Update CloudKit canonical data via admin tool
|
||||
- [ ] **Task 1.2.5:** Write unit test to prevent regression: `test_NWSLTeamNames_DoNotDuplicateCity`
|
||||
|
||||
**Files to Modify:**
|
||||
- `Scripts/output/teams_nwsl.json`
|
||||
- `SportsTime/Resources/teams_canonical.json` (regenerated)
|
||||
- CloudKit `CanonicalTeam` records
|
||||
|
||||
**Estimated Effort:** 2-3 hours
|
||||
|
||||
---
|
||||
|
||||
### 1.3 Schedule View Missing Games Investigation
|
||||
|
||||
**Problem:** User reports NBA San Antonio @ Houston game (tonight) not showing in schedule.
|
||||
|
||||
**Investigation Findings:**
|
||||
|
||||
**Date Filter Analysis (TODAY = Jan 20, 2026):**
|
||||
- Default range: `Date()` to `Date() + 14 days`
|
||||
- If "tonight" means Jan 20, the game SHOULD show
|
||||
- Database contains: `game_nba_2025_20260120_sas_hou` dated 2026-01-20 06:00 UTC
|
||||
|
||||
**Potential Causes:**
|
||||
|
||||
| Cause | Likelihood | How to Verify |
|
||||
|-------|------------|---------------|
|
||||
| Timezone mismatch | High | Game stored as UTC, filter uses local time |
|
||||
| Data not loaded | Medium | Check `AppDataProvider.shared.allGames(for: [.nba])` count |
|
||||
| Sport filter unchecked | Medium | Verify NBA is in `selectedSports` |
|
||||
| Team/Stadium lookup fail | Low | Check `teamsById["team_nba_sas"]` exists |
|
||||
|
||||
**Most Likely Issue: Timezone Bug**
|
||||
|
||||
Game time: `2026-01-20T06:00:00Z` (6 AM UTC = midnight CST on Jan 20)
|
||||
|
||||
If user's device is in CST (UTC-6) and filter is:
|
||||
```swift
|
||||
game.dateTime >= Date() // Using local time
|
||||
```
|
||||
|
||||
At 7 PM CST on Jan 19, `Date()` = `2026-01-20T01:00:00Z`
|
||||
Game is at `2026-01-20T06:00:00Z`
|
||||
→ Game IS >= Date() → Should show
|
||||
|
||||
But if filter comparison isn't timezone-aware, edge cases occur.
|
||||
|
||||
**Tasks:**
|
||||
- [ ] **Task 1.3.1:** Add debug logging to `filterGames()` - log date range and game count
|
||||
- [ ] **Task 1.3.2:** Verify timezone handling in `CanonicalGame.dateTime` vs `Date()`
|
||||
- [ ] **Task 1.3.3:** Check if games are loading - add game count to Schedule header temporarily
|
||||
- [ ] **Task 1.3.4:** Verify NBA games exist in local SwiftData store
|
||||
- [ ] **Task 1.3.5:** If data issue - check `CanonicalSyncService` last sync status
|
||||
- [ ] **Task 1.3.6:** If client issue - fix timezone/filter logic
|
||||
- [ ] **Task 1.3.7:** Add diagnostic screen in Settings showing:
|
||||
- Total games per sport in database
|
||||
- Last sync timestamp
|
||||
- Date range being used for filtering
|
||||
|
||||
**Files to Investigate:**
|
||||
- `SportsTime/Features/Schedule/ViewModels/ScheduleViewModel.swift`
|
||||
- `SportsTime/Core/Services/DataProvider.swift`
|
||||
- `SportsTime/Core/Services/CanonicalSyncService.swift`
|
||||
|
||||
**Estimated Effort:** 2-4 hours (investigation + fix)
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Feature Enhancement (P2)
|
||||
|
||||
### 2.1 "By Games" Mode - Date Selection with Auto-Scroll
|
||||
|
||||
**Problem:** When selecting games in "By Games" routing mode, users want:
|
||||
1. A visible date selection calendar
|
||||
2. When selecting a game, calendar auto-scrolls to that game's date
|
||||
3. Selected game should be in the middle of a 7-day span (position 4 of 7)
|
||||
4. Multiple games should show range from earliest to latest
|
||||
|
||||
**Current Behavior:**
|
||||
- No calendar shown in `gameFirst` mode
|
||||
- Dates are auto-derived AFTER user finishes selecting games
|
||||
- Date range calculated: `min(gameDates)` to `max(gameDates) + 1 day`
|
||||
|
||||
**Proposed UX:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ BY GAMES MODE - STEP 2 │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────┐ │
|
||||
│ │ JANUARY 2026 │ │
|
||||
│ │ Su Mo Tu We Th Fr Sa │ │
|
||||
│ │ 1 2 3 4 │ │
|
||||
│ │ 5 6 7 8 9 10 11 │ │
|
||||
│ │ 12 13 14 15 16 17 18 │ │
|
||||
│ │ 19 20 [21][22][23][24][25] │ │ ← 7-day span
|
||||
│ │ 26 27 28 29 30 31 │ │ centered on
|
||||
│ └─────────────────────────────────────────┘ │ selected game
|
||||
│ │
|
||||
│ SELECTED GAMES (2): │
|
||||
│ ┌─────────────────────────────────────────┐ │
|
||||
│ │ 🏀 SAS @ HOU • Jan 22 • 7:00 PM ✕│ │ ← Game #1 (anchor)
|
||||
│ │ ⚾ STL @ HOU • Jan 25 • 1:10 PM ✕│ │ ← Game #2
|
||||
│ └─────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [+ Add More Games] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Behavior Rules:**
|
||||
|
||||
1. **Single Game Selected:**
|
||||
- Calendar centers on game date (position 4 of 7)
|
||||
- Date range: `gameDate - 3 days` to `gameDate + 3 days`
|
||||
- Visual: 7 consecutive days highlighted
|
||||
|
||||
2. **Multiple Games Selected:**
|
||||
- Date range: `min(gameDates) - 1 day` to `max(gameDates) + 1 day`
|
||||
- All game dates marked with sport-colored dots
|
||||
- Calendar shows earliest month containing games
|
||||
|
||||
3. **User Manually Adjusts Range:**
|
||||
- Allow expanding beyond auto-calculated range
|
||||
- Prevent shrinking below game dates (show error)
|
||||
|
||||
**Implementation Tasks:**
|
||||
|
||||
- [ ] **Task 2.1.1:** Add `DateRangePicker` to `GamePickerStep.swift` (below game summary)
|
||||
- [ ] **Task 2.1.2:** Create `@State var dateRange: ClosedRange<Date>` in ViewModel
|
||||
- [ ] **Task 2.1.3:** Implement `updateDateRangeForSelectedGames()`:
|
||||
```swift
|
||||
func updateDateRangeForSelectedGames() {
|
||||
guard !selectedGameIds.isEmpty else {
|
||||
dateRange = nil // Clear range when no games
|
||||
return
|
||||
}
|
||||
|
||||
let gameDates = selectedGames.map { $0.dateTime }.sorted()
|
||||
|
||||
if gameDates.count == 1 {
|
||||
// Single game: 7-day span centered on game
|
||||
let gameDate = gameDates[0]
|
||||
let start = Calendar.current.date(byAdding: .day, value: -3, to: gameDate)!
|
||||
let end = Calendar.current.date(byAdding: .day, value: 3, to: gameDate)!
|
||||
dateRange = start...end
|
||||
} else {
|
||||
// Multiple games: span from first to last with 1-day buffer
|
||||
let start = Calendar.current.date(byAdding: .day, value: -1, to: gameDates.first!)!
|
||||
let end = Calendar.current.date(byAdding: .day, value: 1, to: gameDates.last!)!
|
||||
dateRange = start...end
|
||||
}
|
||||
}
|
||||
```
|
||||
- [ ] **Task 2.1.4:** Modify `DateRangePicker` to accept game dates for marking:
|
||||
```swift
|
||||
var gameDates: Set<Date> // Dates with selected games
|
||||
```
|
||||
- [ ] **Task 2.1.5:** Add game date markers to calendar (sport-colored dots)
|
||||
- [ ] **Task 2.1.6:** Implement calendar auto-scroll to earliest game month
|
||||
- [ ] **Task 2.1.7:** Add `.onChange(of: selectedGameIds)` to trigger `updateDateRangeForSelectedGames()`
|
||||
- [ ] **Task 2.1.8:** Update `TripWizardView.planTrip()` to use the visible date range
|
||||
- [ ] **Task 2.1.9:** Write UI tests for auto-scroll behavior
|
||||
|
||||
**Files to Modify:**
|
||||
- `SportsTime/Features/Trip/Views/Wizard/Steps/GamePickerStep.swift`
|
||||
- `SportsTime/Features/Trip/Views/Wizard/Steps/DateRangePicker.swift`
|
||||
- `SportsTime/Features/Trip/ViewModels/TripWizardViewModel.swift`
|
||||
|
||||
**Estimated Effort:** 6-8 hours
|
||||
|
||||
---
|
||||
|
||||
## Implementation Schedule
|
||||
|
||||
### Week 1: Bug Fixes
|
||||
|
||||
| Day | Task | Owner |
|
||||
|-----|------|-------|
|
||||
| Day 1 | Task 1.3.1-1.3.4: Investigate schedule missing games | TBD |
|
||||
| Day 1 | Task 1.2.1-1.2.2: Fix NWSL team data | TBD |
|
||||
| Day 2 | Task 1.3.5-1.3.7: Complete schedule fix | TBD |
|
||||
| Day 2 | Task 1.2.3-1.2.5: Verify NWSL fix + tests | TBD |
|
||||
| Day 3 | Task 1.1.1-1.1.5: Fix row selection | TBD |
|
||||
| Day 3 | Integration testing all Phase 1 fixes | TBD |
|
||||
|
||||
### Week 2: Feature Enhancement
|
||||
|
||||
| Day | Task | Owner |
|
||||
|-----|------|-------|
|
||||
| Day 4-5 | Tasks 2.1.1-2.1.4: Date picker integration | TBD |
|
||||
| Day 6 | Tasks 2.1.5-2.1.7: Calendar markers + auto-scroll | TBD |
|
||||
| Day 7 | Tasks 2.1.8-2.1.9: Integration + UI tests | TBD |
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Phase 1 Tests
|
||||
|
||||
- [ ] **1.1:** Tap anywhere on game row → row selected
|
||||
- [ ] **1.1:** Selected row shows checkmark, deselected shows empty circle
|
||||
- [ ] **1.2:** Houston Dash displays as "Houston Dash" (not "Houston Houston Dash")
|
||||
- [ ] **1.2:** All NWSL teams display correctly
|
||||
- [ ] **1.3:** NBA games for today show in Schedule view
|
||||
- [ ] **1.3:** Games from different timezones show on correct local date
|
||||
|
||||
### Phase 2 Tests
|
||||
|
||||
- [ ] **2.1:** Select single game → calendar shows 7-day range centered on game
|
||||
- [ ] **2.1:** Select multiple games → calendar spans earliest to latest
|
||||
- [ ] **2.1:** Game dates have colored markers on calendar
|
||||
- [ ] **2.1:** Selecting game auto-scrolls calendar to that month
|
||||
- [ ] **2.1:** Cannot shrink date range to exclude selected games
|
||||
- [ ] **2.1:** Trip planning uses visible date range
|
||||
|
||||
---
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Probability | Impact | Mitigation |
|
||||
|------|-------------|--------|------------|
|
||||
| NWSL data fix breaks team lookups | Low | High | Test with full data load before deploy |
|
||||
| Schedule timezone fix affects other sports | Medium | Medium | Test with games from multiple timezones |
|
||||
| Date picker state conflicts with game selection | Low | Medium | Clear state isolation between components |
|
||||
| CloudKit sync overwrites data fix | Medium | High | Update CloudKit admin data simultaneously |
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. **Zero** instances of duplicated team names
|
||||
2. **All** today's NBA games visible in Schedule (when NBA filter enabled)
|
||||
3. **100%** row area tappable for game selection
|
||||
4. **Automatic** calendar updates when games selected in "By Games" mode
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes (January 20, 2026)
|
||||
|
||||
### Issue 1: Row Selection - COMPLETED
|
||||
|
||||
**Fix Applied:** Added `.contentShape(Rectangle())` to all picker sheet rows in `GamePickerStep.swift`:
|
||||
- `GamesPickerSheet` (line 468)
|
||||
- `SportsPickerSheet` (line 268)
|
||||
- `TeamsPickerSheet` (line 366)
|
||||
|
||||
This ensures the entire row area is tappable, not just the visible content.
|
||||
|
||||
### Issue 2: NWSL Names - DEFERRED
|
||||
|
||||
Per user request, this fix is deferred for later investigation.
|
||||
|
||||
### Issue 3: By Games Date Selection - COMPLETED
|
||||
|
||||
**Implementation:**
|
||||
- Added `startDate` and `endDate` bindings to `GamePickerStep`
|
||||
- Added `dateRangeSection` view that appears when games are selected
|
||||
- Added `updateDateRangeForSelectedGames()` function with auto-scroll logic:
|
||||
- **Single game:** 7-day span centered on game (position 4 of 7)
|
||||
- **Multiple games:** Range from earliest to latest with 1-day buffer
|
||||
- Added game date markers legend showing selected games by date
|
||||
- Updated `TripWizardView` to pass date bindings
|
||||
|
||||
**Files Modified:**
|
||||
- `GamePickerStep.swift` (new date bindings, date section, auto-update logic)
|
||||
- `TripWizardView.swift` (pass date bindings to GamePickerStep)
|
||||
|
||||
### Issue 4: Missing NBA Games - DEBUGGING ADDED
|
||||
|
||||
**Implementation:**
|
||||
- Added `os.log` logging to `ScheduleViewModel.loadGames()` with:
|
||||
- Query date range
|
||||
- Sports filter
|
||||
- Teams/stadiums loaded count
|
||||
- Games returned count by sport
|
||||
- Added `ScheduleDiagnostics` struct to capture query details
|
||||
- Added `ScheduleDiagnosticsSheet` accessible via toolbar menu in Schedule view
|
||||
- Shows: date range used, sports queried, data loaded counts, games by sport, troubleshooting steps
|
||||
|
||||
**Files Modified:**
|
||||
- `ScheduleViewModel.swift` (logger, diagnostics tracking)
|
||||
- `ScheduleListView.swift` (diagnostics menu item and sheet)
|
||||
|
||||
**How to Use:**
|
||||
1. Open Schedule tab
|
||||
2. Tap filter menu (three lines icon)
|
||||
3. Tap "Diagnostics"
|
||||
4. Review date range, loaded data counts, and games by sport
|
||||
|
||||
---
|
||||
|
||||
## Appendix: File Reference
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Features/Trip/Views/Wizard/Steps/GamePickerStep.swift` | Game selection UI |
|
||||
| `Features/Trip/Views/Wizard/Steps/DateRangePicker.swift` | Calendar component |
|
||||
| `Features/Trip/ViewModels/TripWizardViewModel.swift` | Trip wizard state |
|
||||
| `Features/Schedule/Views/ScheduleListView.swift` | Schedule display |
|
||||
| `Features/Schedule/ViewModels/ScheduleViewModel.swift` | Schedule filtering |
|
||||
| `Core/Models/Domain/Team.swift` | Team model with `fullName` |
|
||||
| `Core/Services/DataProvider.swift` | Data loading and filtering |
|
||||
| `Scripts/output/teams_nwsl.json` | NWSL team source data |
|
||||
| `Resources/teams_canonical.json` | Bundled team data |
|
||||
283
docs/stadiumGuide.md
Normal file
283
docs/stadiumGuide.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# Team-First Planning Mode Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
**Feature:** Select teams (not dates), find optimal trip windows across the entire season.
|
||||
|
||||
**Use Case:** User has 3 stadiums left on bucket list. Select those 3 teams, app finds all windows throughout the season where all 3 teams play at home within a reasonable trip duration.
|
||||
|
||||
## Requirements
|
||||
|
||||
| Constraint | Value |
|
||||
|------------|-------|
|
||||
| Sport scope | Single sport per search |
|
||||
| Start city | Flexible (algorithm picks optimal) |
|
||||
| Max duration | `teams × 2` days (3 teams = 6 days) |
|
||||
| Drive limit | Existing settings value |
|
||||
| Optimize for | Shortest duration + minimal backtracking |
|
||||
| Results | Top 10 options with reasoning |
|
||||
| Season | Current season only |
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Current Flow (Date-First)
|
||||
```
|
||||
User picks dates → Fetch games in range → Find routes → Rank → Display
|
||||
```
|
||||
|
||||
### New Flow (Team-First)
|
||||
```
|
||||
User picks teams → Fetch ALL home games for those teams (full season)
|
||||
→ Generate sliding windows (N-day chunks)
|
||||
→ Filter to windows where all teams have ≥1 home game
|
||||
→ For each valid window: find optimal routes
|
||||
→ Rank by duration + miles → Return top 10
|
||||
```
|
||||
|
||||
### Reuse Ratio: ~90%
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| `allGames(for sports:)` | ✅ Exists | Full season fetch |
|
||||
| Sliding window generator | ✅ Exists | In ScenarioBPlanner |
|
||||
| GameDAGRouter | ✅ Exists | Supports anchor constraints |
|
||||
| ItineraryBuilder | ✅ Exists | No changes needed |
|
||||
| Max driving time | ✅ Exists | `UserPreferences.maxDrivingHoursPerDriver` |
|
||||
| Multi-team selection UI | ❌ New | Currently single-team only |
|
||||
| Team-first planner | ❌ New | ~300 lines adapting Scenario B |
|
||||
| Window optimization | ⚠️ Adapt | Season = 180+ days → need sampling |
|
||||
|
||||
---
|
||||
|
||||
## Parallel Track Structure
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ PHASE 1 (Parallel) │
|
||||
├──────────────────────┬──────────────────────┬──────────────────────────────┤
|
||||
│ Track A: Models │ Track B: Planner │ Track C: UI Components │
|
||||
│ (Agent 1) │ (Agent 2) │ (Agent 3) │
|
||||
├──────────────────────┼──────────────────────┼──────────────────────────────┤
|
||||
│ A1. Add teamFirst │ B1. Create │ C1. Create TeamPickerView │
|
||||
│ to PlanningMode │ ScenarioEPlanner │ (multi-select grid) │
|
||||
│ │ skeleton │ │
|
||||
│ A2. Add │ B2. Implement │ C2. Create │
|
||||
│ selectedTeamIds │ window generator │ TeamFirstWizardStep │
|
||||
│ to TripPrefs │ for teams │ │
|
||||
│ │ │ │
|
||||
│ A3. Update │ B3. Implement route │ C3. Update WizardViewModel │
|
||||
│ PlanningRequest │ finding with │ for teamFirst mode │
|
||||
│ computed props │ team anchors │ │
|
||||
└──────────────────────┴──────────────────────┴──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ PHASE 2 (Parallel) │
|
||||
├─────────────────────────────────┬───────────────────────────────────────────┤
|
||||
│ Track D: Integration │ Track E: Tests │
|
||||
│ (Agent 4) │ (Agent 5) │
|
||||
├─────────────────────────────────┼───────────────────────────────────────────┤
|
||||
│ D1. Update ScenarioPlannerFactory│ E1. Unit tests for window generator │
|
||||
│ to detect teamFirst mode │ │
|
||||
│ │ E2. Unit tests for ScenarioEPlanner │
|
||||
│ D2. Wire TeamPickerView into │ │
|
||||
│ existing wizard flow │ E3. Integration test: 3 teams → routes │
|
||||
│ │ │
|
||||
│ D3. Add teamFirst option to │ E4. Edge case tests (no windows, etc.) │
|
||||
│ planning mode selector │ │
|
||||
└─────────────────────────────────┴───────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ PHASE 3 (Sequential) │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ F1. End-to-end testing & bug fixes │
|
||||
│ F2. Performance optimization (if needed) │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Detailed Task Specifications
|
||||
|
||||
### Track A: Data Models (Agent 1)
|
||||
|
||||
**A1. Add `teamFirst` to PlanningMode**
|
||||
```
|
||||
File: SportsTime/Core/Models/Domain/TripPreferences.swift
|
||||
Action: Add case to PlanningMode enum
|
||||
```
|
||||
|
||||
**A2. Add `selectedTeamIds` to TripPreferences**
|
||||
```
|
||||
File: SportsTime/Core/Models/Domain/TripPreferences.swift
|
||||
Action: Add `var selectedTeamIds: Set<String> = []`
|
||||
Action: Add `var teamFirstMaxDays: Int` computed as `selectedTeamIds.count * 2`
|
||||
```
|
||||
|
||||
**A3. Update PlanningRequest computed properties**
|
||||
```
|
||||
File: SportsTime/Planning/Models/PlanningModels.swift
|
||||
Action: Add computed property to extract teams for selected IDs
|
||||
Action: Add helper to get all home games for selected teams
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Track B: Scenario Planner (Agent 2)
|
||||
|
||||
**B1. Create ScenarioEPlanner skeleton**
|
||||
```
|
||||
File: SportsTime/Planning/Scenarios/ScenarioEPlanner.swift (NEW)
|
||||
Action: Create class conforming to ScenarioPlanner protocol
|
||||
Action: Implement required methods with TODOs
|
||||
```
|
||||
|
||||
**B2. Implement window generator for teams**
|
||||
```
|
||||
File: SportsTime/Planning/Scenarios/ScenarioEPlanner.swift
|
||||
Action: Generate all N-day windows across season
|
||||
Action: Filter to windows where ALL selected teams have ≥1 home game
|
||||
Action: Cap at 50 windows (sample if more)
|
||||
```
|
||||
|
||||
**B3. Implement route finding with team anchors**
|
||||
```
|
||||
File: SportsTime/Planning/Scenarios/ScenarioEPlanner.swift
|
||||
Action: For each valid window, collect all home games for selected teams
|
||||
Action: Pass to GameDAGRouter with those games as anchors
|
||||
Action: Build itineraries, rank by duration + miles
|
||||
Action: Return top 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Track C: UI Components (Agent 3)
|
||||
|
||||
**C1. Create TeamPickerView**
|
||||
```
|
||||
File: SportsTime/Features/Trip/Views/TeamPickerView.swift (NEW)
|
||||
Action: Grid of team logos/names for selected sport
|
||||
Action: Multi-select with checkmarks
|
||||
Action: Show count badge "3 selected"
|
||||
Action: Binding to Set<String> for team IDs
|
||||
```
|
||||
|
||||
**C2. Create TeamFirstWizardStep**
|
||||
```
|
||||
File: SportsTime/Features/Trip/Views/Wizard/TeamFirstWizardStep.swift (NEW)
|
||||
Action: Wrapper that uses TeamPickerView
|
||||
Action: Validation: require ≥2 teams selected
|
||||
Action: "Find trips for these teams" CTA
|
||||
```
|
||||
|
||||
**C3. Update WizardViewModel for teamFirst mode**
|
||||
```
|
||||
File: SportsTime/Features/Trip/ViewModels/TripWizardViewModel.swift
|
||||
Action: Add teamFirst step configuration
|
||||
Action: Handle selectedTeamIds state
|
||||
Action: Skip date selection step when in teamFirst mode
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Track D: Integration (Agent 4)
|
||||
|
||||
**D1. Update ScenarioPlannerFactory**
|
||||
```
|
||||
File: SportsTime/Planning/Scenarios/ScenarioPlannerFactory.swift
|
||||
Action: Add detection for teamFirst mode
|
||||
Action: Return ScenarioEPlanner when selectedTeamIds.count >= 2
|
||||
```
|
||||
|
||||
**D2. Wire TeamPickerView into wizard**
|
||||
```
|
||||
File: SportsTime/Features/Trip/Views/Wizard/TripWizardView.swift
|
||||
Action: Add case for teamFirst step
|
||||
Action: Navigation flow: Sport → Teams → (skip dates) → Results
|
||||
```
|
||||
|
||||
**D3. Add teamFirst to mode selector**
|
||||
```
|
||||
File: SportsTime/Features/Trip/Views/Wizard/PlanningModeSelector.swift
|
||||
Action: Add "Teams First" option with icon
|
||||
Action: Description: "Pick teams, we'll find the best windows"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Track E: Tests (Agent 5)
|
||||
|
||||
**E1. Unit tests for window generator**
|
||||
```
|
||||
File: SportsTimeTests/Planning/ScenarioEPlannerTests.swift (NEW)
|
||||
Tests:
|
||||
- 3 teams, 6-day window → finds valid windows
|
||||
- Window with only 2 of 3 teams → excluded
|
||||
- Empty season → returns empty
|
||||
- Sampling works when >50 windows
|
||||
```
|
||||
|
||||
**E2. Unit tests for ScenarioEPlanner**
|
||||
```
|
||||
File: SportsTimeTests/Planning/ScenarioEPlannerTests.swift
|
||||
Tests:
|
||||
- Returns PlanningResult with routes
|
||||
- All routes include all selected teams
|
||||
- Routes sorted by duration ascending
|
||||
- Respects max driving time constraint
|
||||
```
|
||||
|
||||
**E3. Integration test**
|
||||
```
|
||||
File: SportsTimeTests/Planning/TeamFirstIntegrationTests.swift (NEW)
|
||||
Tests:
|
||||
- Full flow: 3 MLB teams → top 10 routes
|
||||
- Each route visits all 3 stadiums
|
||||
- Total duration ≤ 6 days
|
||||
```
|
||||
|
||||
**E4. Edge case tests**
|
||||
```
|
||||
File: SportsTimeTests/Planning/ScenarioEPlannerTests.swift
|
||||
Tests:
|
||||
- Teams with no overlapping games → graceful error
|
||||
- Single team selected → validation error
|
||||
- Teams in same city → treated as separate stops
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Risks & Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Season = 180+ days = too many windows | Sample every 2nd day, or limit to weekends |
|
||||
| No valid windows (teams never align) | Early validation + user feedback |
|
||||
| Slow computation (10+ windows × routing) | Cap at 50 windows, parallelize |
|
||||
| Teams in same city (e.g., 2 NYC teams) | Treat as separate stops (different stadiums) |
|
||||
|
||||
---
|
||||
|
||||
## Test Strategy
|
||||
|
||||
1. **Unit:** Window generator finds correct multi-team windows
|
||||
2. **Unit:** Anchor constraints include all teams' home games
|
||||
3. **Integration:** 3 teams → returns routes visiting all 3
|
||||
4. **Edge:** Teams with non-overlapping seasons → graceful "no trips found"
|
||||
5. **Perf:** Full MLB season (2,430 games) completes in <5s
|
||||
|
||||
---
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
| Track | New Code | Modified Code | Total |
|
||||
|-------|----------|---------------|-------|
|
||||
| A: Models | 50 lines | 20 lines | 70 lines |
|
||||
| B: Planner | 300 lines | 0 lines | 300 lines |
|
||||
| C: UI | 250 lines | 50 lines | 300 lines |
|
||||
| D: Integration | 30 lines | 80 lines | 110 lines |
|
||||
| E: Tests | 400 lines | 0 lines | 400 lines |
|
||||
| **Total** | **1,030 lines** | **150 lines** | **1,180 lines** |
|
||||
Reference in New Issue
Block a user