Refactor itinerary display: separate day sections and travel sections

- Days are now headers with games as rows in that section
- Travel between days shown in standalone "Travel" sections
- Travel timing is flexible (user decides when to drive)
- Removed complex ItineraryDay bundling approach

🤖 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 12:41:32 -06:00
parent ab89c25f2f
commit 8ec8ed02b1

View File

@@ -326,16 +326,48 @@ struct TripDetailView: View {
Text("Itinerary") Text("Itinerary")
.font(.headline) .font(.headline)
ForEach(tripDays, id: \.self) { dayDate in ForEach(itinerarySections.indices, id: \.self) { index in
SimpleDayCard( let section = itinerarySections[index]
dayNumber: dayNumber(for: dayDate), switch section {
date: dayDate, case .day(let dayNumber, let date, let gamesOnDay):
gamesOnDay: gamesOn(date: dayDate), DaySection(
travelOnDay: travelOn(date: dayDate) dayNumber: dayNumber,
date: date,
games: gamesOnDay
) )
case .travel(let segment):
TravelSection(segment: segment)
} }
} }
} }
}
/// Build itinerary sections: days and travel between days
private var itinerarySections: [ItinerarySection] {
var sections: [ItinerarySection] = []
let calendar = Calendar.current
// Get all days
let days = tripDays
for (index, dayDate) in days.enumerated() {
let dayNum = index + 1
let gamesOnDay = gamesOn(date: dayDate)
// Add day section (even if no games - could be rest day)
if !gamesOnDay.isEmpty || index == 0 || index == days.count - 1 {
sections.append(.day(dayNumber: dayNum, date: dayDate, games: gamesOnDay))
}
// Check for travel AFTER this day (between this day and next)
let travelAfterDay = travelDepartingAfter(date: dayDate, beforeNextGameDay: days.indices.contains(index + 1) ? days[index + 1] : nil)
for segment in travelAfterDay {
sections.append(.travel(segment))
}
}
return sections
}
/// All calendar days in the trip /// All calendar days in the trip
private var tripDays: [Date] { private var tripDays: [Date] {
@@ -354,14 +386,6 @@ struct TripDetailView: View {
return days return days
} }
/// Day number for a given date
private func dayNumber(for date: Date) -> Int {
guard let firstDay = tripDays.first else { return 1 }
let calendar = Calendar.current
let days = calendar.dateComponents([.day], from: firstDay, to: date).day ?? 0
return days + 1
}
/// Games scheduled on a specific date /// Games scheduled on a specific date
private func gamesOn(date: Date) -> [RichGame] { private func gamesOn(date: Date) -> [RichGame] {
let calendar = Calendar.current let calendar = Calendar.current
@@ -372,16 +396,19 @@ struct TripDetailView: View {
return allGameIds.compactMap { games[$0] }.filter { richGame in return allGameIds.compactMap { games[$0] }.filter { richGame in
calendar.startOfDay(for: richGame.game.dateTime) == dayStart calendar.startOfDay(for: richGame.game.dateTime) == dayStart
} }.sorted { $0.game.dateTime < $1.game.dateTime }
} }
/// Travel segments departing on a specific date /// Travel segments that depart after a given day (for between-day travel)
private func travelOn(date: Date) -> [TravelSegment] { private func travelDepartingAfter(date: Date, beforeNextGameDay: Date?) -> [TravelSegment] {
let calendar = Calendar.current let calendar = Calendar.current
let dayStart = calendar.startOfDay(for: date) let dayEnd = calendar.startOfDay(for: date)
return trip.travelSegments.filter { segment in return trip.travelSegments.filter { segment in
calendar.startOfDay(for: segment.departureTime) == dayStart let segmentDay = calendar.startOfDay(for: segment.departureTime)
// Travel is "after" this day if it departs on or after this day
// and arrives at a different city
return segmentDay == dayEnd
} }
} }
@@ -431,13 +458,19 @@ struct TripDetailView: View {
} }
} }
// MARK: - Simple Day Card (queries games and travel separately by date) // MARK: - Itinerary Section (enum for day vs travel sections)
struct SimpleDayCard: View { enum ItinerarySection {
case day(dayNumber: Int, date: Date, games: [RichGame])
case travel(TravelSegment)
}
// MARK: - Day Section (header + games)
struct DaySection: View {
let dayNumber: Int let dayNumber: Int
let date: Date let date: Date
let gamesOnDay: [RichGame] let games: [RichGame]
let travelOnDay: [TravelSegment]
private var formattedDate: String { private var formattedDate: String {
let formatter = DateFormatter() let formatter = DateFormatter()
@@ -445,17 +478,16 @@ struct SimpleDayCard: View {
return formatter.string(from: date) return formatter.string(from: date)
} }
private var isRestDay: Bool { private var gameCity: String? {
gamesOnDay.isEmpty && travelOnDay.isEmpty games.first?.stadium.city
} }
/// City where games are (from stadium) private var isRestDay: Bool {
private var gameCity: String? { games.isEmpty
gamesOnDay.first?.stadium.city
} }
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 8) {
// Day header // Day header
HStack { HStack {
Text("Day \(dayNumber)") Text("Day \(dayNumber)")
@@ -479,17 +511,15 @@ struct SimpleDayCard: View {
} }
} }
// Games (each as its own row) // City label (if games exist)
if !gamesOnDay.isEmpty {
VStack(alignment: .leading, spacing: 6) {
// City label
if let city = gameCity { if let city = gameCity {
Label(city, systemImage: "mappin") Label(city, systemImage: "mappin")
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
ForEach(gamesOnDay, id: \.game.id) { richGame in // Games
ForEach(games, id: \.game.id) { richGame in
HStack { HStack {
Image(systemName: richGame.game.sport.iconName) Image(systemName: richGame.game.sport.iconName)
.foregroundStyle(.blue) .foregroundStyle(.blue)
@@ -503,21 +533,37 @@ struct SimpleDayCard: View {
} }
.padding(.vertical, 6) .padding(.vertical, 6)
.padding(.horizontal, 10) .padding(.horizontal, 10)
.background(Color(.secondarySystemBackground)) .background(Color(.tertiarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: 8))
} }
} }
.padding()
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 12))
}
} }
// Travel segments (each as its own row, separate from games) // MARK: - Travel Section (standalone travel between days)
ForEach(travelOnDay) { segment in
struct TravelSection: View {
let segment: TravelSegment
var body: some View {
VStack(alignment: .leading, spacing: 8) {
// Travel header
Text("Travel")
.font(.subheadline)
.fontWeight(.semibold)
.foregroundStyle(.orange)
// Travel details
HStack(spacing: 8) { HStack(spacing: 8) {
Image(systemName: segment.travelMode.iconName) Image(systemName: segment.travelMode.iconName)
.foregroundStyle(.orange) .foregroundStyle(.orange)
.frame(width: 20) .frame(width: 20)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text("Drive to \(segment.toLocation.name)") Text("\(segment.fromLocation.name) \(segment.toLocation.name)")
.font(.subheadline) .font(.subheadline)
.fontWeight(.medium) .fontWeight(.medium)
@@ -532,15 +578,14 @@ struct SimpleDayCard: View {
.padding(.horizontal, 10) .padding(.horizontal, 10)
.background(Color.orange.opacity(0.1)) .background(Color.orange.opacity(0.1))
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.orange.opacity(0.3), lineWidth: 1)
)
}
} }
.padding() .padding()
.background(Color(.secondarySystemBackground)) .background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 12)) .clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.orange.opacity(0.3), lineWidth: 1)
)
} }
} }