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