From 00a5e4ef0eb094fc0aecb175990ff79aea688d5b Mon Sep 17 00:00:00 2001 From: Trey t Date: Thu, 15 Jan 2026 09:42:08 -0600 Subject: [PATCH] fix(planning): enforce daily driving limit for same-day games and group itinerary by city Two fixes for route planning and display: 1. GameDAGRouter: Same-day game transitions now respect maxDailyDrivingHours constraint. Previously, a 12:05 AM game in Arlington could connect to an 11:40 PM game in Milwaukee (19+ hour drive) because the code only checked available time, not the 8-hour daily limit. 2. TripDetailView: Itinerary sections now group by (day, city) not just day. Games in different cities on the same calendar day are shown as separate sections with travel segments between them. Co-Authored-By: Claude Opus 4.5 --- .../Features/Trip/Views/TripDetailView.swift | 41 ++++++++++++------- .../Planning/Engine/GameDAGRouter.swift | 4 +- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/SportsTime/Features/Trip/Views/TripDetailView.swift b/SportsTime/Features/Trip/Views/TripDetailView.swift index a6bae3c..8f47d7d 100644 --- a/SportsTime/Features/Trip/Views/TripDetailView.swift +++ b/SportsTime/Features/Trip/Views/TripDetailView.swift @@ -348,36 +348,47 @@ struct TripDetailView: View { } } - /// Build itinerary sections: days with travel between different cities + /// Build itinerary sections: group by day AND city, with travel between different cities private var itinerarySections: [ItinerarySection] { var sections: [ItinerarySection] = [] - // Build day sections for days with games - var daySections: [(dayNumber: Int, date: Date, city: String, games: [RichGame])] = [] + // Build day+city sections for days with games + var dayCitySections: [(dayNumber: Int, date: Date, city: String, games: [RichGame])] = [] let days = tripDays for (index, dayDate) in days.enumerated() { let dayNum = index + 1 let gamesOnDay = gamesOn(date: dayDate) - // Get city from games (preferred) or from stops as fallback - let cityForDay = gamesOnDay.first?.stadium.city ?? cityOn(date: dayDate) ?? "" + guard !gamesOnDay.isEmpty else { continue } - // Include days with games - // Skip empty days at the end (departure day after last game) - if !gamesOnDay.isEmpty { - daySections.append((dayNum, dayDate, cityForDay, gamesOnDay)) + // Group games by city, maintaining chronological order + var gamesByCity: [(city: String, games: [RichGame])] = [] + for game in gamesOnDay { + let city = game.stadium.city + if let lastIndex = gamesByCity.indices.last, gamesByCity[lastIndex].city == city { + // Same city as previous game - add to existing group + gamesByCity[lastIndex].games.append(game) + } else { + // Different city - start new group + gamesByCity.append((city, [game])) + } + } + + // Add each city group as a separate section + for cityGroup in gamesByCity { + dayCitySections.append((dayNum, dayDate, cityGroup.city, cityGroup.games)) } } - // Build sections: insert travel BEFORE each day when coming from different city - for (index, daySection) in daySections.enumerated() { + // Build sections: insert travel BEFORE each section when coming from different city + for (index, section) in dayCitySections.enumerated() { - // Check if we need travel BEFORE this day (coming from different city) + // Check if we need travel BEFORE this section (coming from different city) if index > 0 { - let prevSection = daySections[index - 1] + let prevSection = dayCitySections[index - 1] let prevCity = prevSection.city - let currentCity = daySection.city + let currentCity = section.city // If cities differ, find travel segment from prev -> current if !prevCity.isEmpty && !currentCity.isEmpty && prevCity != currentCity { @@ -388,7 +399,7 @@ struct TripDetailView: View { } // Add the day section - sections.append(.day(dayNumber: daySection.dayNumber, date: daySection.date, games: daySection.games)) + sections.append(.day(dayNumber: section.dayNumber, date: section.date, games: section.games)) } return sections diff --git a/SportsTime/Planning/Engine/GameDAGRouter.swift b/SportsTime/Planning/Engine/GameDAGRouter.swift index 356cc90..2968e55 100644 --- a/SportsTime/Planning/Engine/GameDAGRouter.swift +++ b/SportsTime/Planning/Engine/GameDAGRouter.swift @@ -542,8 +542,10 @@ enum GameDAGRouter { let availableHours = deadline.timeIntervalSince(departureTime) / 3600.0 // Calculate driving hours available + // For same-day games: enforce both time availability AND daily driving limit + // For multi-day trips: use total available driving hours across days let maxDrivingHoursAvailable = daysBetween == 0 - ? max(0, availableHours) + ? min(max(0, availableHours), constraints.maxDailyDrivingHours) : Double(daysBetween) * constraints.maxDailyDrivingHours let feasible = drivingHours <= maxDrivingHoursAvailable && drivingHours <= availableHours