diff --git a/docs/plans/2026-01-11-bug-fixes-design.md b/docs/plans/2026-01-11-bug-fixes-design.md new file mode 100644 index 0000000..4a5761f --- /dev/null +++ b/docs/plans/2026-01-11-bug-fixes-design.md @@ -0,0 +1,395 @@ +# Bug Fixes Design Document + +**Date:** 2026-01-11 +**Status:** Ready for Implementation +**Priority:** High (ASAP) + +## Overview + +This document outlines fixes for 12 bugs affecting trip planning and UI functionality. Issues are grouped by category and prioritized by impact on core functionality. + +--- + +## Category A: Planning Mode Bugs (7 issues) + +These affect the main trip planning flow and should be fixed first. + +### Issue #1 & #6: Follow Team Start/End Location + +**Problem:** Follow Team mode uses a plain text field for home location input, while Must-Stop uses `LocationSearchSheet` with MKLocalSearch. The text field doesn't provide proper geocoding or location suggestions. + +**Root Cause:** `TripCreationView` uses different UI patterns for location input depending on context. + +**Files to Modify:** +- `SportsTime/Features/Trip/Views/TripCreationView.swift` +- `SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift` + +**Fix:** +1. Replace the text field in Follow Team mode with a button that opens `LocationSearchSheet` +2. On selection, set both `startLocation` (resolved LocationInput) and `startLocationText` (display name) +3. Preserve `startLocationText` when switching to `.followTeam` mode (currently cleared in `switchPlanningMode()`) + +**Implementation:** +```swift +// In Follow Team section, replace TextField with: +Button { + cityInputType = .homeLocation // Add new case + showCityInput = true +} label: { + HStack { + Text(viewModel.startLocationText.isEmpty ? "Set Home Location" : viewModel.startLocationText) + Spacer() + Image(systemName: "magnifyingglass") + } +} + +// In LocationSearchSheet callback: +case .homeLocation: + viewModel.startLocation = location + viewModel.startLocationText = location.name +``` + +--- + +### Issue #2: "By Game" Shows Date Range Error + +**Problem:** Selecting games in "By Game" mode shows "date range required" error even though the date range should be computed from selected games. + +**Root Cause:** `gameFirstDateRange` computed property may return nil if no games selected yet, but the error message appears prematurely. + +**Files to Modify:** +- `SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift` + +**Fix:** +1. In `planTrip()`, ensure `gameFirstDateRange` is computed before validation +2. Update `formValidationMessage` to show "Select at least one game" instead of date range error for gameFirst mode +3. Verify `effectiveStartDate`/`effectiveEndDate` are set from selected games before planning + +**Verification:** Lines 287-292 in `planTrip()` already handle this, but need to verify the error isn't coming from `ScenarioBPlanner`. + +--- + +### Issue #3: Date Range Not Showing Current Selection + +**Problem:** Date range picker doesn't display the currently selected start/end dates. + +**Files to Modify:** +- `SportsTime/Features/Trip/Views/TripCreationView.swift` (DateRangePicker) + +**Fix:** +1. Verify `DateRangePicker` initializes with `viewModel.startDate` and `viewModel.endDate` +2. Check that `selectionState` is set to `.complete` when both dates exist +3. Ensure visual highlighting shows the selected range on initial render + +--- + +### Issue #4 & #8: Must-Stop Finds Wrong Games / Shows Away Games + +**Problem:** +1. Must-stop locations are accepted but **never used** in the planning logic +2. When must-stop is used, it should only show games where the must-stop city is the **home team's city** + +**Root Cause:** `ScenarioAPlanner.swift` receives `mustStopLocations` via `PlanningRequest` but never filters games by it. + +**Files to Modify:** +- `SportsTime/Planning/Engine/ScenarioAPlanner.swift` +- `SportsTime/Planning/Models/PlanningModels.swift` (for config) + +**Fix:** +1. Add must-stop filtering after date/region filtering in Step 2 +2. Filter to games where the stadium's city matches the must-stop city AND it's a home game + +**Implementation:** +```swift +// In ScenarioAPlanner.plan(), after line 81: + +// Step 2b: Filter by must-stop locations (if any) +var gamesAfterMustStop = gamesInRange +if let mustStop = request.mustStopLocation { + gamesAfterMustStop = gamesInRange.filter { game in + guard let stadium = request.stadiums[game.stadiumId] else { return false } + // Must be in must-stop city AND be a home game (stadium is home team's stadium) + let cityMatches = stadium.city.lowercased() == mustStop.name.lowercased() + // Home games only: the game's stadium is in the must-stop city + return cityMatches + } + + // If must-stop filtering removed all games, fail with clear message + if gamesAfterMustStop.isEmpty { + return .failure( + PlanningFailure( + reason: .noGamesInRange, + violations: [ + ConstraintViolation( + type: .mustStop, + description: "No home games found in \(mustStop.name) during selected dates", + severity: .error + ) + ] + ) + ) + } +} +``` + +--- + +### Issue #5: Scenario Not Updating When Switched + +**Problem:** Switching planning modes may not update the UI correctly. + +**Root Cause:** `planningMode` property has no `didSet` observer to trigger dependent state updates. + +**Files to Modify:** +- `SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift` + +**Fix:** +1. Add `didSet` to `planningMode` property +2. Call `switchPlanningMode()` from the observer to ensure state is reset +3. Alternatively, verify UI is observing `planningMode` changes correctly (since `@Observable` should handle this) + +**Implementation:** +```swift +var planningMode: PlanningMode = .dateRange { + didSet { + if oldValue != planningMode { + // Reset mode-specific state + viewState = .editing + availableGames = [] + } + } +} +``` + +--- + +## Category B: UI/Sort/Filter Issues (4 issues) + +### Issue #7: Add "Most Cities" Sort to All Trips + +**Problem:** `SavedTripsListView` has no sort options; trips are shown by `updatedAt` only. + +**Files to Modify:** +- `SportsTime/Features/Home/Views/HomeView.swift` (SavedTripsListView) + +**Fix:** +1. Add `@State private var sortOption: TripSortOption = .date` +2. Add sort picker to toolbar +3. Sort trips based on selected option + +**Implementation:** +```swift +enum TripSortOption: String, CaseIterable { + case date = "Date" + case mostCities = "Most Cities" + case mostGames = "Most Games" +} + +struct SavedTripsListView: View { + let trips: [SavedTrip] + @State private var sortOption: TripSortOption = .date + + var sortedTrips: [SavedTrip] { + switch sortOption { + case .date: + return trips.sorted { $0.updatedAt > $1.updatedAt } + case .mostCities: + return trips.sorted { ($0.trip?.stops.count ?? 0) > ($1.trip?.stops.count ?? 0) } + case .mostGames: + return trips.sorted { ($0.trip?.totalGames ?? 0) > ($1.trip?.totalGames ?? 0) } + } + } + + var body: some View { + // ... use sortedTrips instead of trips + .toolbar { + ToolbarItem(placement: .primaryAction) { + Menu { + Picker("Sort", selection: $sortOption) { + ForEach(TripSortOption.allCases, id: \.self) { option in + Text(option.rawValue).tag(option) + } + } + } label: { + Image(systemName: "arrow.up.arrow.down") + } + } + } + } +} +``` + +--- + +### Issue #9: Coast-to-Coast Filter by Most Stops + +**Problem:** Coast-to-coast suggested trips should only show the top 2 trips with the most stops (C2C should be massive, epic trips). + +**Files to Modify:** +- `SportsTime/Features/Home/Views/HomeView.swift` (suggestedTripsSection) +- Or `SportsTime/Core/Services/SuggestedTripsGenerator.swift` + +**Fix:** +1. Filter coast-to-coast trips to only the top 2 by stop count +2. Sort by `stops.count` descending before taking top 2 + +**Implementation:** +```swift +// In suggestedTripsSection or SuggestedTripsGenerator: +var coastToCoastTrips: [SuggestedTrip] { + suggestedTripsGenerator.suggestedTrips + .filter { $0.type == .coastToCoast } + .sorted { $0.trip.stops.count > $1.trip.stops.count } + .prefix(2) + .map { $0 } // Convert ArraySlice to Array +} +``` + +--- + +### Issue #11: Maps Should Not Be Movable + +**Problem:** Maps on various screens can be panned/zoomed, but should be locked to show North America only. + +**Files to Modify:** +- `SportsTime/Features/Progress/Views/ProgressMapView.swift` +- Any other views with maps + +**Fix:** +1. Disable map interaction using `interactionModes: []` (iOS 17+) or `.allowsHitTesting(false)` +2. Set fixed region to show continental US + +**Implementation:** +```swift +// iOS 17+ approach: +Map(initialPosition: .region(usRegion), interactionModes: []) { + // annotations +} + +// Or disable hit testing: +Map(coordinateRegion: .constant(usRegion), annotationItems: stadiums) { ... } + .allowsHitTesting(false) + +// Fixed US region: +let usRegion = MKCoordinateRegion( + center: CLLocationCoordinate2D(latitude: 39.8283, longitude: -98.5795), + span: MKCoordinateSpan(latitudeDelta: 50, longitudeDelta: 60) +) +``` + +--- + +### Issue #12: Highlight Today in Schedule View + +**Problem:** Schedule view doesn't visually distinguish games happening today. + +**Files to Modify:** +- `SportsTime/Features/Schedule/Views/ScheduleListView.swift` + +**Fix:** +1. Add "TODAY" badge or background highlight for games on current date +2. Use `Calendar.current.isDateInToday()` to detect today's games + +**Implementation:** +```swift +struct GameRowView: View { + let game: RichGame + var showDate: Bool = false + + private var isToday: Bool { + Calendar.current.isDateInToday(game.game.dateTime) + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + if showDate { + HStack { + Text(formattedDate) + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + + if isToday { + Text("TODAY") + .font(.caption2) + .fontWeight(.bold) + .foregroundStyle(.white) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.orange) + .clipShape(Capsule()) + } + } + } + // ... rest of view + } + .listRowBackground(isToday ? Color.orange.opacity(0.1) : nil) + } +} +``` + +--- + +## Category C: Other Issues (1 issue) + +### Issue #10: Photo Import Loses Metadata + +**Problem:** Importing photos from iPhone doesn't preserve EXIF metadata. + +**Investigation Needed:** +1. Check current photo picker implementation (`PHPickerViewController` vs `UIImagePickerController`) +2. Verify if using `UIImage` compression strips EXIF +3. May need to use `PHAsset` directly to preserve metadata + +**Potential Fix:** +```swift +// Use PHPickerConfiguration with .current to get PHAsset +let config = PHPickerConfiguration(photoLibrary: .shared()) +config.selectionLimit = 1 +config.preferredAssetRepresentationMode = .current // Preserves original + +// Or request full asset data: +let result: PHPickerResult = ... +if let assetId = result.assetIdentifier { + let assets = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil) + // Work with PHAsset to preserve metadata +} +``` + +**Status:** Requires further investigation to identify exact implementation. + +--- + +## Implementation Priority + +1. **Critical (Core Planning):** + - #4 & #8: Must-stop filtering (completely broken) + - #1 & #6: Follow Team location search + - #2: By Game date range error + +2. **High (UX Impact):** + - #5: Scenario switching + - #3: Date range display + - #12: Today highlight + - #11: Lock maps + +3. **Medium (Enhancement):** + - #7: Sort options + - #9: C2C filter + +4. **Low (Separate Investigation):** + - #10: Photo metadata + +--- + +## Testing Checklist + +- [ ] Follow Team mode: Can search and select home location via MKLocalSearch +- [ ] By Game mode: No false "date range required" error +- [ ] Date range picker: Shows currently selected dates on open +- [ ] Must-stop with Chicago: Finds Cubs/White Sox HOME games only +- [ ] Switching modes: UI updates correctly, stale state cleared +- [ ] All Trips: Can sort by Most Cities, Most Games +- [ ] Coast-to-coast: Shows only top 2 trips by stop count +- [ ] Maps: Cannot pan or zoom, fixed to North America +- [ ] Schedule: Today's games have visual indicator