# Flexible Itinerary Ordering Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Enable fully flexible ordering of itinerary items - custom items can go anywhere, travel is reorderable within day constraints, games have sortOrder but are immovable. **Architecture:** Extend existing `TravelDayOverride` model with `sortOrder` field. Update flattening logic to sort all orderable items (games, travel, custom) together by sortOrder within each day. Games get auto-assigned sortOrder (100, 101...) but no drag handle. **Tech Stack:** Swift, SwiftUI, UIKit (UITableViewController), CloudKit --- ## Task 1: Update TravelDayOverride Domain Model **Files:** - Modify: `SportsTime/Core/Models/Domain/TravelDayOverride.swift` **Step 1: Add sortOrder field to TravelDayOverride** ```swift struct TravelDayOverride: Identifiable, Codable, Hashable { let id: UUID let tripId: UUID let travelAnchorId: String var displayDay: Int var sortOrder: Double // NEW: Position within day (allows interleaving with custom items) let createdAt: Date var modifiedAt: Date init( id: UUID = UUID(), tripId: UUID, travelAnchorId: String, displayDay: Int, sortOrder: Double = 50.0, // Default before games (games start at 100) createdAt: Date = Date(), modifiedAt: Date = Date() ) { self.id = id self.tripId = tripId self.travelAnchorId = travelAnchorId self.displayDay = displayDay self.sortOrder = sortOrder self.createdAt = createdAt self.modifiedAt = modifiedAt } } ``` **Step 2: Build to verify compilation** Run: `cd /Users/treyt/Desktop/code/SportsTime && xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | tail -20` Expected: Build succeeds (existing code still works due to default parameter) **Step 3: Commit** ```bash git add SportsTime/Core/Models/Domain/TravelDayOverride.swift git commit -m "feat(model): add sortOrder to TravelDayOverride" ``` --- ## Task 2: Update CloudKit Model for TravelDayOverride **Files:** - Modify: `SportsTime/Core/Models/CloudKit/CKModels.swift` (CKTravelDayOverride section) **Step 1: Add sortOrder field to CKTravelDayOverride** Find `CKTravelDayOverride` struct and add: ```swift struct CKTravelDayOverride { static let overrideIdKey = "overrideId" static let tripIdKey = "tripId" static let travelAnchorIdKey = "travelAnchorId" static let displayDayKey = "displayDay" static let sortOrderKey = "sortOrder" // NEW static let createdAtKey = "createdAt" static let modifiedAtKey = "modifiedAt" let record: CKRecord init(record: CKRecord) { self.record = record } init(override: TravelDayOverride) { let record = CKRecord( recordType: CKRecordType.travelDayOverride, recordID: CKRecord.ID(recordName: override.id.uuidString) ) record[CKTravelDayOverride.overrideIdKey] = override.id.uuidString record[CKTravelDayOverride.tripIdKey] = override.tripId.uuidString record[CKTravelDayOverride.travelAnchorIdKey] = override.travelAnchorId record[CKTravelDayOverride.displayDayKey] = override.displayDay record[CKTravelDayOverride.sortOrderKey] = override.sortOrder // NEW record[CKTravelDayOverride.createdAtKey] = override.createdAt record[CKTravelDayOverride.modifiedAtKey] = override.modifiedAt self.record = record } func toOverride() -> TravelDayOverride? { guard let overrideIdString = record[CKTravelDayOverride.overrideIdKey] as? String, let overrideId = UUID(uuidString: overrideIdString), let tripIdString = record[CKTravelDayOverride.tripIdKey] as? String, let tripId = UUID(uuidString: tripIdString), let travelAnchorId = record[CKTravelDayOverride.travelAnchorIdKey] as? String, let displayDay = record[CKTravelDayOverride.displayDayKey] as? Int, let createdAt = record[CKTravelDayOverride.createdAtKey] as? Date, let modifiedAt = record[CKTravelDayOverride.modifiedAtKey] as? Date else { return nil } // Migration: default sortOrder for existing records without it let sortOrder = record[CKTravelDayOverride.sortOrderKey] as? Double ?? 50.0 return TravelDayOverride( id: overrideId, tripId: tripId, travelAnchorId: travelAnchorId, displayDay: displayDay, sortOrder: sortOrder, createdAt: createdAt, modifiedAt: modifiedAt ) } } ``` **Step 2: Build to verify compilation** Run: `cd /Users/treyt/Desktop/code/SportsTime && xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | tail -20` Expected: Build succeeds **Step 3: Commit** ```bash git add SportsTime/Core/Models/CloudKit/CKModels.swift git commit -m "feat(cloudkit): add sortOrder to CKTravelDayOverride" ``` --- ## Task 3: Update TravelOverrideService **Files:** - Modify: `SportsTime/Core/Services/TravelOverrideService.swift` **Step 1: Update saveOverride to persist sortOrder** In `saveOverride(_:)`, add the sortOrder field when updating existing record: ```swift if let existing = existingOverride { let recordID = CKRecord.ID(recordName: existing.id.uuidString) do { let existingRecord = try await publicDatabase.record(for: recordID) existingRecord[CKTravelDayOverride.displayDayKey] = override.displayDay existingRecord[CKTravelDayOverride.sortOrderKey] = override.sortOrder // NEW existingRecord[CKTravelDayOverride.modifiedAtKey] = now record = existingRecord // ... } } ``` **Step 2: Update fetchOverridesAsDictionary return type** Change from `[String: Int]` to return full `TravelDayOverride` objects: ```swift /// Fetch all travel overrides for a trip, keyed by travelAnchorId /// If duplicate travelAnchorIds exist (legacy data), takes the most recently modified one func fetchOverridesAsDictionary(forTripId tripId: UUID) async throws -> [String: TravelDayOverride] { let overrides = try await fetchOverrides(forTripId: tripId) let sorted = overrides.sorted { $0.modifiedAt > $1.modifiedAt } return Dictionary(sorted.map { ($0.travelAnchorId, $0) }) { first, _ in first } } ``` **Step 3: Build to verify compilation** Run: `cd /Users/treyt/Desktop/code/SportsTime && xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | tail -20` Expected: Build fails (TripDetailView uses old return type) - this is expected **Step 4: Commit** ```bash git add SportsTime/Core/Services/TravelOverrideService.swift git commit -m "feat(service): update TravelOverrideService for sortOrder" ``` --- ## Task 4: Update ItineraryTableViewWrapper Interface **Files:** - Modify: `SportsTime/Features/Trip/Views/ItineraryTableViewWrapper.swift` **Step 1: Change travelDayOverrides type** Update the property and init: ```swift let travelDayOverrides: [String: TravelDayOverride] // Changed from [String: Int] ``` Update init parameter: ```swift init( trip: Trip, games: [RichGame], customItems: [CustomItineraryItem], travelDayOverrides: [String: TravelDayOverride], // Changed // ... ) ``` **Step 2: Update buildItineraryData to use sortOrder** In `buildItineraryData()`, update the override lookup: ```swift // Use override if valid, otherwise use default if let override = travelDayOverrides[travelId], validRange.contains(override.displayDay) { travelByDay[override.displayDay] = (segment, override.sortOrder) } else { let clampedDefault = max(validRange.lowerBound, min(defaultDay, validRange.upperBound)) travelByDay[clampedDefault] = (segment, 50.0) // Default sortOrder before games } ``` **Step 3: Update ItineraryDayData to store travel sortOrder** Change `travelBefore` to include sortOrder: ```swift struct ItineraryDayData: Identifiable { let id: Int let dayNumber: Int let date: Date let games: [RichGame] var items: [ItineraryRowItem] var travelBefore: (segment: TravelSegment, sortOrder: Double)? // Changed // ... } ``` **Step 4: Build (expect failures - controller needs update)** Run: `cd /Users/treyt/Desktop/code/SportsTime && xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | grep -E "error:|warning:" | head -20` **Step 5: Commit** ```bash git add SportsTime/Features/Trip/Views/ItineraryTableViewWrapper.swift git commit -m "feat(wrapper): update interface for travel sortOrder" ``` --- ## Task 5: Update ItineraryRowItem to Include sortOrder **Files:** - Modify: `SportsTime/Features/Trip/Views/ItineraryTableViewController.swift` **Step 1: Add sortOrder to ItineraryRowItem enum** Update cases to include sortOrder where needed: ```swift enum ItineraryRowItem: Identifiable, Equatable { case dayHeader(dayNumber: Int, date: Date) case games([RichGame], dayNumber: Int, sortOrder: Double) // Added sortOrder case travel(TravelSegment, dayNumber: Int, sortOrder: Double) // Added sortOrder case customItem(CustomItineraryItem) // Already has sortOrder in the model /// Get sortOrder for sorting within a day (nil for headers) var sortOrder: Double? { switch self { case .dayHeader: return nil case .games(_, _, let order): return order case .travel(_, _, let order): return order case .customItem(let item): return item.sortOrder } } // Update id and isReorderable as needed... } ``` **Step 2: Update id property** ```swift var id: String { switch self { case .dayHeader(let dayNumber, _): return "day:\(dayNumber)" case .games(_, let dayNumber, _): return "games:\(dayNumber)" case .travel(let segment, _, _): return "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())" case .customItem(let item): return "item:\(item.id.uuidString)" } } ``` **Step 3: Commit** ```bash git add SportsTime/Features/Trip/Views/ItineraryTableViewController.swift git commit -m "feat(controller): add sortOrder to ItineraryRowItem" ``` --- ## Task 6: Update Flattening Logic to Sort All Items Together **Files:** - Modify: `SportsTime/Features/Trip/Views/ItineraryTableViewController.swift` **Step 1: Rewrite reloadData flattening** Replace the flattening loop in `reloadData(days:travelValidRanges:)`: ```swift func reloadData(days: [ItineraryDayData], travelValidRanges: [String: ClosedRange]) { self.travelValidRanges = travelValidRanges flatItems = [] for day in days { // 1. Day header (fixed, always first) flatItems.append(.dayHeader(dayNumber: day.dayNumber, date: day.date)) // 2. Collect all orderable items for this day var orderableItems: [ItineraryRowItem] = [] // Games get sortOrder starting at 100 for (index, game) in day.games.enumerated() { let gameSortOrder = 100.0 + Double(index) orderableItems.append(.games([game], dayNumber: day.dayNumber, sortOrder: gameSortOrder)) } // Travel (if arriving this day) if let (travel, sortOrder) = day.travelBefore { orderableItems.append(.travel(travel, dayNumber: day.dayNumber, sortOrder: sortOrder)) } // Custom items for item in day.items { if case .customItem = item { orderableItems.append(item) } } // 3. Sort all orderable items by sortOrder orderableItems.sort { ($0.sortOrder ?? 0) < ($1.sortOrder ?? 0) } // 4. Append sorted items flatItems.append(contentsOf: orderableItems) } tableView.reloadData() } ``` **Step 2: Build to verify** Run: `cd /Users/treyt/Desktop/code/SportsTime && xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | tail -20` **Step 3: Commit** ```bash git add SportsTime/Features/Trip/Views/ItineraryTableViewController.swift git commit -m "feat(controller): sort all orderable items together by sortOrder" ``` --- ## Task 7: Update Drag Constraints for Custom Items **Files:** - Modify: `SportsTime/Features/Trip/Views/ItineraryTableViewController.swift` **Step 1: Update targetIndexPathForMoveFromRowAt for custom items** Remove the constraints that prevent custom items from going before games/travel. In the `.customItem` case: ```swift case .customItem: // Custom items can go anywhere except: // 1. Before position 0 // 2. Directly ON a day header (redirect to after header) // Don't drop ON a day header - go after it instead if proposedRow < flatItems.count, case .dayHeader = flatItems[proposedRow] { return IndexPath(row: proposedRow + 1, section: 0) } // Allow dropping anywhere else (before games, before travel, etc.) return IndexPath(row: proposedRow, section: 0) ``` **Step 2: Update calculateSortOrder to consider all orderable items** Update `calculateSortOrder(at:)` to scan for games and travel, not just custom items: ```swift private func calculateSortOrder(at row: Int) -> Double { var prevSortOrder: Double? var nextSortOrder: Double? // SCAN BACKWARDS to find previous orderable item in this day for i in stride(from: row - 1, through: 0, by: -1) { switch flatItems[i] { case .dayHeader: // Hit day boundary - no previous item in this day break case .games(_, _, let order): prevSortOrder = order case .travel(_, _, let order): prevSortOrder = order case .customItem(let item): prevSortOrder = item.sortOrder } if prevSortOrder != nil { break } if case .dayHeader = flatItems[i] { break } } // SCAN FORWARDS to find next orderable item in this day for i in row.. Void)? // travelId, newDay, newSortOrder ``` **Step 2: Update moveRowAt to pass sortOrder** In `tableView(_:moveRowAt:to:)`: ```swift case .travel(let segment, _): let newDay = dayForTravelAt(row: destinationIndexPath.row) let sortOrder = calculateSortOrder(at: destinationIndexPath.row) let travelId = "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())" onTravelMoved?(travelId, newDay, sortOrder) ``` **Step 3: Commit** ```bash git add SportsTime/Features/Trip/Views/ItineraryTableViewController.swift git commit -m "feat(controller): include sortOrder in travel move callback" ``` --- ## Task 9: Update ItineraryTableViewWrapper Callback **Files:** - Modify: `SportsTime/Features/Trip/Views/ItineraryTableViewWrapper.swift` **Step 1: Update onTravelMoved callback type** ```swift var onTravelMoved: ((String, Int, Double) -> Void)? // travelId, newDay, newSortOrder ``` **Step 2: Update makeUIViewController** ```swift controller.onTravelMoved = onTravelMoved ``` **Step 3: Commit** ```bash git add SportsTime/Features/Trip/Views/ItineraryTableViewWrapper.swift git commit -m "feat(wrapper): update travel move callback for sortOrder" ``` --- ## Task 10: Update TripDetailView State and Handlers **Files:** - Modify: `SportsTime/Features/Trip/Views/TripDetailView.swift` **Step 1: Update state type** ```swift @State private var travelDayOverrides: [String: TravelDayOverride] = [:] ``` **Step 2: Update onTravelMoved handler** ```swift onTravelMoved: { travelId, newDay, newSortOrder in Task { @MainActor in // Create or update the override let override = TravelDayOverride( id: travelDayOverrides[travelId]?.id ?? UUID(), tripId: trip.id, travelAnchorId: travelId, displayDay: newDay, sortOrder: newSortOrder, createdAt: travelDayOverrides[travelId]?.createdAt ?? Date(), modifiedAt: Date() ) withAnimation { travelDayOverrides[travelId] = override } await saveTravelDayOverride(override) } } ``` **Step 3: Update saveTravelDayOverride function** Change signature and implementation: ```swift private func saveTravelDayOverride(_ override: TravelDayOverride) async { do { _ = try await TravelOverrideService.shared.saveOverride(override) print("✅ [TravelOverride] Saved: \(override.travelAnchorId) -> day \(override.displayDay), sortOrder \(override.sortOrder)") } catch { print("❌ [TravelOverride] Failed to save: \(error)") } } ``` **Step 4: Update loadCustomItems to use new return type** ```swift let overrides = try await TravelOverrideService.shared.fetchOverridesAsDictionary(forTripId: trip.id) print("✅ [TravelOverrides] Loaded \(overrides.count) travel day overrides") travelDayOverrides = overrides ``` **Step 5: Build and fix any remaining compilation errors** Run: `cd /Users/treyt/Desktop/code/SportsTime && xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | tail -30` **Step 6: Commit** ```bash git add SportsTime/Features/Trip/Views/TripDetailView.swift git commit -m "feat(tripdetail): update state and handlers for travel sortOrder" ``` --- ## Task 11: Update Cell Configuration for Games **Files:** - Modify: `SportsTime/Features/Trip/Views/ItineraryTableViewController.swift` **Step 1: Update cellForRowAt for games case** ```swift case .games(let games, _, _): let cell = tableView.dequeueReusableCell(withIdentifier: gamesCellId, for: indexPath) configureGamesCell(cell, games: games) return cell ``` **Step 2: Update travel case** ```swift case .travel(let segment, _, _): let cell = tableView.dequeueReusableCell(withIdentifier: travelCellId, for: indexPath) configureTravelCell(cell, segment: segment) return cell ``` **Step 3: Build to verify** Run: `cd /Users/treyt/Desktop/code/SportsTime && xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | tail -20` **Step 4: Commit** ```bash git add SportsTime/Features/Trip/Views/ItineraryTableViewController.swift git commit -m "fix(controller): update cell configuration for new enum cases" ``` --- ## Task 12: Full Build and Manual Test **Step 1: Full clean build** Run: `cd /Users/treyt/Desktop/code/SportsTime && xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' clean build 2>&1 | tail -30` Expected: BUILD SUCCEEDED **Step 2: Run tests** Run: `cd /Users/treyt/Desktop/code/SportsTime && xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test 2>&1 | tail -50` Expected: All tests pass **Step 3: Manual verification checklist** - [ ] Open app, navigate to a trip with games and travel - [ ] Verify games appear with no drag handle - [ ] Verify custom items have drag handle - [ ] Verify travel has drag handle - [ ] Drag custom item BEFORE a game - should work - [ ] Drag custom item AFTER a game - should work - [ ] Drag travel to different position within same day - should work - [ ] Drag travel to different valid day - should work - [ ] Drag travel to invalid day - should snap to nearest valid **Step 4: Commit** ```bash git add -A git commit -m "feat: complete flexible itinerary ordering implementation" ``` --- ## Task 13: Write Regression Tests **Files:** - Create: `SportsTimeTests/Features/ItineraryOrderingTests.swift` **Step 1: Create test file with ordering tests** ```swift import XCTest @testable import SportsTime final class ItineraryOrderingTests: XCTestCase { // MARK: - sortOrder Calculation Tests func test_calculateSortOrder_betweenTwoItems_returnsMidpoint() { // Given items at sortOrder 1.0 and 3.0 // When calculating sortOrder for position between them // Then should return 2.0 } func test_calculateSortOrder_beforeFirstItem_returnsHalf() { // Given first item at sortOrder 2.0 // When calculating sortOrder for position before it // Then should return 1.0 (half of 2.0) } func test_calculateSortOrder_afterLastItem_returnsLastPlusOne() { // Given last item at sortOrder 5.0 // When calculating sortOrder for position after it // Then should return 6.0 } func test_calculateSortOrder_emptyDay_returnsOne() { // Given day with no orderable items // When calculating sortOrder // Then should return 1.0 } // MARK: - Travel Constraint Tests func test_travelConstraint_cannotMoveBefore DepartureGame() { // Given travel from City A to City B // And City A has game on Day 2 // When attempting to move travel to Day 1 // Then travel should snap to Day 3 (minimum valid) } func test_travelConstraint_canMoveToArrivalGameDay() { // Given travel from City A to City B // And City B has game on Day 5 // When moving travel to Day 5 // Then should be allowed (arrive morning of game) } // MARK: - Custom Item Flexibility Tests func test_customItem_canBePositionedBeforeGame() { // Given a day with a game at sortOrder 100 // When custom item dropped before game // Then custom item should get sortOrder < 100 } func test_customItem_canBePositionedBetweenTravelAndGame() { // Given travel at sortOrder 50, game at sortOrder 100 // When custom item dropped between them // Then custom item should get sortOrder ~75 } } ``` **Step 2: Run tests** Run: `cd /Users/treyt/Desktop/code/SportsTime && xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/ItineraryOrderingTests test 2>&1 | tail -30` **Step 3: Commit** ```bash git add SportsTimeTests/Features/ItineraryOrderingTests.swift git commit -m "test: add regression tests for flexible itinerary ordering" ``` --- ## Summary This plan implements flexible itinerary ordering in 13 tasks: 1. **Tasks 1-3:** Update data models (domain, CloudKit, service) to support sortOrder on travel 2. **Tasks 4-6:** Update wrapper and controller interfaces for new data structure 3. **Tasks 7-9:** Update drag constraints and callbacks 4. **Task 10:** Update TripDetailView state management 5. **Tasks 11-12:** Fix compilation, full build, manual test 6. **Task 13:** Write regression tests Key changes: - `TravelDayOverride` gains `sortOrder: Double` - Games get auto-assigned sortOrder (100, 101, 102...) - All orderable items (games, travel, custom) sort together within each day - Custom items can now go anywhere (before/after games) - Travel remains constrained to valid day range but can be positioned within a day