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:
Trey t
2026-01-17 21:24:47 -06:00
parent cae24efa90
commit af2a5cd204
6 changed files with 0 additions and 764 deletions

View File

@@ -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"
}
}
}
}

View File

@@ -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
}
}

View File

@@ -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)"
}
}
}

View File

@@ -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()
}
}

View File

@@ -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)"
}
}
}