fix(pdf): include all trip days in PDF export
Fixed PDF export missing the last day when games occur on departure date. Root cause: Trip.itineraryDays() calculated lastActivityDate as departure - 1, assuming departure is always after the last activity. When games happen ON the departure date, that day was skipped. Fix: Check if the last stop has games. If so, include the departure date in the itinerary loop. Also includes: - buildCompleteItineraryItems() to merge games, travel, and custom items - Magazine-style PDF layout with sport-specific accent colors - Proper iteration over all trip days in PDF generator Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
//
|
||||
// - Expected Behavior:
|
||||
// - itineraryDays() returns one ItineraryDay per calendar day from first arrival to last activity
|
||||
// - Last activity day is departure - 1 (departure is when you leave)
|
||||
// - Last activity day includes departure day if there are games on that day
|
||||
// - tripDuration is max(1, days between first arrival and last departure + 1)
|
||||
// - cities returns deduplicated city list preserving visit order
|
||||
// - displayName uses " → " separator between cities
|
||||
@@ -113,16 +113,19 @@ struct Trip: Identifiable, Codable, Hashable {
|
||||
var days: [ItineraryDay] = []
|
||||
let calendar = Calendar.current
|
||||
|
||||
guard let firstDate = stops.first?.arrivalDate else { return days }
|
||||
guard let firstDate = stops.first?.arrivalDate,
|
||||
let lastStop = stops.last else { return days }
|
||||
|
||||
// Find the last day with actual activity (last game date or last arrival)
|
||||
// Departure date is the day AFTER the last game, so we use day before departure
|
||||
// Find the last day with actual activity
|
||||
// If the last stop has games, include the departure day (games can happen on departure day)
|
||||
// Otherwise, use departure - 1 (departure is a pure travel day)
|
||||
let lastActivityDate: Date
|
||||
if let lastDeparture = stops.last?.departureDate {
|
||||
// Last activity is day before departure (departure is when you leave)
|
||||
lastActivityDate = calendar.date(byAdding: .day, value: -1, to: lastDeparture) ?? lastDeparture
|
||||
if !lastStop.games.isEmpty {
|
||||
// Last stop has games - include departure day since games may occur on it
|
||||
lastActivityDate = lastStop.departureDate
|
||||
} else {
|
||||
lastActivityDate = stops.last?.arrivalDate ?? firstDate
|
||||
// No games at last stop - departure is just when you leave
|
||||
lastActivityDate = calendar.date(byAdding: .day, value: -1, to: lastStop.departureDate) ?? lastStop.departureDate
|
||||
}
|
||||
|
||||
var currentDate = calendar.startOfDay(for: firstDate)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1199,7 +1199,14 @@ struct TripDetailView: View {
|
||||
exportProgress = nil
|
||||
|
||||
do {
|
||||
let url = try await exportService.exportToPDF(trip: trip, games: games) { progress in
|
||||
// Build complete itinerary items (games + travel + custom)
|
||||
let completeItems = buildCompleteItineraryItems()
|
||||
|
||||
let url = try await exportService.exportToPDF(
|
||||
trip: trip,
|
||||
games: games,
|
||||
itineraryItems: completeItems
|
||||
) { progress in
|
||||
await MainActor.run {
|
||||
self.exportProgress = progress
|
||||
}
|
||||
@@ -1213,6 +1220,57 @@ struct TripDetailView: View {
|
||||
isExporting = false
|
||||
}
|
||||
|
||||
/// Build complete itinerary items by merging games, travel, and custom items
|
||||
private func buildCompleteItineraryItems() -> [ItineraryItem] {
|
||||
var allItems: [ItineraryItem] = []
|
||||
|
||||
// Get itinerary days from trip
|
||||
let tripDays = trip.itineraryDays()
|
||||
|
||||
// 1. Add game items using day.gameIds (reliable source from trip stops)
|
||||
for day in tripDays {
|
||||
for (gameIndex, gameId) in day.gameIds.enumerated() {
|
||||
guard let richGame = games[gameId] else { continue }
|
||||
let gameItem = ItineraryItem(
|
||||
tripId: trip.id,
|
||||
day: day.dayNumber,
|
||||
sortOrder: Double(gameIndex) * 0.01, // Games near the start of the day
|
||||
kind: .game(gameId: gameId, city: richGame.stadium.city)
|
||||
)
|
||||
allItems.append(gameItem)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Add travel items (from trip segments + overrides)
|
||||
for segment in trip.travelSegments {
|
||||
let travelId = "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
|
||||
|
||||
// Use override if available, otherwise default to day 1
|
||||
let override = travelOverrides[travelId]
|
||||
let day = override?.day ?? 1
|
||||
let sortOrder = override?.sortOrder ?? 100.0 // After games by default
|
||||
|
||||
let travelItem = ItineraryItem(
|
||||
tripId: trip.id,
|
||||
day: day,
|
||||
sortOrder: sortOrder,
|
||||
kind: .travel(TravelInfo(
|
||||
fromCity: segment.fromLocation.name,
|
||||
toCity: segment.toLocation.name,
|
||||
distanceMeters: segment.distanceMeters,
|
||||
durationSeconds: segment.durationSeconds
|
||||
))
|
||||
)
|
||||
allItems.append(travelItem)
|
||||
}
|
||||
|
||||
// 3. Add custom items (from CloudKit)
|
||||
let customItems = itineraryItems.filter { $0.isCustom }
|
||||
allItems.append(contentsOf: customItems)
|
||||
|
||||
return allItems
|
||||
}
|
||||
|
||||
private func toggleSaved() {
|
||||
if isSaved {
|
||||
unsaveTrip()
|
||||
|
||||
Reference in New Issue
Block a user