Refactor travel segments and simplify trip options

Travel segment architecture:
- Remove departureTime/arrivalTime from TravelSegment (location-based, not date-based)
- Fix travel sections appearing after destination instead of between cities
- Fix missing travel segments when revisiting same city (consecutive grouping)
- Remove unwanted rest day at end of trip

Planning engine fixes:
- All three planners now group only consecutive games at same stadium
- Visiting A → B → A creates 3 stops with proper travel between

UI simplification:
- Remove redundant sort options (mostDriving/leastDriving, mostCities/leastCities)
- Remove unused "Find Other Sports Along Route" toggle (was dead code)

🤖 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 19:39:53 -06:00
parent 40a6f879e3
commit 4184af60b5
29 changed files with 140675 additions and 144310 deletions

View File

@@ -555,18 +555,6 @@ struct TripCreationView: View {
.tint(Theme.warmOrange)
}
// Other Sports
VStack(alignment: .leading, spacing: Theme.Spacing.xs) {
ThemedToggle(
label: "Find Other Sports Along Route",
isOn: $viewModel.catchOtherSports,
icon: "sportscourt"
)
Text("When enabled, we'll look for games from other sports happening along your route that fit your schedule.")
.font(.system(size: Theme.FontSize.micro))
.foregroundStyle(Theme.textMuted(colorScheme))
}
}
}
}
@@ -1037,6 +1025,28 @@ struct LocationSearchSheet: View {
// MARK: - Trip Options View
// MARK: - Sort Options
enum TripSortOption: String, CaseIterable, Identifiable {
case recommended = "Recommended"
case mostGames = "Most Games"
case leastGames = "Least Games"
case mostMiles = "Most Miles"
case leastMiles = "Least Miles"
case bestEfficiency = "Best Efficiency"
var id: String { rawValue }
var icon: String {
switch self {
case .recommended: return "star.fill"
case .mostGames, .leastGames: return "sportscourt"
case .mostMiles, .leastMiles: return "road.lanes"
case .bestEfficiency: return "gauge.with.dots.needle.33percent"
}
}
}
struct TripOptionsView: View {
let options: [ItineraryOption]
let games: [UUID: RichGame]
@@ -1045,8 +1055,31 @@ struct TripOptionsView: View {
@State private var selectedTrip: Trip?
@State private var showTripDetail = false
@State private var sortOption: TripSortOption = .recommended
@Environment(\.colorScheme) private var colorScheme
private var sortedOptions: [ItineraryOption] {
switch sortOption {
case .recommended:
return options
case .mostGames:
return options.sorted { $0.totalGames > $1.totalGames }
case .leastGames:
return options.sorted { $0.totalGames < $1.totalGames }
case .mostMiles:
return options.sorted { $0.totalDistanceMiles > $1.totalDistanceMiles }
case .leastMiles:
return options.sorted { $0.totalDistanceMiles < $1.totalDistanceMiles }
case .bestEfficiency:
// Games per driving hour (higher is better)
return options.sorted {
let effA = $0.totalDrivingHours > 0 ? Double($0.totalGames) / $0.totalDrivingHours : 0
let effB = $1.totalDrivingHours > 0 ? Double($1.totalGames) / $1.totalDrivingHours : 0
return effA > effB
}
}
}
var body: some View {
ScrollView {
LazyVStack(spacing: 20) {
@@ -1065,10 +1098,15 @@ struct TripOptionsView: View {
.foregroundStyle(Theme.textSecondary(colorScheme))
}
.padding(.top, Theme.Spacing.xl)
.padding(.bottom, Theme.Spacing.md)
.padding(.bottom, Theme.Spacing.sm)
// Options list with staggered animation
ForEach(Array(options.enumerated()), id: \.offset) { index, option in
// Sort picker
sortPicker
.padding(.horizontal, Theme.Spacing.md)
.padding(.bottom, Theme.Spacing.sm)
// Options list
ForEach(Array(sortedOptions.enumerated()), id: \.element.id) { index, option in
TripOptionCard(
option: option,
rank: index + 1,
@@ -1092,6 +1130,38 @@ struct TripOptionsView: View {
}
}
}
private var sortPicker: some View {
Menu {
ForEach(TripSortOption.allCases) { option in
Button {
withAnimation(.easeInOut(duration: 0.2)) {
sortOption = option
}
} label: {
Label(option.rawValue, systemImage: option.icon)
}
}
} label: {
HStack(spacing: 8) {
Image(systemName: sortOption.icon)
.font(.system(size: 14))
Text(sortOption.rawValue)
.font(.system(size: 14, weight: .medium))
Image(systemName: "chevron.down")
.font(.system(size: 12))
}
.foregroundStyle(Theme.textPrimary(colorScheme))
.padding(.horizontal, 16)
.padding(.vertical, 10)
.background(Theme.cardBackground(colorScheme))
.clipShape(Capsule())
.overlay(
Capsule()
.strokeBorder(Theme.textMuted(colorScheme).opacity(0.2), lineWidth: 1)
)
}
}
}
// MARK: - Trip Option Card
@@ -1524,7 +1594,7 @@ struct DateRangePicker: View {
private var daysOfWeekHeader: some View {
HStack(spacing: 0) {
ForEach(daysOfWeek, id: \.self) { day in
ForEach(Array(daysOfWeek.enumerated()), id: \.offset) { _, day in
Text(day)
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(Theme.textMuted(colorScheme))