docs: add bug fixes design for 12 planning and UI issues
Covers: - Planning mode bugs (follow team location, must-stop filtering, date range) - UI improvements (sort options, map locking, today highlight) - Coast-to-coast filter to show top 2 by stops Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
395
docs/plans/2026-01-11-bug-fixes-design.md
Normal file
395
docs/plans/2026-01-11-bug-fixes-design.md
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user