fix: resolve travel anchor ID collision for repeat city pairs

Include segment index in travel anchor IDs ("travel:INDEX:from->to")
so Follow Team trips visiting the same city pair multiple times get
unique, independently addressable travel segments. Prevents override
dictionary collisions and incorrect validDayRange lookups.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-11 10:57:53 -06:00
parent 633f7d883f
commit ff6f4b6c2c
12 changed files with 291 additions and 79 deletions

View File

@@ -138,23 +138,23 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
}
}
for segment in trip.travelSegments {
let travelId = stableTravelAnchorId(segment)
for (segmentIndex, segment) in trip.travelSegments.enumerated() {
let travelId = stableTravelAnchorId(segment, at: segmentIndex)
let fromCity = segment.fromLocation.name
let toCity = segment.toLocation.name
// VALID RANGE:
// - Earliest: day of last from-city game (travel can happen AFTER that game)
// - Latest: day of first to-city game (travel can happen BEFORE that game)
let lastFromGameDay = findLastGameDay(in: fromCity, tripDays: tripDays)
let firstToGameDay = findFirstGameDay(in: toCity, tripDays: tripDays)
let minDay = max(lastFromGameDay == 0 ? 1 : lastFromGameDay, 1)
let maxDay = min(firstToGameDay == 0 ? tripDays.count : firstToGameDay, tripDays.count)
let validRange = (minDay <= maxDay) ? (minDay...maxDay) : (maxDay...maxDay)
travelValidRanges[travelId] = validRange
// Placement (override if valid)
let placement: TravelOverride
if let override = travelOverrides[travelId], validRange.contains(override.day) {
@@ -177,7 +177,7 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
placement = TravelOverride(day: day, sortOrder: sortOrder)
}
let travelItem = ItineraryItem(
tripId: trip.id,
day: placement.day,
@@ -186,6 +186,7 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
TravelInfo(
fromCity: fromCity,
toCity: toCity,
segmentIndex: segmentIndex,
distanceMeters: segment.distanceMeters,
durationSeconds: segment.durationSeconds
)
@@ -217,13 +218,20 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
.filter { $0.day == dayNum }
.sorted { $0.sortOrder < $1.sortOrder }
for travel in travelsForDay {
// Find the segment matching this travel
if let info = travel.travelInfo,
let seg = trip.travelSegments.first(where: {
$0.fromLocation.name.lowercased() == info.fromCity.lowercased()
&& $0.toLocation.name.lowercased() == info.toCity.lowercased()
}) {
rows.append(.travel(seg, dayNumber: dayNum))
// Find the segment matching this travel by segment index (preferred) or city pair (legacy)
if let info = travel.travelInfo {
let seg: TravelSegment?
if let idx = info.segmentIndex, idx < trip.travelSegments.count {
seg = trip.travelSegments[idx]
} else {
seg = trip.travelSegments.first(where: {
$0.fromLocation.name.lowercased() == info.fromCity.lowercased()
&& $0.toLocation.name.lowercased() == info.toCity.lowercased()
})
}
if let seg {
rows.append(.travel(seg, dayNumber: dayNum))
}
}
}
@@ -233,12 +241,12 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
switch r {
case .customItem(let it): return it.sortOrder
case .travel(let seg, _):
let id = stableTravelAnchorId(seg)
let segIdx = trip.travelSegments.firstIndex(where: { $0.id == seg.id }) ?? 0
let id = stableTravelAnchorId(seg, at: segIdx)
return (travelOverrides[id]?.sortOrder)
?? (travelItems.first(where: { ti in
guard case .travel(let inf) = ti.kind else { return false }
return inf.fromCity.lowercased() == seg.fromLocation.name.lowercased()
&& inf.toCity.lowercased() == seg.toLocation.name.lowercased()
return inf.segmentIndex == segIdx
})?.sortOrder ?? 0.0)
default:
return 0.0
@@ -285,8 +293,8 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
.sorted { $0.game.dateTime < $1.game.dateTime }
}
private func stableTravelAnchorId(_ segment: TravelSegment) -> String {
"travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
private func stableTravelAnchorId(_ segment: TravelSegment, at index: Int) -> String {
"travel:\(index):\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
}
private func findLastGameDay(in city: String, tripDays: [Date]) -> Int {