# Itinerary Refactor Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Replace anchor-based positioning with simple sort-order positioning for custom itinerary items, enabling intuitive drag-and-drop without inference bugs. **Architecture:** Remove `anchorType` and `anchorId` from `CustomItineraryItem`, replacing them with `day: Int` (already exists as `anchorDay`) and `sortOrder: Double`. Update all CloudKit, UI, and routing code to use the new model. Travel constraints remain enforced via valid ranges. **Tech Stack:** Swift, SwiftUI, UIKit (UITableViewController), CloudKit, Swift Testing --- ## Task 1: Update CustomItineraryItem Model **Files:** - Modify: `SportsTime/Core/Models/Domain/CustomItineraryItem.swift` - Modify: `SportsTimeTests/Domain/CustomItineraryItemTests.swift` **Step 1: Write failing test for new model structure** Add to `CustomItineraryItemTests.swift`: ```swift @Test("Item initializes with day and sortOrder (no anchors)") func item_InitializesWithDayAndSortOrder() { let tripId = UUID() let item = CustomItineraryItem( tripId: tripId, category: .restaurant, title: "Joe's BBQ", day: 1, sortOrder: 1.5 ) #expect(item.tripId == tripId) #expect(item.category == .restaurant) #expect(item.title == "Joe's BBQ") #expect(item.day == 1) #expect(item.sortOrder == 1.5) } @Test("SortOrder defaults to 0.0") func sortOrder_DefaultsToZero() { let item = CustomItineraryItem( tripId: UUID(), category: .activity, title: "City Tour", day: 2 ) #expect(item.sortOrder == 0.0) } ``` **Step 2: Run test to verify it fails** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/CustomItineraryItemTests test` Expected: FAIL - initializer doesn't match **Step 3: Update CustomItineraryItem model** Replace `SportsTime/Core/Models/Domain/CustomItineraryItem.swift`: ```swift // // CustomItineraryItem.swift // SportsTime // import Foundation import CoreLocation struct CustomItineraryItem: Identifiable, Codable, Hashable { let id: UUID let tripId: UUID var category: ItemCategory var title: String var day: Int // Day number (1-indexed) var sortOrder: Double // Position within day (allows insertion between items) let createdAt: Date var modifiedAt: Date // Optional location for mappable items (from MapKit search) var latitude: Double? var longitude: Double? var address: String? /// Whether this item has a location and can be shown on the map var isMappable: Bool { latitude != nil && longitude != nil } /// Get coordinate if mappable var coordinate: CLLocationCoordinate2D? { guard let lat = latitude, let lon = longitude else { return nil } return CLLocationCoordinate2D(latitude: lat, longitude: lon) } init( id: UUID = UUID(), tripId: UUID, category: ItemCategory, title: String, day: Int, sortOrder: Double = 0.0, createdAt: Date = Date(), modifiedAt: Date = Date(), latitude: Double? = nil, longitude: Double? = nil, address: String? = nil ) { self.id = id self.tripId = tripId self.category = category self.title = title self.day = day self.sortOrder = sortOrder self.createdAt = createdAt self.modifiedAt = modifiedAt self.latitude = latitude self.longitude = longitude self.address = address } enum ItemCategory: String, Codable, CaseIterable { case restaurant case hotel case activity case note var icon: String { switch self { case .restaurant: return "🍽️" case .hotel: return "🏨" case .activity: return "🎯" case .note: return "📝" } } var label: String { switch self { case .restaurant: return "Restaurant" case .hotel: return "Hotel" case .activity: return "Activity" case .note: return "Note" } } var systemImage: String { switch self { case .restaurant: return "fork.knife" case .hotel: return "bed.double.fill" case .activity: return "figure.run" case .note: return "note.text" } } } } ``` **Step 4: Update existing tests to use new model** Replace `SportsTimeTests/Domain/CustomItineraryItemTests.swift`: ```swift // // CustomItineraryItemTests.swift // SportsTimeTests // import Testing @testable import SportsTime import Foundation struct CustomItineraryItemTests { @Test("Item initializes with default values") func item_InitializesWithDefaults() { let tripId = UUID() let item = CustomItineraryItem( tripId: tripId, category: .restaurant, title: "Joe's BBQ", day: 1 ) #expect(item.tripId == tripId) #expect(item.category == .restaurant) #expect(item.title == "Joe's BBQ") #expect(item.day == 1) #expect(item.sortOrder == 0.0) } @Test("Item initializes with day and sortOrder") func item_InitializesWithDayAndSortOrder() { let tripId = UUID() let item = CustomItineraryItem( tripId: tripId, category: .restaurant, title: "Joe's BBQ", day: 1, sortOrder: 1.5 ) #expect(item.tripId == tripId) #expect(item.category == .restaurant) #expect(item.title == "Joe's BBQ") #expect(item.day == 1) #expect(item.sortOrder == 1.5) } @Test("SortOrder defaults to 0.0") func sortOrder_DefaultsToZero() { let item = CustomItineraryItem( tripId: UUID(), category: .activity, title: "City Tour", day: 2 ) #expect(item.sortOrder == 0.0) } @Test("Item category has correct icons") func category_HasCorrectIcons() { #expect(CustomItineraryItem.ItemCategory.restaurant.icon == "🍽️") #expect(CustomItineraryItem.ItemCategory.hotel.icon == "🏨") #expect(CustomItineraryItem.ItemCategory.activity.icon == "🎯") #expect(CustomItineraryItem.ItemCategory.note.icon == "📝") } @Test("Item is Codable") func item_IsCodable() throws { let item = CustomItineraryItem( tripId: UUID(), category: .hotel, title: "Hilton Downtown", day: 2, sortOrder: 3.5 ) let encoded = try JSONEncoder().encode(item) let decoded = try JSONDecoder().decode(CustomItineraryItem.self, from: encoded) #expect(decoded.id == item.id) #expect(decoded.title == item.title) #expect(decoded.day == 2) #expect(decoded.sortOrder == 3.5) } } ``` **Step 5: Run tests to verify they pass** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/CustomItineraryItemTests test` Expected: PASS **Step 6: Commit** ```bash git add SportsTime/Core/Models/Domain/CustomItineraryItem.swift SportsTimeTests/Domain/CustomItineraryItemTests.swift git commit -m "refactor(model): replace anchors with day+sortOrder in CustomItineraryItem BREAKING: Remove anchorType, anchorId, anchorDay Add: day (Int), sortOrder (Double) Co-Authored-By: Claude Opus 4.5 " ``` --- ## Task 2: Update CloudKit Model (CKCustomItineraryItem) **Files:** - Modify: `SportsTime/Core/Models/CloudKit/CKModels.swift` (lines 628-713) **Step 1: Update CKCustomItineraryItem field keys and conversion** In `CKModels.swift`, replace the `CKCustomItineraryItem` struct (around line 628): ```swift // MARK: - CKCustomItineraryItem struct CKCustomItineraryItem { static let itemIdKey = "itemId" static let tripIdKey = "tripId" static let categoryKey = "category" static let titleKey = "title" static let dayKey = "day" // NEW: replaces anchorDay static let sortOrderDoubleKey = "sortOrderDouble" // NEW: Double instead of Int static let createdAtKey = "createdAt" static let modifiedAtKey = "modifiedAt" // Location fields for mappable items static let latitudeKey = "latitude" static let longitudeKey = "longitude" static let addressKey = "address" // DEPRECATED - kept for migration reads only static let anchorTypeKey = "anchorType" static let anchorIdKey = "anchorId" static let anchorDayKey = "anchorDay" static let sortOrderKey = "sortOrder" let record: CKRecord init(record: CKRecord) { self.record = record } init(item: CustomItineraryItem) { let record = CKRecord( recordType: CKRecordType.customItineraryItem, recordID: CKRecord.ID(recordName: item.id.uuidString) ) record[CKCustomItineraryItem.itemIdKey] = item.id.uuidString record[CKCustomItineraryItem.tripIdKey] = item.tripId.uuidString record[CKCustomItineraryItem.categoryKey] = item.category.rawValue record[CKCustomItineraryItem.titleKey] = item.title record[CKCustomItineraryItem.dayKey] = item.day record[CKCustomItineraryItem.sortOrderDoubleKey] = item.sortOrder record[CKCustomItineraryItem.createdAtKey] = item.createdAt record[CKCustomItineraryItem.modifiedAtKey] = item.modifiedAt // Location fields (nil values are not stored in CloudKit) record[CKCustomItineraryItem.latitudeKey] = item.latitude record[CKCustomItineraryItem.longitudeKey] = item.longitude record[CKCustomItineraryItem.addressKey] = item.address self.record = record } func toItem() -> CustomItineraryItem? { guard let itemIdString = record[CKCustomItineraryItem.itemIdKey] as? String, let itemId = UUID(uuidString: itemIdString), let tripIdString = record[CKCustomItineraryItem.tripIdKey] as? String, let tripId = UUID(uuidString: tripIdString), let categoryString = record[CKCustomItineraryItem.categoryKey] as? String, let category = CustomItineraryItem.ItemCategory(rawValue: categoryString), let title = record[CKCustomItineraryItem.titleKey] as? String, let createdAt = record[CKCustomItineraryItem.createdAtKey] as? Date, let modifiedAt = record[CKCustomItineraryItem.modifiedAtKey] as? Date else { return nil } // Read new fields, with migration fallback from old fields let day: Int if let newDay = record[CKCustomItineraryItem.dayKey] as? Int { day = newDay } else if let oldDay = record[CKCustomItineraryItem.anchorDayKey] as? Int { // Migration: use old anchorDay day = oldDay } else { return nil } let sortOrder: Double if let newSortOrder = record[CKCustomItineraryItem.sortOrderDoubleKey] as? Double { sortOrder = newSortOrder } else if let oldSortOrder = record[CKCustomItineraryItem.sortOrderKey] as? Int { // Migration: convert old Int sortOrder to Double sortOrder = Double(oldSortOrder) } else { sortOrder = 0.0 } // Location fields (optional - nil if not stored) let latitude = record[CKCustomItineraryItem.latitudeKey] as? Double let longitude = record[CKCustomItineraryItem.longitudeKey] as? Double let address = record[CKCustomItineraryItem.addressKey] as? String return CustomItineraryItem( id: itemId, tripId: tripId, category: category, title: title, day: day, sortOrder: sortOrder, createdAt: createdAt, modifiedAt: modifiedAt, latitude: latitude, longitude: longitude, address: address ) } } ``` **Step 2: Run build to verify compilation** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` Expected: Build errors in files that reference old anchor fields **Step 3: Commit (partial - model layer)** ```bash git add SportsTime/Core/Models/CloudKit/CKModels.swift git commit -m "refactor(cloudkit): update CKCustomItineraryItem for day+sortOrder Add migration fallback for existing records with old anchor fields. Co-Authored-By: Claude Opus 4.5 " ``` --- ## Task 3: Update CustomItemService **Files:** - Modify: `SportsTime/Core/Services/CustomItemService.swift` **Step 1: Update field references in CustomItemService** Replace the update methods in `CustomItemService.swift`: ```swift func updateItem(_ item: CustomItineraryItem) async throws -> CustomItineraryItem { // Fetch existing record to get changeTag let recordID = CKRecord.ID(recordName: item.id.uuidString) let existingRecord: CKRecord do { existingRecord = try await publicDatabase.record(for: recordID) } catch let error as CKError { throw mapCloudKitError(error) } catch { throw CustomItemError.unknown(error) } // Update fields let now = Date() existingRecord[CKCustomItineraryItem.categoryKey] = item.category.rawValue existingRecord[CKCustomItineraryItem.titleKey] = item.title existingRecord[CKCustomItineraryItem.dayKey] = item.day existingRecord[CKCustomItineraryItem.sortOrderDoubleKey] = item.sortOrder existingRecord[CKCustomItineraryItem.modifiedAtKey] = now // Location fields (nil values clear the field in CloudKit) existingRecord[CKCustomItineraryItem.latitudeKey] = item.latitude existingRecord[CKCustomItineraryItem.longitudeKey] = item.longitude existingRecord[CKCustomItineraryItem.addressKey] = item.address do { try await publicDatabase.save(existingRecord) var updatedItem = item updatedItem.modifiedAt = now return updatedItem } catch let error as CKError { throw mapCloudKitError(error) } catch { throw CustomItemError.unknown(error) } } /// Batch update sortOrder for multiple items (for reordering) func updateSortOrders(_ items: [CustomItineraryItem]) async throws { guard !items.isEmpty else { return } print("☁️ [CloudKit] Batch updating day+sortOrder for \(items.count) items") // Fetch all records let recordIDs = items.map { CKRecord.ID(recordName: $0.id.uuidString) } let fetchResults = try await publicDatabase.records(for: recordIDs) // Update each record's day and sortOrder var recordsToSave: [CKRecord] = [] let now = Date() for item in items { let recordID = CKRecord.ID(recordName: item.id.uuidString) guard case .success(let record) = fetchResults[recordID] else { continue } record[CKCustomItineraryItem.dayKey] = item.day record[CKCustomItineraryItem.sortOrderDoubleKey] = item.sortOrder record[CKCustomItineraryItem.modifiedAtKey] = now recordsToSave.append(record) } // Save all in one batch operation let modifyOp = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: nil) modifyOp.savePolicy = .changedKeys try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in modifyOp.modifyRecordsResultBlock = { result in switch result { case .success: print("☁️ [CloudKit] Batch day+sortOrder update complete") continuation.resume() case .failure(let error): print("☁️ [CloudKit] Batch update failed: \(error)") continuation.resume(throwing: error) } } publicDatabase.add(modifyOp) } } ``` **Step 2: Run build to verify** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` Expected: More build errors in UI files (expected at this stage) **Step 3: Commit** ```bash git add SportsTime/Core/Services/CustomItemService.swift git commit -m "refactor(service): update CustomItemService for day+sortOrder Co-Authored-By: Claude Opus 4.5 " ``` --- ## Task 4: Update ItineraryRowItem Enum **Files:** - Modify: `SportsTime/Features/Trip/Views/ItineraryTableViewController.swift` (lines 26-57) **Step 1: Simplify ItineraryRowItem - remove anchor parameters from addButton** Update the enum at the top of `ItineraryTableViewController.swift`: ```swift /// Represents a row item in the itinerary enum ItineraryRowItem: Identifiable, Equatable { case dayHeader(dayNumber: Int, date: Date, games: [RichGame]) case travel(TravelSegment, dayNumber: Int) case customItem(CustomItineraryItem) case addButton(day: Int) // Simplified - just needs day var id: String { switch self { case .dayHeader(let dayNumber, _, _): return "day:\(dayNumber)" case .travel(let segment, _): return "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())" case .customItem(let item): return "item:\(item.id.uuidString)" case .addButton(let day): return "add:\(day)" } } var isReorderable: Bool { switch self { case .dayHeader, .addButton: return false case .travel, .customItem: return true } } static func == (lhs: ItineraryRowItem, rhs: ItineraryRowItem) -> Bool { lhs.id == rhs.id } } ``` **Step 2: Update callbacks in ItineraryTableViewController** Change the callback signatures (around line 70): ```swift // Callbacks var onTravelMoved: ((String, Int) -> Void)? // travelId, newDay var onCustomItemMoved: ((UUID, Int, Double) -> Void)? // itemId, newDay, newSortOrder var onCustomItemTapped: ((CustomItineraryItem) -> Void)? var onCustomItemDeleted: ((CustomItineraryItem) -> Void)? var onAddButtonTapped: ((Int) -> Void)? // Just day number ``` **Step 3: Run build to find remaining errors** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | head -100` Expected: Build errors - note which files need updates **Step 4: Commit (partial)** ```bash git add SportsTime/Features/Trip/Views/ItineraryTableViewController.swift git commit -m "refactor(ui): simplify ItineraryRowItem callbacks for day+sortOrder WIP: Will fix remaining compilation errors in next tasks Co-Authored-By: Claude Opus 4.5 " ``` --- ## Task 5: Update ItineraryTableViewController Move Logic **Files:** - Modify: `SportsTime/Features/Trip/Views/ItineraryTableViewController.swift` **Step 1: Replace determineAnchor with calculateSortOrder** Remove `determineAnchor` function and add new sort order calculation (around line 438): ```swift // MARK: - Sort Order Calculation /// Calculate sort order for item dropped at a given row within its day private func calculateSortOrder(droppedAtRow row: Int, inDay targetDay: Int) -> Double { // Find all custom items currently in the target day let itemsInDay = flatItems.enumerated().compactMap { (index, item) -> (index: Int, sortOrder: Double)? in guard case .customItem(let custom) = item, custom.day == targetDay else { return nil } return (index, custom.sortOrder) } // If no items in this day, start at 1.0 guard !itemsInDay.isEmpty else { return 1.0 } // Find where the drop position falls relative to existing items let itemsBefore = itemsInDay.filter { $0.index < row } let itemsAfter = itemsInDay.filter { $0.index >= row } if itemsBefore.isEmpty { // Dropping before all items - use half of first item's sortOrder let firstOrder = itemsInDay.first?.sortOrder ?? 1.0 return firstOrder / 2.0 } if itemsAfter.isEmpty { // Dropping after all items - use last + 1 let lastOrder = itemsBefore.last?.sortOrder ?? 0.0 return lastOrder + 1.0 } // Dropping between items - use midpoint let beforeOrder = itemsBefore.last?.sortOrder ?? 0.0 let afterOrder = itemsAfter.first?.sortOrder ?? (beforeOrder + 2.0) return (beforeOrder + afterOrder) / 2.0 } ``` **Step 2: Update moveRowAt to use new callback** Replace the move handling in `tableView(_:moveRowAt:to:)` (around line 250): ```swift override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { let item = flatItems[sourceIndexPath.row] // Remove from source flatItems.remove(at: sourceIndexPath.row) // Insert at destination flatItems.insert(item, at: destinationIndexPath.row) // Notify callbacks switch item { case .travel(let segment, _): // Find which day this travel is now associated with let newDay = dayForTravelAt(row: destinationIndexPath.row) let travelId = "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())" onTravelMoved?(travelId, newDay) case .customItem(let customItem): let destinationDay = dayNumber(forRow: destinationIndexPath.row) let newSortOrder = calculateSortOrder(droppedAtRow: destinationIndexPath.row, inDay: destinationDay) onCustomItemMoved?(customItem.id, destinationDay, newSortOrder) default: break } } ``` **Step 3: Update cell configuration for addButton** Update `configureAddButtonCell` (around line 522): ```swift private func configureAddButtonCell(_ cell: UITableViewCell, day: Int) { cell.contentConfiguration = UIHostingConfiguration { AddButtonRowView(colorScheme: colorScheme) } .margins(.all, 0) .background(.clear) cell.backgroundColor = .clear cell.selectionStyle = .default } ``` **Step 4: Update cellForRowAt for addButton case** Update the switch case (around line 237): ```swift case .addButton(let day): let cell = tableView.dequeueReusableCell(withIdentifier: addButtonCellId, for: indexPath) configureAddButtonCell(cell, day: day) return cell ``` **Step 5: Update didSelectRowAt for addButton case** Update the selection handling (around line 406): ```swift case .addButton(let day): onAddButtonTapped?(day) ``` **Step 6: Run build** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | head -100` **Step 7: Commit** ```bash git add SportsTime/Features/Trip/Views/ItineraryTableViewController.swift git commit -m "refactor(ui): replace anchor inference with direct sortOrder calculation - Remove determineAnchor function - Add calculateSortOrder for direct position computation - Simplify addButton to just use day number Co-Authored-By: Claude Opus 4.5 " ``` --- ## Task 6: Update ItineraryTableViewWrapper **Files:** - Modify: `SportsTime/Features/Trip/Views/ItineraryTableViewWrapper.swift` **Step 1: Update callback signatures** Update the callback types (around line 20-24): ```swift // Callbacks var onTravelMoved: ((String, Int) -> Void)? var onCustomItemMoved: ((UUID, Int, Double) -> Void)? // itemId, day, sortOrder var onCustomItemTapped: ((CustomItineraryItem) -> Void)? var onCustomItemDeleted: ((CustomItineraryItem) -> Void)? var onAddButtonTapped: ((Int) -> Void)? // Just day ``` **Step 2: Update init parameters** Update the initializer (around line 26-48): ```swift init( trip: Trip, games: [RichGame], customItems: [CustomItineraryItem], travelDayOverrides: [String: Int], @ViewBuilder headerContent: () -> HeaderContent, onTravelMoved: ((String, Int) -> Void)? = nil, onCustomItemMoved: ((UUID, Int, Double) -> Void)? = nil, onCustomItemTapped: ((CustomItineraryItem) -> Void)? = nil, onCustomItemDeleted: ((CustomItineraryItem) -> Void)? = nil, onAddButtonTapped: ((Int) -> Void)? = nil ) { self.trip = trip self.games = games self.customItems = customItems self.travelDayOverrides = travelDayOverrides self.headerContent = headerContent() self.onTravelMoved = onTravelMoved self.onCustomItemMoved = onCustomItemMoved self.onCustomItemTapped = onCustomItemTapped self.onCustomItemDeleted = onCustomItemDeleted self.onAddButtonTapped = onAddButtonTapped } ``` **Step 3: Update buildItineraryData to use new model** Replace the custom item handling in `buildItineraryData()` (around line 163-202): ```swift // Custom items for this day (sorted by sortOrder) let itemsForDay = customItems.filter { $0.day == dayNum } .sorted { $0.sortOrder < $1.sortOrder } for item in itemsForDay { items.append(ItineraryRowItem.customItem(item)) } // ONE Add button per day - at the end items.append(ItineraryRowItem.addButton(day: dayNum)) ``` **Step 4: Run build** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | head -100` **Step 5: Commit** ```bash git add SportsTime/Features/Trip/Views/ItineraryTableViewWrapper.swift git commit -m "refactor(ui): update ItineraryTableViewWrapper for day+sortOrder - Simplify custom item grouping (just filter by day, sort by sortOrder) - Remove anchor-based item placement logic Co-Authored-By: Claude Opus 4.5 " ``` --- ## Task 7: Update TripDetailView Callbacks **Files:** - Modify: `SportsTime/Features/Trip/Views/TripDetailView.swift` **Step 1: Update onCustomItemMoved callback** Find the `ItineraryTableViewWrapper` usage (around line 186-242) and update: ```swift onCustomItemMoved: { itemId, day, sortOrder in Task { @MainActor in guard let item = customItems.first(where: { $0.id == itemId }) else { return } await moveItem(item, toDay: day, sortOrder: sortOrder) } }, ``` **Step 2: Update onAddButtonTapped callback** ```swift onAddButtonTapped: { day in addItemAnchor = AddItemAnchor(day: day) } ``` **Step 3: Update AddItemAnchor struct** Find the `AddItemAnchor` struct and simplify it: ```swift struct AddItemAnchor: Identifiable { let id = UUID() let day: Int } ``` **Step 4: Add new moveItem function** Add a new simplified move function: ```swift private func moveItem(_ item: CustomItineraryItem, toDay day: Int, sortOrder: Double) async { print("📦 [Move] Moving '\(item.title)' to day \(day), sortOrder \(sortOrder)") // Update local state immediately var updated = item updated.day = day updated.sortOrder = sortOrder updated.modifiedAt = Date() if let index = customItems.firstIndex(where: { $0.id == item.id }) { customItems[index] = updated } // Persist to CloudKit do { _ = try await CustomItemService.shared.updateItem(updated) print("✅ [Move] Saved to CloudKit") } catch { print("❌ [Move] CloudKit error: \(error)") } } ``` **Step 5: Remove old moveItemToBeginning function** Delete the old `moveItemToBeginning` function that used anchors. **Step 6: Run build** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | head -100` **Step 7: Commit** ```bash git add SportsTime/Features/Trip/Views/TripDetailView.swift git commit -m "refactor(ui): update TripDetailView for day+sortOrder callbacks - Simplify AddItemAnchor to just day - Replace moveItemToBeginning with direct moveItem - Update callback handlers Co-Authored-By: Claude Opus 4.5 " ``` --- ## Task 8: Update AddItemSheet **Files:** - Modify: `SportsTime/Features/Trip/Views/AddItemSheet.swift` **Step 1: Simplify AddItemSheet parameters** Update the struct properties and initializer (around line 11-48): ```swift struct AddItemSheet: View { @Environment(\.dismiss) private var dismiss @Environment(\.colorScheme) private var colorScheme let tripId: UUID let day: Int let existingItem: CustomItineraryItem? var onSave: (CustomItineraryItem) -> Void // Entry mode enum EntryMode: String, CaseIterable { case searchPlaces = "Search Places" case custom = "Custom" } @State private var entryMode: EntryMode = .searchPlaces @State private var selectedCategory: CustomItineraryItem.ItemCategory = .restaurant @State private var title: String = "" @State private var isSaving = false // MapKit search state @State private var searchQuery = "" @State private var searchResults: [MKMapItem] = [] @State private var selectedPlace: MKMapItem? @State private var isSearching = false private var isEditing: Bool { existingItem != nil } ``` **Step 2: Update saveItem function** Replace the `saveItem()` function (around line 247): ```swift private func saveItem() { isSaving = true let item: CustomItineraryItem if let existing = existingItem { // Editing existing item - preserve day and sortOrder let trimmedTitle = title.trimmingCharacters(in: .whitespaces) guard !trimmedTitle.isEmpty else { return } item = CustomItineraryItem( id: existing.id, tripId: existing.tripId, category: selectedCategory, title: trimmedTitle, day: existing.day, sortOrder: existing.sortOrder, createdAt: existing.createdAt, modifiedAt: Date(), latitude: existing.latitude, longitude: existing.longitude, address: existing.address ) } else if entryMode == .searchPlaces, let place = selectedPlace { // Creating from MapKit search let placeName = place.name ?? "Unknown Place" let coordinate = place.placemark.coordinate item = CustomItineraryItem( tripId: tripId, category: selectedCategory, title: placeName, day: day, sortOrder: Double(Date().timeIntervalSince1970), // Use timestamp for unique sortOrder latitude: coordinate.latitude, longitude: coordinate.longitude, address: formatAddress(for: place) ) } else { // Creating custom item (no location) let trimmedTitle = title.trimmingCharacters(in: .whitespaces) guard !trimmedTitle.isEmpty else { return } item = CustomItineraryItem( tripId: tripId, category: selectedCategory, title: trimmedTitle, day: day, sortOrder: Double(Date().timeIntervalSince1970) ) } onSave(item) dismiss() } ``` **Step 3: Update onAppear** ```swift .onAppear { if let existing = existingItem { selectedCategory = existing.category title = existing.title entryMode = .custom } } ``` **Step 4: Update Preview** ```swift #Preview { AddItemSheet( tripId: UUID(), day: 1, existingItem: nil ) { _ in } } ``` **Step 5: Run build** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build 2>&1 | head -100` **Step 6: Commit** ```bash git add SportsTime/Features/Trip/Views/AddItemSheet.swift git commit -m "refactor(ui): simplify AddItemSheet to use day only - Remove anchorType and anchorId parameters - Use timestamp for initial sortOrder of new items Co-Authored-By: Claude Opus 4.5 " ``` --- ## Task 9: Update TripDetailView Sheet Presentation **Files:** - Modify: `SportsTime/Features/Trip/Views/TripDetailView.swift` **Step 1: Update sheet bindings for AddItemSheet** Find the `.sheet(item: $addItemAnchor)` (around line 119) and update: ```swift .sheet(item: $addItemAnchor) { anchor in AddItemSheet( tripId: trip.id, day: anchor.day, existingItem: nil ) { item in Task { await saveCustomItem(item) } } } .sheet(item: $editingItem) { item in AddItemSheet( tripId: trip.id, day: item.day, existingItem: item ) { updatedItem in Task { await saveCustomItem(updatedItem) } } } ``` **Step 2: Run build** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` Expected: PASS **Step 3: Commit** ```bash git add SportsTime/Features/Trip/Views/TripDetailView.swift git commit -m "refactor(ui): update TripDetailView sheet bindings for simplified AddItemSheet Co-Authored-By: Claude Opus 4.5 " ``` --- ## Task 10: Update routeWaypoints for Simplified Model **Files:** - Modify: `SportsTime/Features/Trip/Views/TripDetailView.swift` **Step 1: Simplify routeWaypoints computed property** Replace the `routeWaypoints` property (around line 1154): ```swift /// Route waypoints including both game stops and mappable custom items in itinerary order private var routeWaypoints: [(name: String, coordinate: CLLocationCoordinate2D, isCustomItem: Bool)] { var waypoints: [(name: String, coordinate: CLLocationCoordinate2D, isCustomItem: Bool)] = [] let days = tripDays // Group custom items by day, sorted by sortOrder let itemsByDay = Dictionary(grouping: mappableCustomItems) { $0.day } for (dayIndex, dayDate) in days.enumerated() { let dayNumber = dayIndex + 1 // Find games on this day let gamesOnDay = gamesOn(date: dayDate) let calendar = Calendar.current let dayCity = gamesOnDay.first?.stadium.city ?? trip.stops.first(where: { stop in let arrival = calendar.startOfDay(for: stop.arrivalDate) let departure = calendar.startOfDay(for: stop.departureDate) let day = calendar.startOfDay(for: dayDate) return day >= arrival && day <= departure })?.city // Game stop for this day (only add once per city) if let city = dayCity { let alreadyHasCity = waypoints.contains(where: { wp in if wp.isCustomItem { return false } if wp.name == city { return true } if let stop = trip.stops.first(where: { $0.city == city }), let stadiumId = stop.stadium, let stadium = dataProvider.stadium(for: stadiumId), wp.name == stadium.name { return true } return false }) if !alreadyHasCity { if let stop = trip.stops.first(where: { $0.city == city }) { if let stadiumId = stop.stadium, let stadium = dataProvider.stadium(for: stadiumId) { waypoints.append((stadium.name, stadium.coordinate, false)) } else if let coord = stop.coordinate { waypoints.append((city, coord, false)) } } } } // Custom items for this day (in sortOrder) if let items = itemsByDay[dayNumber] { let sortedItems = items.sorted { $0.sortOrder < $1.sortOrder } for item in sortedItems { if let coord = item.coordinate { waypoints.append((item.title, coord, true)) } } } } return waypoints } ``` **Step 2: Remove debug logging (optional cleanup)** Remove the debug print statements if desired. **Step 3: Run full test suite** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test` **Step 4: Commit** ```bash git add SportsTime/Features/Trip/Views/TripDetailView.swift git commit -m "refactor(routing): simplify routeWaypoints for day+sortOrder model Route now follows exact visual position: games first, then custom items sorted by sortOrder within each day. Co-Authored-By: Claude Opus 4.5 " ``` --- ## Task 11: Final Cleanup and Testing **Files:** - All modified files **Step 1: Run full build** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build` Expected: BUILD SUCCEEDED **Step 2: Run all tests** Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test` Expected: All tests pass **Step 3: Manual testing checklist** - [ ] Add a custom item to Day 1 - [ ] Add a custom item to Day 3 - [ ] Drag item from Day 1 to Day 3 - [ ] Verify item appears in correct position - [ ] Verify map route updates correctly - [ ] Edit a custom item - [ ] Delete a custom item - [ ] Drag travel segment to different day - [ ] Verify travel constraints are enforced **Step 4: Final commit** ```bash git add -A git commit -m "feat(itinerary): complete refactor to day+sortOrder positioning Replaces complex anchor-based system with simple (day, sortOrder) positioning. Benefits: - Items dropped exactly where you expect - Route follows visual order - No more disappearing items - Simpler codebase Co-Authored-By: Claude Opus 4.5 " ``` --- ## Summary | Task | Description | Estimated Complexity | |------|-------------|---------------------| | 1 | Update CustomItineraryItem model | Simple | | 2 | Update CKCustomItineraryItem CloudKit model | Simple | | 3 | Update CustomItemService | Simple | | 4 | Update ItineraryRowItem enum | Simple | | 5 | Update ItineraryTableViewController move logic | Medium | | 6 | Update ItineraryTableViewWrapper | Medium | | 7 | Update TripDetailView callbacks | Medium | | 8 | Update AddItemSheet | Simple | | 9 | Update TripDetailView sheet presentation | Simple | | 10 | Update routeWaypoints | Simple | | 11 | Final cleanup and testing | Simple | Total: 11 tasks, mostly simple refactoring with compile-time safety checks.