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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user