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

@@ -531,11 +531,10 @@ enum ItineraryReorderingLogic {
return valid
case .travel(let segment, _):
let travelId = "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
let validDayRange = travelValidRanges[travelId]
// Use existing model if available, otherwise create a default
let model = findTravelItem(segment) ?? makeTravelItem(segment)
let travelId = travelIdForSegment(segment, in: travelValidRanges, model: model)
let validDayRange = travelValidRanges[travelId]
guard let constraints = constraints else {
// No constraint engine, allow all rows except 0 and day headers
@@ -750,15 +749,16 @@ enum ItineraryReorderingLogic {
constraints: ItineraryConstraints?,
findTravelItem: (TravelSegment) -> ItineraryItem?
) -> DragZones {
let travelId = "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
let model = findTravelItem(segment)
let travelId = travelIdForSegment(segment, in: travelValidRanges, model: model)
guard let validRange = travelValidRanges[travelId] else {
return DragZones(invalidRowIndices: [], validDropRows: [], barrierGameIds: [])
}
var invalidRows = Set<Int>()
var validRows: [Int] = []
for (index, rowItem) in flatItems.enumerated() {
let dayNum: Int
switch rowItem {
@@ -820,6 +820,36 @@ enum ItineraryReorderingLogic {
)
}
// MARK: - Travel ID Lookup
/// Find the travel ID key for a segment in the travelValidRanges dictionary.
/// Keys are formatted as "travel:INDEX:from->to".
/// When multiple keys share the same city pair (repeat visits), matches by
/// checking all keys and preferring the one whose index matches the model's segmentIndex.
private static func travelIdForSegment(
_ segment: TravelSegment,
in travelValidRanges: [String: ClosedRange<Int>],
model: ItineraryItem? = nil
) -> String {
let suffix = "\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
let matchingKeys = travelValidRanges.keys.filter { $0.hasSuffix(suffix) }
if matchingKeys.count == 1, let key = matchingKeys.first {
return key
}
// Multiple matches (repeat city pair) use segmentIndex from model to disambiguate
if let segIdx = model?.travelInfo?.segmentIndex {
let expected = "travel:\(segIdx):\(suffix)"
if matchingKeys.contains(expected) {
return expected
}
}
// Fallback: return first match or construct without index
return matchingKeys.first ?? "travel:\(suffix)"
}
// MARK: - Utility Functions
/// Finds the nearest value in a sorted array using binary search.