diff --git a/SportsTime/Core/Models/Domain/ItineraryItem.swift b/SportsTime/Core/Models/Domain/ItineraryItem.swift new file mode 100644 index 0000000..bbf003b --- /dev/null +++ b/SportsTime/Core/Models/Domain/ItineraryItem.swift @@ -0,0 +1,124 @@ +import Foundation +import CoreLocation + +/// Unified model for all itinerary items (games, travel, custom) +struct ItineraryItem: Identifiable, Codable, Hashable { + let id: UUID + let tripId: UUID + var day: Int // 1-indexed day number + var sortOrder: Double // Position within day (fractional) + var kind: ItemKind + var modifiedAt: Date + + init( + id: UUID = UUID(), + tripId: UUID, + day: Int, + sortOrder: Double, + kind: ItemKind, + modifiedAt: Date = Date() + ) { + self.id = id + self.tripId = tripId + self.day = day + self.sortOrder = sortOrder + self.kind = kind + self.modifiedAt = modifiedAt + } +} + +/// The type of itinerary item +enum ItemKind: Codable, Hashable { + case game(gameId: String) + case travel(TravelInfo) + case custom(CustomInfo) +} + +/// Travel-specific information +struct TravelInfo: Codable, Hashable { + let fromCity: String + let toCity: String + var distanceMeters: Double? + var durationSeconds: Double? + + var formattedDistance: String { + guard let meters = distanceMeters else { return "" } + let miles = meters / 1609.34 + return String(format: "%.0f mi", miles) + } + + var formattedDuration: String { + guard let seconds = durationSeconds else { return "" } + let hours = Int(seconds) / 3600 + let minutes = (Int(seconds) % 3600) / 60 + if hours > 0 { + return "\(hours)h \(minutes)m" + } + return "\(minutes)m" + } +} + +/// Custom item information +struct CustomInfo: Codable, Hashable { + var title: String + var icon: String + var time: Date? + var latitude: Double? + var longitude: Double? + var address: String? + + var coordinate: CLLocationCoordinate2D? { + guard let lat = latitude, let lon = longitude else { return nil } + return CLLocationCoordinate2D(latitude: lat, longitude: lon) + } + + var isMappable: Bool { + latitude != nil && longitude != nil + } +} + +// MARK: - Convenience Properties + +extension ItineraryItem { + var isGame: Bool { + if case .game = kind { return true } + return false + } + + var isTravel: Bool { + if case .travel = kind { return true } + return false + } + + var isCustom: Bool { + if case .custom = kind { return true } + return false + } + + var travelInfo: TravelInfo? { + if case .travel(let info) = kind { return info } + return nil + } + + var customInfo: CustomInfo? { + if case .custom(let info) = kind { return info } + return nil + } + + var gameId: String? { + if case .game(let id) = kind { return id } + return nil + } + + /// Display title for the item + var displayTitle: String { + switch kind { + case .game(let gameId): + return "Game: \(gameId)" + case .travel(let info): + return "\(info.fromCity) → \(info.toCity)" + case .custom(let info): + return info.title + } + } +}