feat: add unified ItineraryItem model
Replaces CustomItineraryItem and TravelDayOverride with single model. Supports game, travel, and custom item kinds. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
124
SportsTime/Core/Models/Domain/ItineraryItem.swift
Normal file
124
SportsTime/Core/Models/Domain/ItineraryItem.swift
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user