diff --git a/SportsTime/Core/Models/Domain/TripPreferences.swift b/SportsTime/Core/Models/Domain/TripPreferences.swift index d0a717c..7bcd354 100644 --- a/SportsTime/Core/Models/Domain/TripPreferences.swift +++ b/SportsTime/Core/Models/Domain/TripPreferences.swift @@ -240,6 +240,9 @@ struct TripPreferences: Codable, Hashable { /// Whether to start/end from a home location (vs fly-in/fly-out) var useHomeLocation: Bool + /// Trip duration for gameFirst mode sliding windows (number of days) + var gameFirstTripDuration: Int + init( planningMode: PlanningMode = .dateRange, startLocation: LocationInput? = nil, @@ -262,7 +265,8 @@ struct TripPreferences: Codable, Hashable { allowRepeatCities: Bool = true, selectedRegions: Set = [.east, .central, .west], followTeamId: UUID? = nil, - useHomeLocation: Bool = true + useHomeLocation: Bool = true, + gameFirstTripDuration: Int = 7 ) { self.planningMode = planningMode self.startLocation = startLocation @@ -286,6 +290,7 @@ struct TripPreferences: Codable, Hashable { self.selectedRegions = selectedRegions self.followTeamId = followTeamId self.useHomeLocation = useHomeLocation + self.gameFirstTripDuration = gameFirstTripDuration } var totalDriverHoursPerDay: Double { diff --git a/SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift b/SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift index 4685c25..75eda2f 100644 --- a/SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift +++ b/SportsTime/Features/Trip/ViewModels/TripCreationViewModel.swift @@ -137,6 +137,9 @@ final class TripCreationViewModel { var followTeamId: UUID? var useHomeLocation: Bool = true + // Game First Mode - Trip duration for sliding windows + var gameFirstTripDuration: Int = 7 + // MARK: - Dependencies private let planningEngine = TripPlanningEngine() @@ -407,7 +410,8 @@ final class TripCreationViewModel { allowRepeatCities: allowRepeatCities, selectedRegions: selectedRegions, followTeamId: followTeamId, - useHomeLocation: useHomeLocation + useHomeLocation: useHomeLocation, + gameFirstTripDuration: gameFirstTripDuration ) // Build planning request @@ -589,7 +593,10 @@ final class TripCreationViewModel { numberOfDrivers: numberOfDrivers, maxDrivingHoursPerDriver: maxDrivingHoursPerDriver, allowRepeatCities: allowRepeatCities, - selectedRegions: selectedRegions + selectedRegions: selectedRegions, + followTeamId: followTeamId, + useHomeLocation: useHomeLocation, + gameFirstTripDuration: gameFirstTripDuration ) return convertToTrip(option: option, preferences: preferences) } diff --git a/SportsTime/Features/Trip/Views/TripCreationView.swift b/SportsTime/Features/Trip/Views/TripCreationView.swift index cb294c9..f7bab58 100644 --- a/SportsTime/Features/Trip/Views/TripCreationView.swift +++ b/SportsTime/Features/Trip/Views/TripCreationView.swift @@ -66,9 +66,10 @@ struct TripCreationView: View { datesSection case .gameFirst: - // Sports + Game Picker + // Sports + Game Picker + Trip Duration sportsSection gameBrowserSection + tripDurationSection case .locations: // Locations + Sports + optional games @@ -512,6 +513,39 @@ struct TripCreationView: View { } } + private var tripDurationSection: some View { + ThemedSection(title: "Trip Duration") { + VStack(alignment: .leading, spacing: Theme.Spacing.sm) { + HStack { + Image(systemName: "calendar.badge.clock") + .foregroundStyle(Theme.warmOrange) + Text("Days") + .font(.body) + .foregroundStyle(Theme.textPrimary(colorScheme)) + + Spacer() + + Stepper( + value: $viewModel.gameFirstTripDuration, + in: 2...21, + step: 1 + ) { + Text("\(viewModel.gameFirstTripDuration) days") + .font(.body.monospacedDigit()) + .foregroundStyle(Theme.textPrimary(colorScheme)) + } + } + .padding(Theme.Spacing.md) + .background(Theme.cardBackgroundElevated(colorScheme)) + .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) + + Text("We'll find all possible \(viewModel.gameFirstTripDuration)-day trips that include your selected games") + .font(.caption) + .foregroundStyle(Theme.textSecondary(colorScheme)) + } + } + } + private var sportsSection: some View { ThemedSection(title: "Sports") { SportSelectorGrid( @@ -1406,6 +1440,7 @@ struct LocationSearchSheet: View { enum TripSortOption: String, CaseIterable, Identifiable { case recommended = "Recommended" + case mostCities = "Most Cities" case mostGames = "Most Games" case leastGames = "Least Games" case mostMiles = "Most Miles" @@ -1417,6 +1452,7 @@ enum TripSortOption: String, CaseIterable, Identifiable { var icon: String { switch self { case .recommended: return "star.fill" + case .mostCities: return "mappin.and.ellipse" case .mostGames, .leastGames: return "sportscourt" case .mostMiles, .leastMiles: return "road.lanes" case .bestEfficiency: return "gauge.with.dots.needle.33percent" @@ -1514,6 +1550,8 @@ struct TripOptionsView: View { switch sortOption { case .recommended: return filtered + case .mostCities: + return filtered.sorted { $0.stops.count > $1.stops.count } case .mostGames: return filtered.sorted { $0.totalGames > $1.totalGames } case .leastGames: diff --git a/SportsTime/Planning/Engine/ScenarioBPlanner.swift b/SportsTime/Planning/Engine/ScenarioBPlanner.swift index 05659a3..460e85d 100644 --- a/SportsTime/Planning/Engine/ScenarioBPlanner.swift +++ b/SportsTime/Planning/Engine/ScenarioBPlanner.swift @@ -221,13 +221,19 @@ final class ScenarioBPlanner: ScenarioPlanner { request: PlanningRequest ) -> [DateInterval] { - // If explicit date range exists, use it - if let dateRange = request.dateRange { + // For gameFirst mode, ALWAYS use sliding windows with gameFirstTripDuration + // This generates all possible N-day windows containing the selected games + let isGameFirstMode = request.preferences.planningMode == .gameFirst + let duration = isGameFirstMode + ? request.preferences.gameFirstTripDuration + : request.preferences.effectiveTripDuration + + // If not gameFirst and explicit date range exists, use it directly + if !isGameFirstMode, let dateRange = request.dateRange { return [dateRange] } - // Otherwise, use trip duration to create sliding windows - let duration = request.preferences.effectiveTripDuration + // Use trip duration to create sliding windows guard duration > 0 else { return [] } // Find the span of selected games