diff --git a/SportsTime/Core/Models/CloudKit/CKModels.swift b/SportsTime/Core/Models/CloudKit/CKModels.swift index 415f653..b1b3ede 100644 --- a/SportsTime/Core/Models/CloudKit/CKModels.swift +++ b/SportsTime/Core/Models/CloudKit/CKModels.swift @@ -632,16 +632,19 @@ struct CKCustomItineraryItem { static let tripIdKey = "tripId" static let categoryKey = "category" static let titleKey = "title" - static let anchorTypeKey = "anchorType" - static let anchorIdKey = "anchorId" - static let anchorDayKey = "anchorDay" - static let sortOrderKey = "sortOrder" + 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 @@ -658,10 +661,8 @@ struct CKCustomItineraryItem { record[CKCustomItineraryItem.tripIdKey] = item.tripId.uuidString record[CKCustomItineraryItem.categoryKey] = item.category.rawValue record[CKCustomItineraryItem.titleKey] = item.title - record[CKCustomItineraryItem.anchorTypeKey] = item.anchorType.rawValue - record[CKCustomItineraryItem.anchorIdKey] = item.anchorId - record[CKCustomItineraryItem.anchorDayKey] = item.anchorDay - record[CKCustomItineraryItem.sortOrderKey] = item.sortOrder + 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) @@ -679,15 +680,30 @@ struct CKCustomItineraryItem { let categoryString = record[CKCustomItineraryItem.categoryKey] as? String, let category = CustomItineraryItem.ItemCategory(rawValue: categoryString), let title = record[CKCustomItineraryItem.titleKey] as? String, - let anchorTypeString = record[CKCustomItineraryItem.anchorTypeKey] as? String, - let anchorType = CustomItineraryItem.AnchorType(rawValue: anchorTypeString), - let anchorDay = record[CKCustomItineraryItem.anchorDayKey] as? Int, let createdAt = record[CKCustomItineraryItem.createdAtKey] as? Date, let modifiedAt = record[CKCustomItineraryItem.modifiedAtKey] as? Date else { return nil } - let anchorId = record[CKCustomItineraryItem.anchorIdKey] as? String - let sortOrder = record[CKCustomItineraryItem.sortOrderKey] as? Int ?? 0 + // 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 @@ -699,9 +715,7 @@ struct CKCustomItineraryItem { tripId: tripId, category: category, title: title, - anchorType: anchorType, - anchorId: anchorId, - anchorDay: anchorDay, + day: day, sortOrder: sortOrder, createdAt: createdAt, modifiedAt: modifiedAt, diff --git a/SportsTime/Core/Models/Domain/CustomItineraryItem.swift b/SportsTime/Core/Models/Domain/CustomItineraryItem.swift index 004705f..b49d8d5 100644 --- a/SportsTime/Core/Models/Domain/CustomItineraryItem.swift +++ b/SportsTime/Core/Models/Domain/CustomItineraryItem.swift @@ -11,10 +11,8 @@ struct CustomItineraryItem: Identifiable, Codable, Hashable { let tripId: UUID var category: ItemCategory var title: String - var anchorType: AnchorType - var anchorId: String? - var anchorDay: Int - var sortOrder: Int // For ordering within same anchor position + var day: Int // Day number (1-indexed) + var sortOrder: Double // Position within day (allows insertion between items) let createdAt: Date var modifiedAt: Date @@ -39,10 +37,8 @@ struct CustomItineraryItem: Identifiable, Codable, Hashable { tripId: UUID, category: ItemCategory, title: String, - anchorType: AnchorType = .startOfDay, - anchorId: String? = nil, - anchorDay: Int, - sortOrder: Int = 0, + day: Int, + sortOrder: Double = 0.0, createdAt: Date = Date(), modifiedAt: Date = Date(), latitude: Double? = nil, @@ -53,9 +49,7 @@ struct CustomItineraryItem: Identifiable, Codable, Hashable { self.tripId = tripId self.category = category self.title = title - self.anchorType = anchorType - self.anchorId = anchorId - self.anchorDay = anchorDay + self.day = day self.sortOrder = sortOrder self.createdAt = createdAt self.modifiedAt = modifiedAt @@ -97,10 +91,4 @@ struct CustomItineraryItem: Identifiable, Codable, Hashable { } } } - - enum AnchorType: String, Codable { - case startOfDay - case afterGame - case afterTravel - } } diff --git a/SportsTime/Core/Models/Local/SavedTrip.swift b/SportsTime/Core/Models/Local/SavedTrip.swift index db2201e..90d67fd 100644 --- a/SportsTime/Core/Models/Local/SavedTrip.swift +++ b/SportsTime/Core/Models/Local/SavedTrip.swift @@ -109,9 +109,8 @@ final class LocalCustomItem { var tripId: UUID var category: String var title: String - var anchorType: String - var anchorId: String? - var anchorDay: Int + var day: Int + var sortOrder: Double var createdAt: Date var modifiedAt: Date var pendingSync: Bool // True if needs to sync to CloudKit @@ -121,9 +120,8 @@ final class LocalCustomItem { tripId: UUID, category: CustomItineraryItem.ItemCategory, title: String, - anchorType: CustomItineraryItem.AnchorType = .startOfDay, - anchorId: String? = nil, - anchorDay: Int, + day: Int, + sortOrder: Double = 0.0, createdAt: Date = Date(), modifiedAt: Date = Date(), pendingSync: Bool = false @@ -132,17 +130,15 @@ final class LocalCustomItem { self.tripId = tripId self.category = category.rawValue self.title = title - self.anchorType = anchorType.rawValue - self.anchorId = anchorId - self.anchorDay = anchorDay + self.day = day + self.sortOrder = sortOrder self.createdAt = createdAt self.modifiedAt = modifiedAt self.pendingSync = pendingSync } var toItem: CustomItineraryItem? { - guard let category = CustomItineraryItem.ItemCategory(rawValue: category), - let anchorType = CustomItineraryItem.AnchorType(rawValue: anchorType) + guard let category = CustomItineraryItem.ItemCategory(rawValue: category) else { return nil } return CustomItineraryItem( @@ -150,9 +146,8 @@ final class LocalCustomItem { tripId: tripId, category: category, title: title, - anchorType: anchorType, - anchorId: anchorId, - anchorDay: anchorDay, + day: day, + sortOrder: sortOrder, createdAt: createdAt, modifiedAt: modifiedAt ) @@ -164,9 +159,8 @@ final class LocalCustomItem { tripId: item.tripId, category: item.category, title: item.title, - anchorType: item.anchorType, - anchorId: item.anchorId, - anchorDay: item.anchorDay, + day: item.day, + sortOrder: item.sortOrder, createdAt: item.createdAt, modifiedAt: item.modifiedAt, pendingSync: pendingSync diff --git a/SportsTime/Core/Services/CustomItemService.swift b/SportsTime/Core/Services/CustomItemService.swift index 1c8e39e..239157b 100644 --- a/SportsTime/Core/Services/CustomItemService.swift +++ b/SportsTime/Core/Services/CustomItemService.swift @@ -55,10 +55,8 @@ actor CustomItemService { let now = Date() existingRecord[CKCustomItineraryItem.categoryKey] = item.category.rawValue existingRecord[CKCustomItineraryItem.titleKey] = item.title - existingRecord[CKCustomItineraryItem.anchorTypeKey] = item.anchorType.rawValue - existingRecord[CKCustomItineraryItem.anchorIdKey] = item.anchorId - existingRecord[CKCustomItineraryItem.anchorDayKey] = item.anchorDay - existingRecord[CKCustomItineraryItem.sortOrderKey] = item.sortOrder + 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 @@ -77,16 +75,16 @@ actor CustomItemService { } } - /// Batch update sortOrder for multiple items (for reordering) + /// Batch update day+sortOrder for multiple items (for reordering) func updateSortOrders(_ items: [CustomItineraryItem]) async throws { guard !items.isEmpty else { return } - print("☁️ [CloudKit] Batch updating sortOrder for \(items.count) items") + 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 sortOrder + // Update each record's day and sortOrder var recordsToSave: [CKRecord] = [] let now = Date() @@ -94,7 +92,8 @@ actor CustomItemService { let recordID = CKRecord.ID(recordName: item.id.uuidString) guard case .success(let record) = fetchResults[recordID] else { continue } - record[CKCustomItineraryItem.sortOrderKey] = item.sortOrder + record[CKCustomItineraryItem.dayKey] = item.day + record[CKCustomItineraryItem.sortOrderDoubleKey] = item.sortOrder record[CKCustomItineraryItem.modifiedAtKey] = now recordsToSave.append(record) } @@ -107,7 +106,7 @@ actor CustomItemService { modifyOp.modifyRecordsResultBlock = { result in switch result { case .success: - print("☁️ [CloudKit] Batch sortOrder update complete") + print("☁️ [CloudKit] Batch day+sortOrder update complete") continuation.resume() case .failure(let error): print("☁️ [CloudKit] Batch update failed: \(error)") @@ -156,7 +155,7 @@ actor CustomItemService { print("☁️ [CloudKit] Failed to parse record: \(record.recordID.recordName)") } return item - }.sorted { ($0.anchorDay, $0.sortOrder) < ($1.anchorDay, $1.sortOrder) } + }.sorted { ($0.day, $0.sortOrder) < ($1.day, $1.sortOrder) } print("☁️ [CloudKit] Parsed \(items.count) valid items") return items diff --git a/SportsTime/Features/Trip/Views/AddItemSheet.swift b/SportsTime/Features/Trip/Views/AddItemSheet.swift index 66fdada..e1bc46e 100644 --- a/SportsTime/Features/Trip/Views/AddItemSheet.swift +++ b/SportsTime/Features/Trip/Views/AddItemSheet.swift @@ -13,9 +13,7 @@ struct AddItemSheet: View { @Environment(\.colorScheme) private var colorScheme let tripId: UUID - let anchorDay: Int - let anchorType: CustomItineraryItem.AnchorType - let anchorId: String? + let day: Int let existingItem: CustomItineraryItem? var onSave: (CustomItineraryItem) -> Void @@ -250,7 +248,7 @@ struct AddItemSheet: View { let item: CustomItineraryItem if let existing = existingItem { - // Editing existing item + // Editing existing item - preserve day and sortOrder let trimmedTitle = title.trimmingCharacters(in: .whitespaces) guard !trimmedTitle.isEmpty else { return } @@ -259,9 +257,7 @@ struct AddItemSheet: View { tripId: existing.tripId, category: selectedCategory, title: trimmedTitle, - anchorType: existing.anchorType, - anchorId: existing.anchorId, - anchorDay: existing.anchorDay, + day: existing.day, sortOrder: existing.sortOrder, createdAt: existing.createdAt, modifiedAt: Date(), @@ -274,13 +270,13 @@ struct AddItemSheet: View { let placeName = place.name ?? "Unknown Place" let coordinate = place.placemark.coordinate + // New items get sortOrder 0 (will be placed at beginning, caller can adjust) item = CustomItineraryItem( tripId: tripId, category: selectedCategory, title: placeName, - anchorType: anchorType, - anchorId: anchorId, - anchorDay: anchorDay, + day: day, + sortOrder: 0.0, latitude: coordinate.latitude, longitude: coordinate.longitude, address: formatAddress(for: place) @@ -290,13 +286,13 @@ struct AddItemSheet: View { let trimmedTitle = title.trimmingCharacters(in: .whitespaces) guard !trimmedTitle.isEmpty else { return } + // New items get sortOrder 0 (will be placed at beginning, caller can adjust) item = CustomItineraryItem( tripId: tripId, category: selectedCategory, title: trimmedTitle, - anchorType: anchorType, - anchorId: anchorId, - anchorDay: anchorDay + day: day, + sortOrder: 0.0 ) } @@ -385,9 +381,7 @@ private struct CategoryButton: View { #Preview { AddItemSheet( tripId: UUID(), - anchorDay: 1, - anchorType: .startOfDay, - anchorId: nil, + day: 1, existingItem: nil ) { _ in } } diff --git a/SportsTime/Features/Trip/Views/CustomItemRow.swift b/SportsTime/Features/Trip/Views/CustomItemRow.swift index e59b406..4ca2af9 100644 --- a/SportsTime/Features/Trip/Views/CustomItemRow.swift +++ b/SportsTime/Features/Trip/Views/CustomItemRow.swift @@ -78,7 +78,7 @@ struct CustomItemRow: View { tripId: UUID(), category: .restaurant, title: "Joe's BBQ - Best brisket in Texas!", - anchorDay: 1 + day: 1 ), onTap: {}, onDelete: {} @@ -88,7 +88,7 @@ struct CustomItemRow: View { tripId: UUID(), category: .hotel, title: "Hilton Downtown", - anchorDay: 1 + day: 1 ), onTap: {}, onDelete: {} diff --git a/SportsTime/Features/Trip/Views/ItineraryTableViewController.swift b/SportsTime/Features/Trip/Views/ItineraryTableViewController.swift index 8a7cb79..710a6f8 100644 --- a/SportsTime/Features/Trip/Views/ItineraryTableViewController.swift +++ b/SportsTime/Features/Trip/Views/ItineraryTableViewController.swift @@ -27,7 +27,7 @@ enum ItineraryRowItem: Identifiable, Equatable { case dayHeader(dayNumber: Int, date: Date, games: [RichGame]) case travel(TravelSegment, dayNumber: Int) // dayNumber = the day this travel is associated with case customItem(CustomItineraryItem) - case addButton(day: Int, anchorType: CustomItineraryItem.AnchorType, anchorId: String?) + case addButton(day: Int) // Simplified - just needs day var id: String { switch self { @@ -37,8 +37,8 @@ enum ItineraryRowItem: Identifiable, Equatable { return "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())" case .customItem(let item): return "item:\(item.id.uuidString)" - case .addButton(let day, let anchorType, let anchorId): - return "add:\(day)-\(anchorType.rawValue)-\(anchorId ?? "nil")" + case .addButton(let day): + return "add:\(day)" } } @@ -68,10 +68,10 @@ final class ItineraryTableViewController: UITableViewController { // Callbacks var onTravelMoved: ((String, Int) -> Void)? // travelId, newDay - var onCustomItemMoved: ((UUID, Int, CustomItineraryItem.AnchorType, String?) -> Void)? + var onCustomItemMoved: ((UUID, Int, Double) -> Void)? // itemId, newDay, newSortOrder var onCustomItemTapped: ((CustomItineraryItem) -> Void)? var onCustomItemDeleted: ((CustomItineraryItem) -> Void)? - var onAddButtonTapped: ((Int, CustomItineraryItem.AnchorType, String?) -> Void)? + var onAddButtonTapped: ((Int) -> Void)? // Just day number // Cell reuse identifiers private let dayHeaderCellId = "DayHeaderCell" @@ -234,9 +234,9 @@ final class ItineraryTableViewController: UITableViewController { configureCustomItemCell(cell, item: customItem) return cell - case .addButton(let day, let anchorType, let anchorId): + case .addButton(let day): let cell = tableView.dequeueReusableCell(withIdentifier: addButtonCellId, for: indexPath) - configureAddButtonCell(cell, day: day, anchorType: anchorType, anchorId: anchorId) + configureAddButtonCell(cell, day: day) return cell } } @@ -266,8 +266,8 @@ final class ItineraryTableViewController: UITableViewController { case .customItem(let customItem): let destinationDay = dayNumber(forRow: destinationIndexPath.row) - let (anchorType, anchorId) = determineAnchor(at: destinationIndexPath.row) - onCustomItemMoved?(customItem.id, destinationDay, anchorType, anchorId) + let sortOrder = calculateSortOrder(at: destinationIndexPath.row) + onCustomItemMoved?(customItem.id, destinationDay, sortOrder) default: break @@ -403,8 +403,8 @@ final class ItineraryTableViewController: UITableViewController { case .customItem(let customItem): onCustomItemTapped?(customItem) - case .addButton(let day, let anchorType, let anchorId): - onAddButtonTapped?(day, anchorType, anchorId) + case .addButton(let day): + onAddButtonTapped?(day) default: break @@ -435,53 +435,64 @@ final class ItineraryTableViewController: UITableViewController { // MARK: - Helper Methods - private func determineAnchor(at row: Int) -> (CustomItineraryItem.AnchorType, String?) { - // Scan backwards to find the day's context - // Structure: travel (optional) -> dayHeader -> items - var foundTravel: TravelSegment? - var foundDayGames: [RichGame] = [] + /// Calculate the sortOrder for an item dropped at the given row position + /// Uses midpoint insertion: if between sortOrder 1.0 and 2.0, returns 1.5 + private func calculateSortOrder(at row: Int) -> Double { + // Find adjacent custom items to calculate midpoint + var prevSortOrder: Double? + var nextSortOrder: Double? + // Scan backwards for previous custom item in same day for i in stride(from: row - 1, through: 0, by: -1) { switch flatItems[i] { - case .travel(let segment, _): - // Found travel - if this is the first significant item, use afterTravel - // But only if we haven't passed a day header yet - if foundDayGames.isEmpty { - foundTravel = segment - } - // Travel marks the boundary - stop scanning - let travelId = "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())" - // If the drop is right after travel (no day header between), use afterTravel - if foundDayGames.isEmpty { - return (.afterTravel, travelId) - } - // Otherwise we already passed a day header, use that context + case .customItem(let item): + prevSortOrder = item.sortOrder break - - case .dayHeader(_, _, let games): - // Found the day header for this section - foundDayGames = games - // If day has games, items dropped after should be afterGame - if let lastGame = games.last { - return (.afterGame, lastGame.game.id) - } - // No games - check if there's travel before this day - // Continue scanning to find travel - continue - - case .customItem, .addButton: - // Skip these, keep scanning backwards + case .dayHeader, .travel: + // Hit a boundary - no previous item in this section + break + case .addButton: continue } + if prevSortOrder != nil { break } + // If we hit dayHeader or travel, stop scanning + if case .dayHeader = flatItems[i] { break } + if case .travel = flatItems[i] { break } } - // If we found travel but no games, use afterTravel - if let segment = foundTravel { - let travelId = "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())" - return (.afterTravel, travelId) + // Scan forwards for next custom item in same day + for i in row..: UIViewControllerRepresent // Callbacks var onTravelMoved: ((String, Int) -> Void)? - var onCustomItemMoved: ((UUID, Int, CustomItineraryItem.AnchorType, String?) -> Void)? + var onCustomItemMoved: ((UUID, Int, Double) -> Void)? // itemId, newDay, newSortOrder var onCustomItemTapped: ((CustomItineraryItem) -> Void)? var onCustomItemDeleted: ((CustomItineraryItem) -> Void)? - var onAddButtonTapped: ((Int, CustomItineraryItem.AnchorType, String?) -> Void)? + var onAddButtonTapped: ((Int) -> Void)? // Just day number init( trip: Trip, @@ -30,10 +30,10 @@ struct ItineraryTableViewWrapper: UIViewControllerRepresent travelDayOverrides: [String: Int], @ViewBuilder headerContent: () -> HeaderContent, onTravelMoved: ((String, Int) -> Void)? = nil, - onCustomItemMoved: ((UUID, Int, CustomItineraryItem.AnchorType, String?) -> Void)? = nil, + onCustomItemMoved: ((UUID, Int, Double) -> Void)? = nil, onCustomItemTapped: ((CustomItineraryItem) -> Void)? = nil, onCustomItemDeleted: ((CustomItineraryItem) -> Void)? = nil, - onAddButtonTapped: ((Int, CustomItineraryItem.AnchorType, String?) -> Void)? = nil + onAddButtonTapped: ((Int) -> Void)? = nil ) { self.trip = trip self.games = games @@ -159,47 +159,16 @@ struct ItineraryTableViewWrapper: UIViewControllerRepresent // Travel before this day (travel is stored on the destination day) let travelBefore: TravelSegment? = travelByDay[dayNum] - // Custom items after travel (if travel arrives on this day) - if let travelSegment = travelBefore { - let travelId = stableTravelAnchorId(travelSegment) - let itemsAfterTravel = customItems.filter { - $0.anchorType == .afterTravel && $0.anchorId == travelId - }.sorted { $0.sortOrder < $1.sortOrder } + // Custom items for this day - simply filter by day and sort by sortOrder + let dayItems = customItems.filter { $0.day == dayNum } + .sorted { $0.sortOrder < $1.sortOrder } - for item in itemsAfterTravel { - items.append(ItineraryRowItem.customItem(item)) - } - } - - // Custom items at start of day - let itemsAtStart = customItems.filter { - $0.anchorDay == dayNum && $0.anchorType == .startOfDay - }.sorted { $0.sortOrder < $1.sortOrder } - - for item in itemsAtStart { + for item in dayItems { items.append(ItineraryRowItem.customItem(item)) } - // Custom items after game - if let lastGame = gamesOnDay.last { - let itemsAfterGame = customItems.filter { - $0.anchorDay == dayNum && $0.anchorType == .afterGame && $0.anchorId == lastGame.game.id - }.sorted { $0.sortOrder < $1.sortOrder } - - for item in itemsAfterGame { - items.append(ItineraryRowItem.customItem(item)) - } - } - - // ONE Add button per day - after the last thing (game > travel > rest day) - if let lastGame = gamesOnDay.last { - items.append(ItineraryRowItem.addButton(day: dayNum, anchorType: .afterGame, anchorId: lastGame.game.id)) - } else if let travelSegment = travelBefore { - let travelId = stableTravelAnchorId(travelSegment) - items.append(ItineraryRowItem.addButton(day: dayNum, anchorType: .afterTravel, anchorId: travelId)) - } else { - items.append(ItineraryRowItem.addButton(day: dayNum, anchorType: .startOfDay, anchorId: nil)) - } + // ONE Add button per day + items.append(ItineraryRowItem.addButton(day: dayNum)) let dayData = ItineraryDayData( id: dayNum, diff --git a/SportsTime/Features/Trip/Views/TripDetailView.swift b/SportsTime/Features/Trip/Views/TripDetailView.swift index aacbbe7..6a7213b 100644 --- a/SportsTime/Features/Trip/Views/TripDetailView.swift +++ b/SportsTime/Features/Trip/Views/TripDetailView.swift @@ -119,9 +119,7 @@ struct TripDetailView: View { .sheet(item: $addItemAnchor) { anchor in AddItemSheet( tripId: trip.id, - anchorDay: anchor.day, - anchorType: anchor.type, - anchorId: anchor.anchorId, + day: anchor.day, existingItem: nil ) { item in Task { await saveCustomItem(item) } @@ -130,9 +128,7 @@ struct TripDetailView: View { .sheet(item: $editingItem) { item in AddItemSheet( tripId: trip.id, - anchorDay: item.anchorDay, - anchorType: item.anchorType, - anchorId: item.anchorId, + day: item.day, existingItem: item ) { updatedItem in Task { await saveCustomItem(updatedItem) } @@ -158,7 +154,7 @@ struct TripDetailView: View { // Recalculate routes when custom items change (mappable items affect route) print("🗺️ [MapUpdate] customItems changed, count: \(newItems.count)") for item in newItems where item.isMappable { - print("🗺️ [MapUpdate] Mappable: \(item.title) on day \(item.anchorDay), anchor: \(item.anchorType.rawValue)") + print("🗺️ [MapUpdate] Mappable: \(item.title) on day \(item.day), sortOrder: \(item.sortOrder)") } Task { updateMapRegion() @@ -224,10 +220,10 @@ struct TripDetailView: View { await saveTravelDayOverride(travelAnchorId: travelId, displayDay: newDay) } }, - onCustomItemMoved: { itemId, day, anchorType, anchorId in + onCustomItemMoved: { itemId, day, sortOrder in Task { @MainActor in guard let item = customItems.first(where: { $0.id == itemId }) else { return } - await moveItemToBeginning(item, toDay: day, anchorType: anchorType, anchorId: anchorId) + await moveItem(item, toDay: day, sortOrder: sortOrder) } }, onCustomItemTapped: { item in @@ -236,8 +232,8 @@ struct TripDetailView: View { onCustomItemDeleted: { item in Task { await deleteCustomItem(item) } }, - onAddButtonTapped: { day, anchorType, anchorId in - addItemAnchor = AddItemAnchor(day: day, type: anchorType, anchorId: anchorId) + onAddButtonTapped: { day in + addItemAnchor = AddItemAnchor(day: day) } ) .ignoresSafeArea(edges: .bottom) @@ -605,13 +601,13 @@ struct TripDetailView: View { handleCustomItemDrop(providers: providers, targetItem: item) } - case .addButton(let day, let anchorType, let anchorId): + case .addButton(let day): VStack(spacing: 0) { if isDropTarget { DropTargetIndicator() } InlineAddButton { - addItemAnchor = AddItemAnchor(day: day, type: anchorType, anchorId: anchorId) + addItemAnchor = AddItemAnchor(day: day) } } .onDrop(of: [.text, .plainText, .utf8PlainText], isTargeted: Binding( @@ -628,7 +624,7 @@ struct TripDetailView: View { } } )) { providers in - handleAddButtonDrop(providers: providers, day: day, anchorType: anchorType, anchorId: anchorId) + handleAddButtonDrop(providers: providers, day: day) } } } @@ -652,8 +648,12 @@ struct TripDetailView: View { Task { @MainActor in let day = self.findDayForTravelSegment(segment) - let stableAnchorId = self.stableTravelAnchorId(segment) - await self.moveItemToBeginning(droppedItem, toDay: day, anchorType: .afterTravel, anchorId: stableAnchorId) + // Place at beginning of day (sortOrder before existing items) + let minSortOrder = self.customItems + .filter { $0.day == day && $0.id != droppedItem.id } + .map { $0.sortOrder } + .min() ?? 1.0 + await self.moveItem(droppedItem, toDay: day, sortOrder: minSortOrder / 2.0) } } return true @@ -676,13 +676,19 @@ struct TripDetailView: View { droppedItem.id != targetItem.id else { return } Task { @MainActor in - await self.moveItem(droppedItem, toDay: targetItem.anchorDay, anchorType: targetItem.anchorType, anchorId: targetItem.anchorId, beforeItem: targetItem) + // Place before target item using midpoint insertion + let itemsInDay = self.customItems.filter { $0.day == targetItem.day && $0.id != droppedItem.id } + .sorted { $0.sortOrder < $1.sortOrder } + let targetIdx = itemsInDay.firstIndex(where: { $0.id == targetItem.id }) ?? 0 + let prevSortOrder = targetIdx > 0 ? itemsInDay[targetIdx - 1].sortOrder : 0.0 + let newSortOrder = (prevSortOrder + targetItem.sortOrder) / 2.0 + await self.moveItem(droppedItem, toDay: targetItem.day, sortOrder: newSortOrder) } } return true } - private func handleAddButtonDrop(providers: [NSItemProvider], day: Int, anchorType: CustomItineraryItem.AnchorType, anchorId: String?) -> Bool { + private func handleAddButtonDrop(providers: [NSItemProvider], day: Int) -> Bool { guard let provider = providers.first, provider.canLoadObject(ofClass: NSString.self) else { return false } @@ -698,7 +704,12 @@ struct TripDetailView: View { let droppedItem = self.customItems.first(where: { $0.id == itemId }) else { return } Task { @MainActor in - await self.moveItemToBeginning(droppedItem, toDay: day, anchorType: anchorType, anchorId: anchorId) + // Calculate sortOrder: append at end of day's items + let maxSortOrder = self.customItems + .filter { $0.day == day && $0.id != droppedItem.id } + .map { $0.sortOrder } + .max() ?? 0.0 + await self.moveItem(droppedItem, toDay: day, sortOrder: maxSortOrder + 1.0) } } return true @@ -719,8 +730,8 @@ struct TripDetailView: View { return "travel-\(segment.fromLocation.name)-\(segment.toLocation.name)" case .customItem(let item): return "item-\(item.id.uuidString)" - case .addButton(let day, let anchorType, _): - return "add-\(day)-\(anchorType.rawValue)" + case .addButton(let day): + return "add-\(day)" } } @@ -747,120 +758,26 @@ struct TripDetailView: View { return "travel:\(from)->\(to)" } - private func moveItem(_ item: CustomItineraryItem, toDay day: Int, anchorType: CustomItineraryItem.AnchorType, anchorId: String?, beforeItem: CustomItineraryItem? = nil) async { + /// Move item to a new day and sortOrder position + private func moveItem(_ item: CustomItineraryItem, toDay day: Int, sortOrder: Double) async { var updated = item - updated.anchorDay = day - updated.anchorType = anchorType - updated.anchorId = anchorId + updated.day = day + updated.sortOrder = sortOrder updated.modifiedAt = Date() - // Calculate sortOrder - let itemsAtSameAnchor = customItems.filter { - $0.anchorDay == day && - $0.anchorType == anchorType && - $0.anchorId == anchorId && - $0.id != item.id - }.sorted { $0.sortOrder < $1.sortOrder } - - var itemsToSync: [CustomItineraryItem] = [] - - print("📍 [Move] itemsAtSameAnchor: \(itemsAtSameAnchor.map { "\($0.title) (id: \($0.id.uuidString.prefix(8)))" })") - if let beforeItem = beforeItem { - print("📍 [Move] beforeItem: \(beforeItem.title) (id: \(beforeItem.id.uuidString.prefix(8)))") - } - - if let beforeItem = beforeItem, - let beforeIndex = itemsAtSameAnchor.firstIndex(where: { $0.id == beforeItem.id }) { - updated.sortOrder = beforeIndex - print("📍 [Move] Setting \(item.title) sortOrder to \(beforeIndex) (before \(beforeItem.title))") - - // Shift other items and track them for syncing - for i in beforeIndex..