feat: improve planning engine travel handling, itinerary reordering, and scenario planners

Add TravelInfo initializers and city normalization helpers to fix repeat
city-pair disambiguation. Improve drag-and-drop reordering with segment
index tracking and source-row-aware zone calculation. Enhance all five
scenario planners with better next-day departure handling and travel
segment placement. Add comprehensive tests across all planners.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-13 08:55:23 -06:00
parent 1c97f35754
commit 9736773475
19 changed files with 928 additions and 171 deletions

View File

@@ -831,21 +831,23 @@ enum ItineraryReorderingLogic {
in travelValidRanges: [String: ClosedRange<Int>],
model: ItineraryItem? = nil
) -> String {
let suffix = "\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
let from = TravelInfo.normalizeCityName(segment.fromLocation.name)
let to = TravelInfo.normalizeCityName(segment.toLocation.name)
let suffix = "\(from)->\(to)"
let matchingKeys = travelValidRanges.keys.filter { $0.hasSuffix(suffix) }
if let segIdx = model?.travelInfo?.segmentIndex {
let indexedKey = "travel:\(segIdx):\(suffix)"
if matchingKeys.contains(indexedKey) {
return indexedKey
}
return indexedKey
}
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)"
}