chore: remove legacy itinerary models and services
Removed: - CustomItineraryItem - TravelDayOverride - CustomItemService - CustomItemSubscriptionService - TravelOverrideService - CustomItemRow These are replaced by unified ItineraryItem model and service. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,94 +0,0 @@
|
||||
//
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
//
|
||||
// TravelDayOverride.swift
|
||||
// SportsTime
|
||||
//
|
||||
// Stores user-customized travel day positions
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Represents a user override of the default travel day placement.
|
||||
/// Travel segments normally appear on the day after the last game in the departure city.
|
||||
/// This model allows users to drag travel to a different day within valid bounds.
|
||||
struct TravelDayOverride: Identifiable, Codable, Hashable {
|
||||
let id: UUID
|
||||
let tripId: UUID
|
||||
/// Stable identifier for the travel segment (e.g., "travel:houston->arlington")
|
||||
let travelAnchorId: String
|
||||
/// The day number (1-indexed) where the travel should display
|
||||
var displayDay: Int
|
||||
let createdAt: Date
|
||||
var modifiedAt: Date
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
tripId: UUID,
|
||||
travelAnchorId: String,
|
||||
displayDay: Int,
|
||||
createdAt: Date = Date(),
|
||||
modifiedAt: Date = Date()
|
||||
) {
|
||||
self.id = id
|
||||
self.tripId = tripId
|
||||
self.travelAnchorId = travelAnchorId
|
||||
self.displayDay = displayDay
|
||||
self.createdAt = createdAt
|
||||
self.modifiedAt = modifiedAt
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
//
|
||||
// CustomItemService.swift
|
||||
// SportsTime
|
||||
//
|
||||
// CloudKit service for custom itinerary items
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CloudKit
|
||||
|
||||
actor CustomItemService {
|
||||
static let shared = CustomItemService()
|
||||
|
||||
private let container: CKContainer
|
||||
private let publicDatabase: CKDatabase
|
||||
|
||||
private init() {
|
||||
self.container = CKContainer(identifier: "iCloud.com.sportstime.app")
|
||||
self.publicDatabase = container.publicCloudDatabase
|
||||
}
|
||||
|
||||
// MARK: - CRUD Operations
|
||||
|
||||
func createItem(_ item: CustomItineraryItem) async throws -> CustomItineraryItem {
|
||||
print("☁️ [CloudKit] Creating record for item: \(item.id)")
|
||||
let ckItem = CKCustomItineraryItem(item: item)
|
||||
|
||||
do {
|
||||
let savedRecord = try await publicDatabase.save(ckItem.record)
|
||||
print("☁️ [CloudKit] Record saved: \(savedRecord.recordID.recordName)")
|
||||
return item
|
||||
} catch let error as CKError {
|
||||
print("☁️ [CloudKit] CKError on create: \(error.code.rawValue) - \(error.localizedDescription)")
|
||||
throw mapCloudKitError(error)
|
||||
} catch {
|
||||
print("☁️ [CloudKit] Unknown error on create: \(error)")
|
||||
throw CustomItemError.unknown(error)
|
||||
}
|
||||
}
|
||||
|
||||
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 day+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<Void, Error>) 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)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteItem(_ itemId: UUID) async throws {
|
||||
let recordID = CKRecord.ID(recordName: itemId.uuidString)
|
||||
|
||||
do {
|
||||
try await publicDatabase.deleteRecord(withID: recordID)
|
||||
} catch let error as CKError {
|
||||
if error.code != .unknownItem {
|
||||
throw mapCloudKitError(error)
|
||||
}
|
||||
// Item already deleted - ignore
|
||||
} catch {
|
||||
throw CustomItemError.unknown(error)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchItems(forTripId tripId: UUID) async throws -> [CustomItineraryItem] {
|
||||
print("☁️ [CloudKit] Fetching items for tripId: \(tripId.uuidString)")
|
||||
let predicate = NSPredicate(
|
||||
format: "%K == %@",
|
||||
CKCustomItineraryItem.tripIdKey,
|
||||
tripId.uuidString
|
||||
)
|
||||
let query = CKQuery(recordType: CKRecordType.customItineraryItem, predicate: predicate)
|
||||
|
||||
do {
|
||||
let (results, _) = try await publicDatabase.records(matching: query)
|
||||
print("☁️ [CloudKit] Query returned \(results.count) records")
|
||||
|
||||
let items = results.compactMap { result -> CustomItineraryItem? in
|
||||
guard case .success(let record) = result.1 else {
|
||||
print("☁️ [CloudKit] Record fetch failed for: \(result.0.recordName)")
|
||||
return nil
|
||||
}
|
||||
let item = CKCustomItineraryItem(record: record).toItem()
|
||||
if item == nil {
|
||||
print("☁️ [CloudKit] Failed to parse record: \(record.recordID.recordName)")
|
||||
}
|
||||
return item
|
||||
}.sorted { ($0.day, $0.sortOrder) < ($1.day, $1.sortOrder) }
|
||||
|
||||
print("☁️ [CloudKit] Parsed \(items.count) valid items")
|
||||
return items
|
||||
} catch let error as CKError {
|
||||
print("☁️ [CloudKit] CKError on fetch: \(error.code.rawValue) - \(error.localizedDescription)")
|
||||
throw mapCloudKitError(error)
|
||||
} catch {
|
||||
print("☁️ [CloudKit] Unknown error on fetch: \(error)")
|
||||
throw CustomItemError.unknown(error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Error Mapping
|
||||
|
||||
private func mapCloudKitError(_ error: CKError) -> CustomItemError {
|
||||
switch error.code {
|
||||
case .notAuthenticated:
|
||||
return .notSignedIn
|
||||
case .networkUnavailable, .networkFailure:
|
||||
return .networkUnavailable
|
||||
case .unknownItem:
|
||||
return .itemNotFound
|
||||
default:
|
||||
return .unknown(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
|
||||
enum CustomItemError: Error, LocalizedError {
|
||||
case notSignedIn
|
||||
case itemNotFound
|
||||
case networkUnavailable
|
||||
case unknown(Error)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .notSignedIn:
|
||||
return "Please sign in to iCloud to use custom items."
|
||||
case .itemNotFound:
|
||||
return "Item not found. It may have been deleted."
|
||||
case .networkUnavailable:
|
||||
return "Unable to connect. Please check your internet connection."
|
||||
case .unknown(let error):
|
||||
return "An error occurred: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
//
|
||||
// CustomItemSubscriptionService.swift
|
||||
// SportsTime
|
||||
//
|
||||
// CloudKit subscription service for real-time custom item updates
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CloudKit
|
||||
import Combine
|
||||
|
||||
/// Manages CloudKit subscriptions for custom itinerary items on shared trips
|
||||
actor CustomItemSubscriptionService {
|
||||
static let shared = CustomItemSubscriptionService()
|
||||
|
||||
private let container: CKContainer
|
||||
private let publicDatabase: CKDatabase
|
||||
|
||||
/// Publisher that emits trip IDs when their custom items change
|
||||
private let changeSubject = PassthroughSubject<UUID, Never>()
|
||||
var changePublisher: AnyPublisher<UUID, Never> {
|
||||
changeSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
/// Track which trip IDs have active subscriptions
|
||||
private var subscribedTripIds: Set<UUID> = []
|
||||
|
||||
private init() {
|
||||
self.container = CKContainer(identifier: "iCloud.com.sportstime.app")
|
||||
self.publicDatabase = container.publicCloudDatabase
|
||||
}
|
||||
|
||||
// MARK: - Subscription Management
|
||||
|
||||
/// Subscribe to changes for a specific trip's custom items
|
||||
func subscribeToTrip(_ tripId: UUID) async throws {
|
||||
guard !subscribedTripIds.contains(tripId) else {
|
||||
print("📡 [Subscription] Already subscribed to trip: \(tripId)")
|
||||
return
|
||||
}
|
||||
|
||||
let subscriptionID = "custom-items-\(tripId.uuidString)"
|
||||
|
||||
// Check if subscription already exists
|
||||
do {
|
||||
_ = try await publicDatabase.subscription(for: subscriptionID)
|
||||
subscribedTripIds.insert(tripId)
|
||||
print("📡 [Subscription] Found existing subscription for trip: \(tripId)")
|
||||
return
|
||||
} catch let error as CKError where error.code == .unknownItem {
|
||||
// Subscription doesn't exist, create it
|
||||
} catch {
|
||||
print("📡 [Subscription] Error checking subscription: \(error)")
|
||||
}
|
||||
|
||||
// Create subscription for this trip's custom items
|
||||
let predicate = NSPredicate(
|
||||
format: "%K == %@",
|
||||
CKCustomItineraryItem.tripIdKey,
|
||||
tripId.uuidString
|
||||
)
|
||||
|
||||
let subscription = CKQuerySubscription(
|
||||
recordType: CKRecordType.customItineraryItem,
|
||||
predicate: predicate,
|
||||
subscriptionID: subscriptionID,
|
||||
options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
|
||||
)
|
||||
|
||||
// Configure notification
|
||||
let notificationInfo = CKSubscription.NotificationInfo()
|
||||
notificationInfo.shouldSendContentAvailable = true // Silent notification
|
||||
subscription.notificationInfo = notificationInfo
|
||||
|
||||
do {
|
||||
try await publicDatabase.save(subscription)
|
||||
subscribedTripIds.insert(tripId)
|
||||
print("📡 [Subscription] Created subscription for trip: \(tripId)")
|
||||
} catch let error as CKError {
|
||||
print("📡 [Subscription] Failed to create subscription: \(error.code.rawValue) - \(error.localizedDescription)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/// Unsubscribe from a specific trip's custom items
|
||||
func unsubscribeFromTrip(_ tripId: UUID) async throws {
|
||||
guard subscribedTripIds.contains(tripId) else { return }
|
||||
|
||||
let subscriptionID = "custom-items-\(tripId.uuidString)"
|
||||
|
||||
do {
|
||||
try await publicDatabase.deleteSubscription(withID: subscriptionID)
|
||||
subscribedTripIds.remove(tripId)
|
||||
print("📡 [Subscription] Removed subscription for trip: \(tripId)")
|
||||
} catch let error as CKError where error.code == .unknownItem {
|
||||
// Already deleted
|
||||
subscribedTripIds.remove(tripId)
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe to all trips that the user has access to (via polls)
|
||||
func subscribeToSharedTrips(tripIds: [UUID]) async {
|
||||
for tripId in tripIds {
|
||||
do {
|
||||
try await subscribeToTrip(tripId)
|
||||
} catch {
|
||||
print("📡 [Subscription] Failed to subscribe to trip \(tripId): \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notification Handling
|
||||
|
||||
/// Handle CloudKit notification - call this from AppDelegate/SceneDelegate
|
||||
func handleNotification(_ notification: CKNotification) {
|
||||
guard let queryNotification = notification as? CKQueryNotification,
|
||||
let subscriptionID = queryNotification.subscriptionID,
|
||||
subscriptionID.hasPrefix("custom-items-") else {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract trip ID from subscription ID
|
||||
let tripIdString = String(subscriptionID.dropFirst("custom-items-".count))
|
||||
guard let tripId = UUID(uuidString: tripIdString) else { return }
|
||||
|
||||
print("📡 [Subscription] Received change notification for trip: \(tripId)")
|
||||
changeSubject.send(tripId)
|
||||
}
|
||||
|
||||
/// Handle notification from userInfo dictionary (from push notification)
|
||||
nonisolated func handleRemoteNotification(userInfo: [AnyHashable: Any]) async {
|
||||
guard let notification = CKNotification(fromRemoteNotificationDictionary: userInfo) else {
|
||||
return
|
||||
}
|
||||
await handleNotification(notification)
|
||||
}
|
||||
|
||||
// MARK: - Cleanup
|
||||
|
||||
/// Remove all subscriptions (call on sign out or cleanup)
|
||||
func removeAllSubscriptions() async {
|
||||
for tripId in subscribedTripIds {
|
||||
try? await unsubscribeFromTrip(tripId)
|
||||
}
|
||||
subscribedTripIds.removeAll()
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
//
|
||||
// TravelOverrideService.swift
|
||||
// SportsTime
|
||||
//
|
||||
// CloudKit service for travel day position overrides
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CloudKit
|
||||
|
||||
actor TravelOverrideService {
|
||||
static let shared = TravelOverrideService()
|
||||
|
||||
private let container: CKContainer
|
||||
private let publicDatabase: CKDatabase
|
||||
|
||||
private init() {
|
||||
self.container = CKContainer(identifier: "iCloud.com.sportstime.app")
|
||||
self.publicDatabase = container.publicCloudDatabase
|
||||
}
|
||||
|
||||
// MARK: - CRUD Operations
|
||||
|
||||
/// Create or update a travel day override (upserts by tripId + travelAnchorId)
|
||||
func saveOverride(_ override: TravelDayOverride) async throws -> TravelDayOverride {
|
||||
print("☁️ [CloudKit] Saving travel override for: \(override.travelAnchorId)")
|
||||
|
||||
// Look up existing override by travelAnchorId (not by UUID)
|
||||
let existingOverrides = try await fetchOverrides(forTripId: override.tripId)
|
||||
let existingOverride = existingOverrides.first { $0.travelAnchorId == override.travelAnchorId }
|
||||
|
||||
let record: CKRecord
|
||||
let now = Date()
|
||||
|
||||
if let existing = existingOverride {
|
||||
// Fetch the actual CKRecord to update it
|
||||
let recordID = CKRecord.ID(recordName: existing.id.uuidString)
|
||||
do {
|
||||
let existingRecord = try await publicDatabase.record(for: recordID)
|
||||
existingRecord[CKTravelDayOverride.displayDayKey] = override.displayDay
|
||||
existingRecord[CKTravelDayOverride.modifiedAtKey] = now
|
||||
record = existingRecord
|
||||
print("☁️ [CloudKit] Updating existing override record: \(existing.id)")
|
||||
} catch {
|
||||
// Record doesn't exist anymore, create new
|
||||
let ckOverride = CKTravelDayOverride(override: override)
|
||||
record = ckOverride.record
|
||||
}
|
||||
} else {
|
||||
// Create new record
|
||||
let ckOverride = CKTravelDayOverride(override: override)
|
||||
record = ckOverride.record
|
||||
print("☁️ [CloudKit] Creating new override record")
|
||||
}
|
||||
|
||||
do {
|
||||
try await publicDatabase.save(record)
|
||||
print("☁️ [CloudKit] Travel override saved: \(record.recordID.recordName)")
|
||||
var savedOverride = override
|
||||
savedOverride.modifiedAt = now
|
||||
return savedOverride
|
||||
} catch let error as CKError {
|
||||
print("☁️ [CloudKit] CKError on save: \(error.code.rawValue) - \(error.localizedDescription)")
|
||||
throw mapCloudKitError(error)
|
||||
} catch {
|
||||
print("☁️ [CloudKit] Unknown error on save: \(error)")
|
||||
throw TravelOverrideError.unknown(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a travel day override
|
||||
func deleteOverride(_ overrideId: UUID) async throws {
|
||||
let recordID = CKRecord.ID(recordName: overrideId.uuidString)
|
||||
|
||||
do {
|
||||
try await publicDatabase.deleteRecord(withID: recordID)
|
||||
print("☁️ [CloudKit] Travel override deleted: \(overrideId)")
|
||||
} catch let error as CKError {
|
||||
if error.code != .unknownItem {
|
||||
throw mapCloudKitError(error)
|
||||
}
|
||||
// Already deleted - ignore
|
||||
} catch {
|
||||
throw TravelOverrideError.unknown(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete override by travel anchor ID (useful when removing by travel segment)
|
||||
func deleteOverride(forTripId tripId: UUID, travelAnchorId: String) async throws {
|
||||
// First fetch to find the record
|
||||
let overrides = try await fetchOverrides(forTripId: tripId)
|
||||
if let existing = overrides.first(where: { $0.travelAnchorId == travelAnchorId }) {
|
||||
try await deleteOverride(existing.id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch all travel day overrides for a trip
|
||||
func fetchOverrides(forTripId tripId: UUID) async throws -> [TravelDayOverride] {
|
||||
print("☁️ [CloudKit] Fetching travel overrides for tripId: \(tripId.uuidString)")
|
||||
let predicate = NSPredicate(
|
||||
format: "%K == %@",
|
||||
CKTravelDayOverride.tripIdKey,
|
||||
tripId.uuidString
|
||||
)
|
||||
let query = CKQuery(recordType: CKRecordType.travelDayOverride, predicate: predicate)
|
||||
|
||||
do {
|
||||
let (results, _) = try await publicDatabase.records(matching: query)
|
||||
print("☁️ [CloudKit] Query returned \(results.count) travel override records")
|
||||
|
||||
let overrides = results.compactMap { result -> TravelDayOverride? in
|
||||
guard case .success(let record) = result.1 else {
|
||||
print("☁️ [CloudKit] Record fetch failed for: \(result.0.recordName)")
|
||||
return nil
|
||||
}
|
||||
let override = CKTravelDayOverride(record: record).toOverride()
|
||||
if override == nil {
|
||||
print("☁️ [CloudKit] Failed to parse travel override record: \(record.recordID.recordName)")
|
||||
}
|
||||
return override
|
||||
}
|
||||
|
||||
print("☁️ [CloudKit] Parsed \(overrides.count) valid travel overrides")
|
||||
return overrides
|
||||
} catch let error as CKError {
|
||||
print("☁️ [CloudKit] CKError on fetch: \(error.code.rawValue) - \(error.localizedDescription)")
|
||||
throw mapCloudKitError(error)
|
||||
} catch {
|
||||
print("☁️ [CloudKit] Unknown error on fetch: \(error)")
|
||||
throw TravelOverrideError.unknown(error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert array of overrides to dictionary keyed by travelAnchorId
|
||||
/// If duplicate travelAnchorIds exist (legacy data), takes the most recently modified one
|
||||
func fetchOverridesAsDictionary(forTripId tripId: UUID) async throws -> [String: Int] {
|
||||
let overrides = try await fetchOverrides(forTripId: tripId)
|
||||
// Sort by modifiedAt descending so most recent comes first
|
||||
let sorted = overrides.sorted { $0.modifiedAt > $1.modifiedAt }
|
||||
// Use uniquingKeysWith to keep the first (most recent) value for duplicates
|
||||
return Dictionary(sorted.map { ($0.travelAnchorId, $0.displayDay) }) { first, _ in first }
|
||||
}
|
||||
|
||||
// MARK: - Error Mapping
|
||||
|
||||
private func mapCloudKitError(_ error: CKError) -> TravelOverrideError {
|
||||
switch error.code {
|
||||
case .notAuthenticated:
|
||||
return .notSignedIn
|
||||
case .networkUnavailable, .networkFailure:
|
||||
return .networkUnavailable
|
||||
case .unknownItem:
|
||||
return .overrideNotFound
|
||||
default:
|
||||
return .unknown(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
|
||||
enum TravelOverrideError: Error, LocalizedError {
|
||||
case notSignedIn
|
||||
case overrideNotFound
|
||||
case networkUnavailable
|
||||
case unknown(Error)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .notSignedIn:
|
||||
return "Please sign in to iCloud to customize travel days."
|
||||
case .overrideNotFound:
|
||||
return "Travel day setting not found."
|
||||
case .networkUnavailable:
|
||||
return "Unable to connect. Please check your internet connection."
|
||||
case .unknown(let error):
|
||||
return "An error occurred: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user