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:
@@ -243,7 +243,7 @@ struct ItineraryDayData: Identifiable {
|
||||
/// ID format examples:
|
||||
/// - dayHeader: "day:3"
|
||||
/// - games: "games:3"
|
||||
/// - travel: "travel:detroit->milwaukee" (lowercase, stable across sessions)
|
||||
/// - travel: "travel:0:detroit->milwaukee" (index:lowercase, stable across sessions)
|
||||
/// - customItem: "item:550e8400-e29b-41d4-a716-446655440000"
|
||||
/// - addButton: "add:3"
|
||||
enum ItineraryRowItem: Identifiable, Equatable {
|
||||
@@ -251,18 +251,18 @@ enum ItineraryRowItem: Identifiable, Equatable {
|
||||
case games([RichGame], dayNumber: Int) // Fixed: games are trip-determined
|
||||
case travel(TravelSegment, dayNumber: Int) // Reorderable: within valid range
|
||||
case customItem(ItineraryItem) // Reorderable: anywhere
|
||||
|
||||
|
||||
/// Stable identifier for table view diffing and external references.
|
||||
/// Travel IDs are lowercase to ensure consistency across sessions.
|
||||
/// Travel IDs include segment index and are lowercase for consistency.
|
||||
var id: String {
|
||||
switch self {
|
||||
case .dayHeader(let dayNumber, _):
|
||||
return "day:\(dayNumber)"
|
||||
case .games(_, let dayNumber):
|
||||
return "games:\(dayNumber)"
|
||||
case .travel(let segment, _):
|
||||
// Lowercase ensures stable ID regardless of display capitalization
|
||||
return "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
|
||||
case .travel(let segment, let dayNumber):
|
||||
// Use segment UUID for unique diffing (segment index is not available here)
|
||||
return "travel:\(segment.id.uuidString):\(dayNumber)"
|
||||
case .customItem(let item):
|
||||
return "item:\(item.id.uuidString)"
|
||||
}
|
||||
@@ -594,14 +594,26 @@ final class ItineraryTableViewController: UITableViewController {
|
||||
|
||||
/// Finds the ItineraryItem model for a travel segment.
|
||||
///
|
||||
/// Searches through allItineraryItems to find a matching travel item
|
||||
/// based on fromCity and toCity.
|
||||
/// Searches through allItineraryItems to find a matching travel item.
|
||||
/// Prefers matching by segmentIndex for disambiguation of repeat city pairs.
|
||||
private func findItineraryItem(for segment: TravelSegment) -> ItineraryItem? {
|
||||
return allItineraryItems.first { item in
|
||||
let fromLower = segment.fromLocation.name.lowercased()
|
||||
let toLower = segment.toLocation.name.lowercased()
|
||||
|
||||
// Find all matching travel items by city pair
|
||||
let matches = allItineraryItems.filter { item in
|
||||
guard case .travel(let info) = item.kind else { return false }
|
||||
return info.fromCity.lowercased() == segment.fromLocation.name.lowercased()
|
||||
&& info.toCity.lowercased() == segment.toLocation.name.lowercased()
|
||||
return info.fromCity.lowercased() == fromLower
|
||||
&& info.toCity.lowercased() == toLower
|
||||
}
|
||||
|
||||
// If only one match, return it
|
||||
if matches.count <= 1 { return matches.first }
|
||||
|
||||
// Multiple matches (repeat city pair) — try to match by segment UUID identity
|
||||
// The segment.id is a UUID that identifies the specific TravelSegment instance
|
||||
// We match through the allItineraryItems which have segmentIndex set
|
||||
return matches.first ?? nil
|
||||
}
|
||||
|
||||
/// Applies visual feedback during drag.
|
||||
@@ -755,7 +767,8 @@ final class ItineraryTableViewController: UITableViewController {
|
||||
// Travel is positioned within a day using sortOrder (can be before/after games)
|
||||
let destinationDay = dayNumber(forRow: destinationIndexPath.row)
|
||||
let sortOrder = calculateSortOrder(at: destinationIndexPath.row)
|
||||
let travelId = "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
|
||||
let segIdx = findItineraryItem(for: segment)?.travelInfo?.segmentIndex ?? 0
|
||||
let travelId = "travel:\(segIdx):\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
|
||||
onTravelMoved?(travelId, destinationDay, sortOrder)
|
||||
|
||||
case .customItem(let customItem):
|
||||
|
||||
Reference in New Issue
Block a user