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:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user