Redesign trip option cards and fix various UI/planning issues

TripOptionCard improvements:
- Replace horizontal route with vertical layout (start → end with arrow)
- Remove rank badges (1, 2, 3, etc.)
- Split stats into two rows: cities/miles and sports with game counts
- Clear selection when navigating back from detail view

Settings cleanup:
- Remove unused settings (preferred game time, playoff games, notifications)
- Convert remaining settings to sliders

Planning fixes:
- Fix multi-day driving calculation in canTransition
- Remove over-restrictive trip rejection in TravelEstimator
- Clear games cache when sport selection changes

UI polish:
- RoutePreviewStrip shows all cities (abbreviated)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-07 21:05:25 -06:00
parent 4184af60b5
commit 5bbfd30a70
12 changed files with 230 additions and 208 deletions

View File

@@ -176,6 +176,62 @@ struct ItineraryOption: Identifiable {
var totalGames: Int {
stops.reduce(0) { $0 + $1.games.count }
}
/// Sorts and ranks itinerary options based on leisure level preference.
///
/// - Parameters:
/// - options: The itinerary options to sort
/// - leisureLevel: The user's leisure preference
/// - limit: Maximum number of options to return (default 10)
/// - Returns: Sorted and ranked options
///
/// Sorting behavior:
/// - Packed: Most games first, then least driving
/// - Moderate: Best efficiency (games per driving hour)
/// - Relaxed: Least driving first, then fewer games
static func sortByLeisure(
_ options: [ItineraryOption],
leisureLevel: LeisureLevel,
limit: Int = 10
) -> [ItineraryOption] {
let sorted = options.sorted { a, b in
let aGames = a.totalGames
let bGames = b.totalGames
switch leisureLevel {
case .packed:
// Most games first, then least driving
if aGames != bGames { return aGames > bGames }
return a.totalDrivingHours < b.totalDrivingHours
case .moderate:
// Best efficiency (games per driving hour)
let effA = a.totalDrivingHours > 0 ? Double(aGames) / a.totalDrivingHours : Double(aGames)
let effB = b.totalDrivingHours > 0 ? Double(bGames) / b.totalDrivingHours : Double(bGames)
if effA != effB { return effA > effB }
return aGames > bGames
case .relaxed:
// Least driving first, then fewer games is fine
if a.totalDrivingHours != b.totalDrivingHours {
return a.totalDrivingHours < b.totalDrivingHours
}
return aGames < bGames
}
}
// Re-rank after sorting
return Array(sorted.prefix(limit)).enumerated().map { index, option in
ItineraryOption(
rank: index + 1,
stops: option.stops,
travelSegments: option.travelSegments,
totalDrivingHours: option.totalDrivingHours,
totalDistanceMiles: option.totalDistanceMiles,
geographicRationale: option.geographicRationale
)
}
}
}
// MARK: - Itinerary Stop