refactor(itinerary): replace anchor-based positioning with day/sortOrder
Replace complex anchor system (anchorType, anchorId, anchorDay) with simple (day: Int, sortOrder: Double) positioning for custom items. Changes: - CustomItineraryItem: Remove anchor fields, add day and sortOrder - CKModels: Add migration fallback from old CloudKit fields - ItineraryTableViewController: Add calculateSortOrder() for midpoint insertion - TripDetailView: Simplify callbacks, itinerarySections, and routeWaypoints - AddItemSheet: Take simple day parameter instead of anchor - SavedTrip: Update LocalCustomItem SwiftData model Benefits: - Items freely movable via drag-and-drop - Route waypoints follow exact visual order - Simpler mental model: position = (day, sortOrder) - Midpoint insertion allows unlimited reordering Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -632,16 +632,19 @@ struct CKCustomItineraryItem {
|
|||||||
static let tripIdKey = "tripId"
|
static let tripIdKey = "tripId"
|
||||||
static let categoryKey = "category"
|
static let categoryKey = "category"
|
||||||
static let titleKey = "title"
|
static let titleKey = "title"
|
||||||
static let anchorTypeKey = "anchorType"
|
static let dayKey = "day" // NEW: replaces anchorDay
|
||||||
static let anchorIdKey = "anchorId"
|
static let sortOrderDoubleKey = "sortOrderDouble" // NEW: Double instead of Int
|
||||||
static let anchorDayKey = "anchorDay"
|
|
||||||
static let sortOrderKey = "sortOrder"
|
|
||||||
static let createdAtKey = "createdAt"
|
static let createdAtKey = "createdAt"
|
||||||
static let modifiedAtKey = "modifiedAt"
|
static let modifiedAtKey = "modifiedAt"
|
||||||
// Location fields for mappable items
|
// Location fields for mappable items
|
||||||
static let latitudeKey = "latitude"
|
static let latitudeKey = "latitude"
|
||||||
static let longitudeKey = "longitude"
|
static let longitudeKey = "longitude"
|
||||||
static let addressKey = "address"
|
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
|
let record: CKRecord
|
||||||
|
|
||||||
@@ -658,10 +661,8 @@ struct CKCustomItineraryItem {
|
|||||||
record[CKCustomItineraryItem.tripIdKey] = item.tripId.uuidString
|
record[CKCustomItineraryItem.tripIdKey] = item.tripId.uuidString
|
||||||
record[CKCustomItineraryItem.categoryKey] = item.category.rawValue
|
record[CKCustomItineraryItem.categoryKey] = item.category.rawValue
|
||||||
record[CKCustomItineraryItem.titleKey] = item.title
|
record[CKCustomItineraryItem.titleKey] = item.title
|
||||||
record[CKCustomItineraryItem.anchorTypeKey] = item.anchorType.rawValue
|
record[CKCustomItineraryItem.dayKey] = item.day
|
||||||
record[CKCustomItineraryItem.anchorIdKey] = item.anchorId
|
record[CKCustomItineraryItem.sortOrderDoubleKey] = item.sortOrder
|
||||||
record[CKCustomItineraryItem.anchorDayKey] = item.anchorDay
|
|
||||||
record[CKCustomItineraryItem.sortOrderKey] = item.sortOrder
|
|
||||||
record[CKCustomItineraryItem.createdAtKey] = item.createdAt
|
record[CKCustomItineraryItem.createdAtKey] = item.createdAt
|
||||||
record[CKCustomItineraryItem.modifiedAtKey] = item.modifiedAt
|
record[CKCustomItineraryItem.modifiedAtKey] = item.modifiedAt
|
||||||
// Location fields (nil values are not stored in CloudKit)
|
// Location fields (nil values are not stored in CloudKit)
|
||||||
@@ -679,15 +680,30 @@ struct CKCustomItineraryItem {
|
|||||||
let categoryString = record[CKCustomItineraryItem.categoryKey] as? String,
|
let categoryString = record[CKCustomItineraryItem.categoryKey] as? String,
|
||||||
let category = CustomItineraryItem.ItemCategory(rawValue: categoryString),
|
let category = CustomItineraryItem.ItemCategory(rawValue: categoryString),
|
||||||
let title = record[CKCustomItineraryItem.titleKey] as? String,
|
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 createdAt = record[CKCustomItineraryItem.createdAtKey] as? Date,
|
||||||
let modifiedAt = record[CKCustomItineraryItem.modifiedAtKey] as? Date
|
let modifiedAt = record[CKCustomItineraryItem.modifiedAtKey] as? Date
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
let anchorId = record[CKCustomItineraryItem.anchorIdKey] as? String
|
// Read new fields, with migration fallback from old fields
|
||||||
let sortOrder = record[CKCustomItineraryItem.sortOrderKey] as? Int ?? 0
|
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)
|
// Location fields (optional - nil if not stored)
|
||||||
let latitude = record[CKCustomItineraryItem.latitudeKey] as? Double
|
let latitude = record[CKCustomItineraryItem.latitudeKey] as? Double
|
||||||
@@ -699,9 +715,7 @@ struct CKCustomItineraryItem {
|
|||||||
tripId: tripId,
|
tripId: tripId,
|
||||||
category: category,
|
category: category,
|
||||||
title: title,
|
title: title,
|
||||||
anchorType: anchorType,
|
day: day,
|
||||||
anchorId: anchorId,
|
|
||||||
anchorDay: anchorDay,
|
|
||||||
sortOrder: sortOrder,
|
sortOrder: sortOrder,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
modifiedAt: modifiedAt,
|
modifiedAt: modifiedAt,
|
||||||
|
|||||||
@@ -11,10 +11,8 @@ struct CustomItineraryItem: Identifiable, Codable, Hashable {
|
|||||||
let tripId: UUID
|
let tripId: UUID
|
||||||
var category: ItemCategory
|
var category: ItemCategory
|
||||||
var title: String
|
var title: String
|
||||||
var anchorType: AnchorType
|
var day: Int // Day number (1-indexed)
|
||||||
var anchorId: String?
|
var sortOrder: Double // Position within day (allows insertion between items)
|
||||||
var anchorDay: Int
|
|
||||||
var sortOrder: Int // For ordering within same anchor position
|
|
||||||
let createdAt: Date
|
let createdAt: Date
|
||||||
var modifiedAt: Date
|
var modifiedAt: Date
|
||||||
|
|
||||||
@@ -39,10 +37,8 @@ struct CustomItineraryItem: Identifiable, Codable, Hashable {
|
|||||||
tripId: UUID,
|
tripId: UUID,
|
||||||
category: ItemCategory,
|
category: ItemCategory,
|
||||||
title: String,
|
title: String,
|
||||||
anchorType: AnchorType = .startOfDay,
|
day: Int,
|
||||||
anchorId: String? = nil,
|
sortOrder: Double = 0.0,
|
||||||
anchorDay: Int,
|
|
||||||
sortOrder: Int = 0,
|
|
||||||
createdAt: Date = Date(),
|
createdAt: Date = Date(),
|
||||||
modifiedAt: Date = Date(),
|
modifiedAt: Date = Date(),
|
||||||
latitude: Double? = nil,
|
latitude: Double? = nil,
|
||||||
@@ -53,9 +49,7 @@ struct CustomItineraryItem: Identifiable, Codable, Hashable {
|
|||||||
self.tripId = tripId
|
self.tripId = tripId
|
||||||
self.category = category
|
self.category = category
|
||||||
self.title = title
|
self.title = title
|
||||||
self.anchorType = anchorType
|
self.day = day
|
||||||
self.anchorId = anchorId
|
|
||||||
self.anchorDay = anchorDay
|
|
||||||
self.sortOrder = sortOrder
|
self.sortOrder = sortOrder
|
||||||
self.createdAt = createdAt
|
self.createdAt = createdAt
|
||||||
self.modifiedAt = modifiedAt
|
self.modifiedAt = modifiedAt
|
||||||
@@ -97,10 +91,4 @@ struct CustomItineraryItem: Identifiable, Codable, Hashable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AnchorType: String, Codable {
|
|
||||||
case startOfDay
|
|
||||||
case afterGame
|
|
||||||
case afterTravel
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,9 +109,8 @@ final class LocalCustomItem {
|
|||||||
var tripId: UUID
|
var tripId: UUID
|
||||||
var category: String
|
var category: String
|
||||||
var title: String
|
var title: String
|
||||||
var anchorType: String
|
var day: Int
|
||||||
var anchorId: String?
|
var sortOrder: Double
|
||||||
var anchorDay: Int
|
|
||||||
var createdAt: Date
|
var createdAt: Date
|
||||||
var modifiedAt: Date
|
var modifiedAt: Date
|
||||||
var pendingSync: Bool // True if needs to sync to CloudKit
|
var pendingSync: Bool // True if needs to sync to CloudKit
|
||||||
@@ -121,9 +120,8 @@ final class LocalCustomItem {
|
|||||||
tripId: UUID,
|
tripId: UUID,
|
||||||
category: CustomItineraryItem.ItemCategory,
|
category: CustomItineraryItem.ItemCategory,
|
||||||
title: String,
|
title: String,
|
||||||
anchorType: CustomItineraryItem.AnchorType = .startOfDay,
|
day: Int,
|
||||||
anchorId: String? = nil,
|
sortOrder: Double = 0.0,
|
||||||
anchorDay: Int,
|
|
||||||
createdAt: Date = Date(),
|
createdAt: Date = Date(),
|
||||||
modifiedAt: Date = Date(),
|
modifiedAt: Date = Date(),
|
||||||
pendingSync: Bool = false
|
pendingSync: Bool = false
|
||||||
@@ -132,17 +130,15 @@ final class LocalCustomItem {
|
|||||||
self.tripId = tripId
|
self.tripId = tripId
|
||||||
self.category = category.rawValue
|
self.category = category.rawValue
|
||||||
self.title = title
|
self.title = title
|
||||||
self.anchorType = anchorType.rawValue
|
self.day = day
|
||||||
self.anchorId = anchorId
|
self.sortOrder = sortOrder
|
||||||
self.anchorDay = anchorDay
|
|
||||||
self.createdAt = createdAt
|
self.createdAt = createdAt
|
||||||
self.modifiedAt = modifiedAt
|
self.modifiedAt = modifiedAt
|
||||||
self.pendingSync = pendingSync
|
self.pendingSync = pendingSync
|
||||||
}
|
}
|
||||||
|
|
||||||
var toItem: CustomItineraryItem? {
|
var toItem: CustomItineraryItem? {
|
||||||
guard let category = CustomItineraryItem.ItemCategory(rawValue: category),
|
guard let category = CustomItineraryItem.ItemCategory(rawValue: category)
|
||||||
let anchorType = CustomItineraryItem.AnchorType(rawValue: anchorType)
|
|
||||||
else { return nil }
|
else { return nil }
|
||||||
|
|
||||||
return CustomItineraryItem(
|
return CustomItineraryItem(
|
||||||
@@ -150,9 +146,8 @@ final class LocalCustomItem {
|
|||||||
tripId: tripId,
|
tripId: tripId,
|
||||||
category: category,
|
category: category,
|
||||||
title: title,
|
title: title,
|
||||||
anchorType: anchorType,
|
day: day,
|
||||||
anchorId: anchorId,
|
sortOrder: sortOrder,
|
||||||
anchorDay: anchorDay,
|
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
modifiedAt: modifiedAt
|
modifiedAt: modifiedAt
|
||||||
)
|
)
|
||||||
@@ -164,9 +159,8 @@ final class LocalCustomItem {
|
|||||||
tripId: item.tripId,
|
tripId: item.tripId,
|
||||||
category: item.category,
|
category: item.category,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
anchorType: item.anchorType,
|
day: item.day,
|
||||||
anchorId: item.anchorId,
|
sortOrder: item.sortOrder,
|
||||||
anchorDay: item.anchorDay,
|
|
||||||
createdAt: item.createdAt,
|
createdAt: item.createdAt,
|
||||||
modifiedAt: item.modifiedAt,
|
modifiedAt: item.modifiedAt,
|
||||||
pendingSync: pendingSync
|
pendingSync: pendingSync
|
||||||
|
|||||||
@@ -55,10 +55,8 @@ actor CustomItemService {
|
|||||||
let now = Date()
|
let now = Date()
|
||||||
existingRecord[CKCustomItineraryItem.categoryKey] = item.category.rawValue
|
existingRecord[CKCustomItineraryItem.categoryKey] = item.category.rawValue
|
||||||
existingRecord[CKCustomItineraryItem.titleKey] = item.title
|
existingRecord[CKCustomItineraryItem.titleKey] = item.title
|
||||||
existingRecord[CKCustomItineraryItem.anchorTypeKey] = item.anchorType.rawValue
|
existingRecord[CKCustomItineraryItem.dayKey] = item.day
|
||||||
existingRecord[CKCustomItineraryItem.anchorIdKey] = item.anchorId
|
existingRecord[CKCustomItineraryItem.sortOrderDoubleKey] = item.sortOrder
|
||||||
existingRecord[CKCustomItineraryItem.anchorDayKey] = item.anchorDay
|
|
||||||
existingRecord[CKCustomItineraryItem.sortOrderKey] = item.sortOrder
|
|
||||||
existingRecord[CKCustomItineraryItem.modifiedAtKey] = now
|
existingRecord[CKCustomItineraryItem.modifiedAtKey] = now
|
||||||
// Location fields (nil values clear the field in CloudKit)
|
// Location fields (nil values clear the field in CloudKit)
|
||||||
existingRecord[CKCustomItineraryItem.latitudeKey] = item.latitude
|
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 {
|
func updateSortOrders(_ items: [CustomItineraryItem]) async throws {
|
||||||
guard !items.isEmpty else { return }
|
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
|
// Fetch all records
|
||||||
let recordIDs = items.map { CKRecord.ID(recordName: $0.id.uuidString) }
|
let recordIDs = items.map { CKRecord.ID(recordName: $0.id.uuidString) }
|
||||||
let fetchResults = try await publicDatabase.records(for: recordIDs)
|
let fetchResults = try await publicDatabase.records(for: recordIDs)
|
||||||
|
|
||||||
// Update each record's sortOrder
|
// Update each record's day and sortOrder
|
||||||
var recordsToSave: [CKRecord] = []
|
var recordsToSave: [CKRecord] = []
|
||||||
let now = Date()
|
let now = Date()
|
||||||
|
|
||||||
@@ -94,7 +92,8 @@ actor CustomItemService {
|
|||||||
let recordID = CKRecord.ID(recordName: item.id.uuidString)
|
let recordID = CKRecord.ID(recordName: item.id.uuidString)
|
||||||
guard case .success(let record) = fetchResults[recordID] else { continue }
|
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
|
record[CKCustomItineraryItem.modifiedAtKey] = now
|
||||||
recordsToSave.append(record)
|
recordsToSave.append(record)
|
||||||
}
|
}
|
||||||
@@ -107,7 +106,7 @@ actor CustomItemService {
|
|||||||
modifyOp.modifyRecordsResultBlock = { result in
|
modifyOp.modifyRecordsResultBlock = { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
print("☁️ [CloudKit] Batch sortOrder update complete")
|
print("☁️ [CloudKit] Batch day+sortOrder update complete")
|
||||||
continuation.resume()
|
continuation.resume()
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
print("☁️ [CloudKit] Batch update failed: \(error)")
|
print("☁️ [CloudKit] Batch update failed: \(error)")
|
||||||
@@ -156,7 +155,7 @@ actor CustomItemService {
|
|||||||
print("☁️ [CloudKit] Failed to parse record: \(record.recordID.recordName)")
|
print("☁️ [CloudKit] Failed to parse record: \(record.recordID.recordName)")
|
||||||
}
|
}
|
||||||
return item
|
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")
|
print("☁️ [CloudKit] Parsed \(items.count) valid items")
|
||||||
return items
|
return items
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ struct AddItemSheet: View {
|
|||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
let tripId: UUID
|
let tripId: UUID
|
||||||
let anchorDay: Int
|
let day: Int
|
||||||
let anchorType: CustomItineraryItem.AnchorType
|
|
||||||
let anchorId: String?
|
|
||||||
let existingItem: CustomItineraryItem?
|
let existingItem: CustomItineraryItem?
|
||||||
var onSave: (CustomItineraryItem) -> Void
|
var onSave: (CustomItineraryItem) -> Void
|
||||||
|
|
||||||
@@ -250,7 +248,7 @@ struct AddItemSheet: View {
|
|||||||
let item: CustomItineraryItem
|
let item: CustomItineraryItem
|
||||||
|
|
||||||
if let existing = existingItem {
|
if let existing = existingItem {
|
||||||
// Editing existing item
|
// Editing existing item - preserve day and sortOrder
|
||||||
let trimmedTitle = title.trimmingCharacters(in: .whitespaces)
|
let trimmedTitle = title.trimmingCharacters(in: .whitespaces)
|
||||||
guard !trimmedTitle.isEmpty else { return }
|
guard !trimmedTitle.isEmpty else { return }
|
||||||
|
|
||||||
@@ -259,9 +257,7 @@ struct AddItemSheet: View {
|
|||||||
tripId: existing.tripId,
|
tripId: existing.tripId,
|
||||||
category: selectedCategory,
|
category: selectedCategory,
|
||||||
title: trimmedTitle,
|
title: trimmedTitle,
|
||||||
anchorType: existing.anchorType,
|
day: existing.day,
|
||||||
anchorId: existing.anchorId,
|
|
||||||
anchorDay: existing.anchorDay,
|
|
||||||
sortOrder: existing.sortOrder,
|
sortOrder: existing.sortOrder,
|
||||||
createdAt: existing.createdAt,
|
createdAt: existing.createdAt,
|
||||||
modifiedAt: Date(),
|
modifiedAt: Date(),
|
||||||
@@ -274,13 +270,13 @@ struct AddItemSheet: View {
|
|||||||
let placeName = place.name ?? "Unknown Place"
|
let placeName = place.name ?? "Unknown Place"
|
||||||
let coordinate = place.placemark.coordinate
|
let coordinate = place.placemark.coordinate
|
||||||
|
|
||||||
|
// New items get sortOrder 0 (will be placed at beginning, caller can adjust)
|
||||||
item = CustomItineraryItem(
|
item = CustomItineraryItem(
|
||||||
tripId: tripId,
|
tripId: tripId,
|
||||||
category: selectedCategory,
|
category: selectedCategory,
|
||||||
title: placeName,
|
title: placeName,
|
||||||
anchorType: anchorType,
|
day: day,
|
||||||
anchorId: anchorId,
|
sortOrder: 0.0,
|
||||||
anchorDay: anchorDay,
|
|
||||||
latitude: coordinate.latitude,
|
latitude: coordinate.latitude,
|
||||||
longitude: coordinate.longitude,
|
longitude: coordinate.longitude,
|
||||||
address: formatAddress(for: place)
|
address: formatAddress(for: place)
|
||||||
@@ -290,13 +286,13 @@ struct AddItemSheet: View {
|
|||||||
let trimmedTitle = title.trimmingCharacters(in: .whitespaces)
|
let trimmedTitle = title.trimmingCharacters(in: .whitespaces)
|
||||||
guard !trimmedTitle.isEmpty else { return }
|
guard !trimmedTitle.isEmpty else { return }
|
||||||
|
|
||||||
|
// New items get sortOrder 0 (will be placed at beginning, caller can adjust)
|
||||||
item = CustomItineraryItem(
|
item = CustomItineraryItem(
|
||||||
tripId: tripId,
|
tripId: tripId,
|
||||||
category: selectedCategory,
|
category: selectedCategory,
|
||||||
title: trimmedTitle,
|
title: trimmedTitle,
|
||||||
anchorType: anchorType,
|
day: day,
|
||||||
anchorId: anchorId,
|
sortOrder: 0.0
|
||||||
anchorDay: anchorDay
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,9 +381,7 @@ private struct CategoryButton: View {
|
|||||||
#Preview {
|
#Preview {
|
||||||
AddItemSheet(
|
AddItemSheet(
|
||||||
tripId: UUID(),
|
tripId: UUID(),
|
||||||
anchorDay: 1,
|
day: 1,
|
||||||
anchorType: .startOfDay,
|
|
||||||
anchorId: nil,
|
|
||||||
existingItem: nil
|
existingItem: nil
|
||||||
) { _ in }
|
) { _ in }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ struct CustomItemRow: View {
|
|||||||
tripId: UUID(),
|
tripId: UUID(),
|
||||||
category: .restaurant,
|
category: .restaurant,
|
||||||
title: "Joe's BBQ - Best brisket in Texas!",
|
title: "Joe's BBQ - Best brisket in Texas!",
|
||||||
anchorDay: 1
|
day: 1
|
||||||
),
|
),
|
||||||
onTap: {},
|
onTap: {},
|
||||||
onDelete: {}
|
onDelete: {}
|
||||||
@@ -88,7 +88,7 @@ struct CustomItemRow: View {
|
|||||||
tripId: UUID(),
|
tripId: UUID(),
|
||||||
category: .hotel,
|
category: .hotel,
|
||||||
title: "Hilton Downtown",
|
title: "Hilton Downtown",
|
||||||
anchorDay: 1
|
day: 1
|
||||||
),
|
),
|
||||||
onTap: {},
|
onTap: {},
|
||||||
onDelete: {}
|
onDelete: {}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ enum ItineraryRowItem: Identifiable, Equatable {
|
|||||||
case dayHeader(dayNumber: Int, date: Date, games: [RichGame])
|
case dayHeader(dayNumber: Int, date: Date, games: [RichGame])
|
||||||
case travel(TravelSegment, dayNumber: Int) // dayNumber = the day this travel is associated with
|
case travel(TravelSegment, dayNumber: Int) // dayNumber = the day this travel is associated with
|
||||||
case customItem(CustomItineraryItem)
|
case customItem(CustomItineraryItem)
|
||||||
case addButton(day: Int, anchorType: CustomItineraryItem.AnchorType, anchorId: String?)
|
case addButton(day: Int) // Simplified - just needs day
|
||||||
|
|
||||||
var id: String {
|
var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -37,8 +37,8 @@ enum ItineraryRowItem: Identifiable, Equatable {
|
|||||||
return "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
|
return "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
|
||||||
case .customItem(let item):
|
case .customItem(let item):
|
||||||
return "item:\(item.id.uuidString)"
|
return "item:\(item.id.uuidString)"
|
||||||
case .addButton(let day, let anchorType, let anchorId):
|
case .addButton(let day):
|
||||||
return "add:\(day)-\(anchorType.rawValue)-\(anchorId ?? "nil")"
|
return "add:\(day)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,10 +68,10 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
var onTravelMoved: ((String, Int) -> Void)? // travelId, newDay
|
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 onCustomItemTapped: ((CustomItineraryItem) -> Void)?
|
||||||
var onCustomItemDeleted: ((CustomItineraryItem) -> Void)?
|
var onCustomItemDeleted: ((CustomItineraryItem) -> Void)?
|
||||||
var onAddButtonTapped: ((Int, CustomItineraryItem.AnchorType, String?) -> Void)?
|
var onAddButtonTapped: ((Int) -> Void)? // Just day number
|
||||||
|
|
||||||
// Cell reuse identifiers
|
// Cell reuse identifiers
|
||||||
private let dayHeaderCellId = "DayHeaderCell"
|
private let dayHeaderCellId = "DayHeaderCell"
|
||||||
@@ -234,9 +234,9 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
configureCustomItemCell(cell, item: customItem)
|
configureCustomItemCell(cell, item: customItem)
|
||||||
return cell
|
return cell
|
||||||
|
|
||||||
case .addButton(let day, let anchorType, let anchorId):
|
case .addButton(let day):
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: addButtonCellId, for: indexPath)
|
let cell = tableView.dequeueReusableCell(withIdentifier: addButtonCellId, for: indexPath)
|
||||||
configureAddButtonCell(cell, day: day, anchorType: anchorType, anchorId: anchorId)
|
configureAddButtonCell(cell, day: day)
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,8 +266,8 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
|
|
||||||
case .customItem(let customItem):
|
case .customItem(let customItem):
|
||||||
let destinationDay = dayNumber(forRow: destinationIndexPath.row)
|
let destinationDay = dayNumber(forRow: destinationIndexPath.row)
|
||||||
let (anchorType, anchorId) = determineAnchor(at: destinationIndexPath.row)
|
let sortOrder = calculateSortOrder(at: destinationIndexPath.row)
|
||||||
onCustomItemMoved?(customItem.id, destinationDay, anchorType, anchorId)
|
onCustomItemMoved?(customItem.id, destinationDay, sortOrder)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
@@ -403,8 +403,8 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
case .customItem(let customItem):
|
case .customItem(let customItem):
|
||||||
onCustomItemTapped?(customItem)
|
onCustomItemTapped?(customItem)
|
||||||
|
|
||||||
case .addButton(let day, let anchorType, let anchorId):
|
case .addButton(let day):
|
||||||
onAddButtonTapped?(day, anchorType, anchorId)
|
onAddButtonTapped?(day)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
@@ -435,53 +435,64 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
|
|
||||||
// MARK: - Helper Methods
|
// MARK: - Helper Methods
|
||||||
|
|
||||||
private func determineAnchor(at row: Int) -> (CustomItineraryItem.AnchorType, String?) {
|
/// Calculate the sortOrder for an item dropped at the given row position
|
||||||
// Scan backwards to find the day's context
|
/// Uses midpoint insertion: if between sortOrder 1.0 and 2.0, returns 1.5
|
||||||
// Structure: travel (optional) -> dayHeader -> items
|
private func calculateSortOrder(at row: Int) -> Double {
|
||||||
var foundTravel: TravelSegment?
|
// Find adjacent custom items to calculate midpoint
|
||||||
var foundDayGames: [RichGame] = []
|
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) {
|
for i in stride(from: row - 1, through: 0, by: -1) {
|
||||||
switch flatItems[i] {
|
switch flatItems[i] {
|
||||||
case .travel(let segment, _):
|
case .customItem(let item):
|
||||||
// Found travel - if this is the first significant item, use afterTravel
|
prevSortOrder = item.sortOrder
|
||||||
// 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
|
|
||||||
break
|
break
|
||||||
|
case .dayHeader, .travel:
|
||||||
case .dayHeader(_, _, let games):
|
// Hit a boundary - no previous item in this section
|
||||||
// Found the day header for this section
|
break
|
||||||
foundDayGames = games
|
case .addButton:
|
||||||
// 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
|
|
||||||
continue
|
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
|
// Scan forwards for next custom item in same day
|
||||||
if let segment = foundTravel {
|
for i in row..<flatItems.count {
|
||||||
let travelId = "travel:\(segment.fromLocation.name.lowercased())->\(segment.toLocation.name.lowercased())"
|
switch flatItems[i] {
|
||||||
return (.afterTravel, travelId)
|
case .customItem(let item):
|
||||||
|
nextSortOrder = item.sortOrder
|
||||||
|
break
|
||||||
|
case .dayHeader, .travel:
|
||||||
|
// Hit a boundary - no next item in this section
|
||||||
|
break
|
||||||
|
case .addButton:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if nextSortOrder != nil { break }
|
||||||
|
// If we hit dayHeader or travel, stop scanning
|
||||||
|
if case .dayHeader = flatItems[i] { break }
|
||||||
|
if case .travel = flatItems[i] { break }
|
||||||
}
|
}
|
||||||
|
|
||||||
return (.startOfDay, nil)
|
// Calculate appropriate sortOrder
|
||||||
|
switch (prevSortOrder, nextSortOrder) {
|
||||||
|
case (nil, nil):
|
||||||
|
// No adjacent items - use 1.0
|
||||||
|
return 1.0
|
||||||
|
case (let prev?, nil):
|
||||||
|
// After last item - use prev + 1.0
|
||||||
|
return prev + 1.0
|
||||||
|
case (nil, let next?):
|
||||||
|
// Before first item - use next / 2.0
|
||||||
|
return next / 2.0
|
||||||
|
case (let prev?, let next?):
|
||||||
|
// Between two items - use midpoint
|
||||||
|
return (prev + next) / 2.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Cell Configuration
|
// MARK: - Cell Configuration
|
||||||
@@ -519,7 +530,7 @@ final class ItineraryTableViewController: UITableViewController {
|
|||||||
cell.selectionStyle = .default
|
cell.selectionStyle = .default
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configureAddButtonCell(_ cell: UITableViewCell, day: Int, anchorType: CustomItineraryItem.AnchorType, anchorId: String?) {
|
private func configureAddButtonCell(_ cell: UITableViewCell, day: Int) {
|
||||||
cell.contentConfiguration = UIHostingConfiguration {
|
cell.contentConfiguration = UIHostingConfiguration {
|
||||||
AddButtonRowView(colorScheme: colorScheme)
|
AddButtonRowView(colorScheme: colorScheme)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
|
|||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
var onTravelMoved: ((String, Int) -> Void)?
|
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 onCustomItemTapped: ((CustomItineraryItem) -> Void)?
|
||||||
var onCustomItemDeleted: ((CustomItineraryItem) -> Void)?
|
var onCustomItemDeleted: ((CustomItineraryItem) -> Void)?
|
||||||
var onAddButtonTapped: ((Int, CustomItineraryItem.AnchorType, String?) -> Void)?
|
var onAddButtonTapped: ((Int) -> Void)? // Just day number
|
||||||
|
|
||||||
init(
|
init(
|
||||||
trip: Trip,
|
trip: Trip,
|
||||||
@@ -30,10 +30,10 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
|
|||||||
travelDayOverrides: [String: Int],
|
travelDayOverrides: [String: Int],
|
||||||
@ViewBuilder headerContent: () -> HeaderContent,
|
@ViewBuilder headerContent: () -> HeaderContent,
|
||||||
onTravelMoved: ((String, Int) -> Void)? = nil,
|
onTravelMoved: ((String, Int) -> Void)? = nil,
|
||||||
onCustomItemMoved: ((UUID, Int, CustomItineraryItem.AnchorType, String?) -> Void)? = nil,
|
onCustomItemMoved: ((UUID, Int, Double) -> Void)? = nil,
|
||||||
onCustomItemTapped: ((CustomItineraryItem) -> Void)? = nil,
|
onCustomItemTapped: ((CustomItineraryItem) -> Void)? = nil,
|
||||||
onCustomItemDeleted: ((CustomItineraryItem) -> Void)? = nil,
|
onCustomItemDeleted: ((CustomItineraryItem) -> Void)? = nil,
|
||||||
onAddButtonTapped: ((Int, CustomItineraryItem.AnchorType, String?) -> Void)? = nil
|
onAddButtonTapped: ((Int) -> Void)? = nil
|
||||||
) {
|
) {
|
||||||
self.trip = trip
|
self.trip = trip
|
||||||
self.games = games
|
self.games = games
|
||||||
@@ -159,47 +159,16 @@ struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresent
|
|||||||
// Travel before this day (travel is stored on the destination day)
|
// Travel before this day (travel is stored on the destination day)
|
||||||
let travelBefore: TravelSegment? = travelByDay[dayNum]
|
let travelBefore: TravelSegment? = travelByDay[dayNum]
|
||||||
|
|
||||||
// Custom items after travel (if travel arrives on this day)
|
// Custom items for this day - simply filter by day and sort by sortOrder
|
||||||
if let travelSegment = travelBefore {
|
let dayItems = customItems.filter { $0.day == dayNum }
|
||||||
let travelId = stableTravelAnchorId(travelSegment)
|
.sorted { $0.sortOrder < $1.sortOrder }
|
||||||
let itemsAfterTravel = customItems.filter {
|
|
||||||
$0.anchorType == .afterTravel && $0.anchorId == travelId
|
|
||||||
}.sorted { $0.sortOrder < $1.sortOrder }
|
|
||||||
|
|
||||||
for item in itemsAfterTravel {
|
for item in dayItems {
|
||||||
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 {
|
|
||||||
items.append(ItineraryRowItem.customItem(item))
|
items.append(ItineraryRowItem.customItem(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom items after game
|
// ONE Add button per day
|
||||||
if let lastGame = gamesOnDay.last {
|
items.append(ItineraryRowItem.addButton(day: dayNum))
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
let dayData = ItineraryDayData(
|
let dayData = ItineraryDayData(
|
||||||
id: dayNum,
|
id: dayNum,
|
||||||
|
|||||||
@@ -119,9 +119,7 @@ struct TripDetailView: View {
|
|||||||
.sheet(item: $addItemAnchor) { anchor in
|
.sheet(item: $addItemAnchor) { anchor in
|
||||||
AddItemSheet(
|
AddItemSheet(
|
||||||
tripId: trip.id,
|
tripId: trip.id,
|
||||||
anchorDay: anchor.day,
|
day: anchor.day,
|
||||||
anchorType: anchor.type,
|
|
||||||
anchorId: anchor.anchorId,
|
|
||||||
existingItem: nil
|
existingItem: nil
|
||||||
) { item in
|
) { item in
|
||||||
Task { await saveCustomItem(item) }
|
Task { await saveCustomItem(item) }
|
||||||
@@ -130,9 +128,7 @@ struct TripDetailView: View {
|
|||||||
.sheet(item: $editingItem) { item in
|
.sheet(item: $editingItem) { item in
|
||||||
AddItemSheet(
|
AddItemSheet(
|
||||||
tripId: trip.id,
|
tripId: trip.id,
|
||||||
anchorDay: item.anchorDay,
|
day: item.day,
|
||||||
anchorType: item.anchorType,
|
|
||||||
anchorId: item.anchorId,
|
|
||||||
existingItem: item
|
existingItem: item
|
||||||
) { updatedItem in
|
) { updatedItem in
|
||||||
Task { await saveCustomItem(updatedItem) }
|
Task { await saveCustomItem(updatedItem) }
|
||||||
@@ -158,7 +154,7 @@ struct TripDetailView: View {
|
|||||||
// Recalculate routes when custom items change (mappable items affect route)
|
// Recalculate routes when custom items change (mappable items affect route)
|
||||||
print("🗺️ [MapUpdate] customItems changed, count: \(newItems.count)")
|
print("🗺️ [MapUpdate] customItems changed, count: \(newItems.count)")
|
||||||
for item in newItems where item.isMappable {
|
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 {
|
Task {
|
||||||
updateMapRegion()
|
updateMapRegion()
|
||||||
@@ -224,10 +220,10 @@ struct TripDetailView: View {
|
|||||||
await saveTravelDayOverride(travelAnchorId: travelId, displayDay: newDay)
|
await saveTravelDayOverride(travelAnchorId: travelId, displayDay: newDay)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onCustomItemMoved: { itemId, day, anchorType, anchorId in
|
onCustomItemMoved: { itemId, day, sortOrder in
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
guard let item = customItems.first(where: { $0.id == itemId }) else { return }
|
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
|
onCustomItemTapped: { item in
|
||||||
@@ -236,8 +232,8 @@ struct TripDetailView: View {
|
|||||||
onCustomItemDeleted: { item in
|
onCustomItemDeleted: { item in
|
||||||
Task { await deleteCustomItem(item) }
|
Task { await deleteCustomItem(item) }
|
||||||
},
|
},
|
||||||
onAddButtonTapped: { day, anchorType, anchorId in
|
onAddButtonTapped: { day in
|
||||||
addItemAnchor = AddItemAnchor(day: day, type: anchorType, anchorId: anchorId)
|
addItemAnchor = AddItemAnchor(day: day)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.ignoresSafeArea(edges: .bottom)
|
.ignoresSafeArea(edges: .bottom)
|
||||||
@@ -605,13 +601,13 @@ struct TripDetailView: View {
|
|||||||
handleCustomItemDrop(providers: providers, targetItem: item)
|
handleCustomItemDrop(providers: providers, targetItem: item)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .addButton(let day, let anchorType, let anchorId):
|
case .addButton(let day):
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
if isDropTarget {
|
if isDropTarget {
|
||||||
DropTargetIndicator()
|
DropTargetIndicator()
|
||||||
}
|
}
|
||||||
InlineAddButton {
|
InlineAddButton {
|
||||||
addItemAnchor = AddItemAnchor(day: day, type: anchorType, anchorId: anchorId)
|
addItemAnchor = AddItemAnchor(day: day)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onDrop(of: [.text, .plainText, .utf8PlainText], isTargeted: Binding(
|
.onDrop(of: [.text, .plainText, .utf8PlainText], isTargeted: Binding(
|
||||||
@@ -628,7 +624,7 @@ struct TripDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)) { providers in
|
)) { 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
|
Task { @MainActor in
|
||||||
let day = self.findDayForTravelSegment(segment)
|
let day = self.findDayForTravelSegment(segment)
|
||||||
let stableAnchorId = self.stableTravelAnchorId(segment)
|
// Place at beginning of day (sortOrder before existing items)
|
||||||
await self.moveItemToBeginning(droppedItem, toDay: day, anchorType: .afterTravel, anchorId: stableAnchorId)
|
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
|
return true
|
||||||
@@ -676,13 +676,19 @@ struct TripDetailView: View {
|
|||||||
droppedItem.id != targetItem.id else { return }
|
droppedItem.id != targetItem.id else { return }
|
||||||
|
|
||||||
Task { @MainActor in
|
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
|
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 {
|
guard let provider = providers.first, provider.canLoadObject(ofClass: NSString.self) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -698,7 +704,12 @@ struct TripDetailView: View {
|
|||||||
let droppedItem = self.customItems.first(where: { $0.id == itemId }) else { return }
|
let droppedItem = self.customItems.first(where: { $0.id == itemId }) else { return }
|
||||||
|
|
||||||
Task { @MainActor in
|
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
|
return true
|
||||||
@@ -719,8 +730,8 @@ struct TripDetailView: View {
|
|||||||
return "travel-\(segment.fromLocation.name)-\(segment.toLocation.name)"
|
return "travel-\(segment.fromLocation.name)-\(segment.toLocation.name)"
|
||||||
case .customItem(let item):
|
case .customItem(let item):
|
||||||
return "item-\(item.id.uuidString)"
|
return "item-\(item.id.uuidString)"
|
||||||
case .addButton(let day, let anchorType, _):
|
case .addButton(let day):
|
||||||
return "add-\(day)-\(anchorType.rawValue)"
|
return "add-\(day)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -747,120 +758,26 @@ struct TripDetailView: View {
|
|||||||
return "travel:\(from)->\(to)"
|
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
|
var updated = item
|
||||||
updated.anchorDay = day
|
updated.day = day
|
||||||
updated.anchorType = anchorType
|
updated.sortOrder = sortOrder
|
||||||
updated.anchorId = anchorId
|
|
||||||
updated.modifiedAt = Date()
|
updated.modifiedAt = Date()
|
||||||
|
|
||||||
// Calculate sortOrder
|
print("📍 [Move] Moving \(item.title) to day \(day), sortOrder: \(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..<itemsAtSameAnchor.count {
|
|
||||||
if var shiftItem = customItems.first(where: { $0.id == itemsAtSameAnchor[i].id }) {
|
|
||||||
shiftItem.sortOrder = i + 1
|
|
||||||
shiftItem.modifiedAt = Date()
|
|
||||||
print("📍 [Move] Shifting \(shiftItem.title) sortOrder to \(i + 1)")
|
|
||||||
if let idx = customItems.firstIndex(where: { $0.id == shiftItem.id }) {
|
|
||||||
customItems[idx] = shiftItem
|
|
||||||
}
|
|
||||||
itemsToSync.append(shiftItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updated.sortOrder = itemsAtSameAnchor.count
|
|
||||||
print("📍 [Move] Setting \(item.title) sortOrder to \(itemsAtSameAnchor.count) (end of list)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update local state
|
// Update local state
|
||||||
if let idx = customItems.firstIndex(where: { $0.id == item.id }) {
|
if let idx = customItems.firstIndex(where: { $0.id == item.id }) {
|
||||||
customItems[idx] = updated
|
customItems[idx] = updated
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync moved item and all shifted items to CloudKit
|
|
||||||
do {
|
|
||||||
_ = try await CustomItemService.shared.updateItem(updated)
|
|
||||||
print("✅ [Move] Synced \(updated.title) to day \(day), anchor: \(anchorType.rawValue), sortOrder: \(updated.sortOrder)")
|
|
||||||
|
|
||||||
// Also sync shifted items
|
|
||||||
for shiftItem in itemsToSync {
|
|
||||||
_ = try await CustomItemService.shared.updateItem(shiftItem)
|
|
||||||
print("✅ [Move] Synced shifted item \(shiftItem.title) with sortOrder: \(shiftItem.sortOrder)")
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
print("❌ [Move] Failed to sync: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Move item to the beginning of an anchor position (sortOrder 0), shifting existing items down
|
|
||||||
private func moveItemToBeginning(_ item: CustomItineraryItem, toDay day: Int, anchorType: CustomItineraryItem.AnchorType, anchorId: String?) async {
|
|
||||||
var updated = item
|
|
||||||
updated.anchorDay = day
|
|
||||||
updated.anchorType = anchorType
|
|
||||||
updated.anchorId = anchorId
|
|
||||||
updated.sortOrder = 0 // Insert at beginning
|
|
||||||
updated.modifiedAt = Date()
|
|
||||||
|
|
||||||
// Get existing items at this anchor position (excluding the moved item)
|
|
||||||
let existingItems = customItems.filter {
|
|
||||||
$0.anchorDay == day &&
|
|
||||||
$0.anchorType == anchorType &&
|
|
||||||
$0.anchorId == anchorId &&
|
|
||||||
$0.id != item.id
|
|
||||||
}.sorted { $0.sortOrder < $1.sortOrder }
|
|
||||||
|
|
||||||
print("📍 [MoveToBeginning] Moving \(item.title) to beginning of day \(day), anchor: \(anchorType.rawValue)")
|
|
||||||
print("📍 [MoveToBeginning] Existing items to shift: \(existingItems.map { "\($0.title) (order: \($0.sortOrder))" })")
|
|
||||||
|
|
||||||
// Shift all existing items down by 1
|
|
||||||
var itemsToSync: [CustomItineraryItem] = []
|
|
||||||
for (index, existingItem) in existingItems.enumerated() {
|
|
||||||
if var shiftItem = customItems.first(where: { $0.id == existingItem.id }) {
|
|
||||||
shiftItem.sortOrder = index + 1 // Shift down
|
|
||||||
shiftItem.modifiedAt = Date()
|
|
||||||
print("📍 [MoveToBeginning] Shifting \(shiftItem.title) sortOrder to \(index + 1)")
|
|
||||||
if let idx = customItems.firstIndex(where: { $0.id == shiftItem.id }) {
|
|
||||||
customItems[idx] = shiftItem
|
|
||||||
}
|
|
||||||
itemsToSync.append(shiftItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update local state for the moved item
|
|
||||||
if let idx = customItems.firstIndex(where: { $0.id == item.id }) {
|
|
||||||
customItems[idx] = updated
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync to CloudKit
|
// Sync to CloudKit
|
||||||
do {
|
do {
|
||||||
_ = try await CustomItemService.shared.updateItem(updated)
|
_ = try await CustomItemService.shared.updateItem(updated)
|
||||||
print("✅ [MoveToBeginning] Synced \(updated.title) with sortOrder: 0")
|
print("✅ [Move] Synced \(updated.title) with day: \(day), sortOrder: \(sortOrder)")
|
||||||
|
|
||||||
for shiftItem in itemsToSync {
|
|
||||||
_ = try await CustomItemService.shared.updateItem(shiftItem)
|
|
||||||
print("✅ [MoveToBeginning] Synced shifted item \(shiftItem.title) with sortOrder: \(shiftItem.sortOrder)")
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
print("❌ [MoveToBeginning] Failed to sync: \(error)")
|
print("❌ [Move] Failed to sync: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -909,60 +826,25 @@ struct TripDetailView: View {
|
|||||||
for (index, dayDate) in days.enumerated() {
|
for (index, dayDate) in days.enumerated() {
|
||||||
let dayNum = index + 1
|
let dayNum = index + 1
|
||||||
let gamesOnDay = gamesOn(date: dayDate)
|
let gamesOnDay = gamesOn(date: dayDate)
|
||||||
let isRestDay = gamesOnDay.isEmpty
|
|
||||||
|
|
||||||
// Travel for this day (if any)
|
// Travel for this day (if any) - appears before day header
|
||||||
if let travelSegment = travelByDay[dayNum] {
|
if let travelSegment = travelByDay[dayNum] {
|
||||||
sections.append(.travel(travelSegment))
|
sections.append(.travel(travelSegment))
|
||||||
|
|
||||||
if allowCustomItems {
|
|
||||||
let stableId = stableTravelAnchorId(travelSegment)
|
|
||||||
|
|
||||||
// Add button after travel
|
|
||||||
sections.append(.addButton(day: dayNum, anchorType: .afterTravel, anchorId: stableId))
|
|
||||||
|
|
||||||
// Custom items after this travel (sorted by sortOrder)
|
|
||||||
let itemsAfterTravel = customItems.filter {
|
|
||||||
$0.anchorType == .afterTravel && $0.anchorId == stableId
|
|
||||||
}.sorted { $0.sortOrder < $1.sortOrder }
|
|
||||||
for item in itemsAfterTravel {
|
|
||||||
sections.append(.customItem(item))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom items at start of day (before games or as main content for rest days)
|
|
||||||
if allowCustomItems {
|
|
||||||
let itemsAtStart = customItems.filter {
|
|
||||||
$0.anchorDay == dayNum && $0.anchorType == .startOfDay
|
|
||||||
}.sorted { $0.sortOrder < $1.sortOrder }
|
|
||||||
for item in itemsAtStart {
|
|
||||||
sections.append(.customItem(item))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Day section - shows games or minimal rest day display
|
// Day section - shows games or minimal rest day display
|
||||||
sections.append(.day(dayNumber: dayNum, date: dayDate, games: gamesOnDay))
|
sections.append(.day(dayNumber: dayNum, date: dayDate, games: gamesOnDay))
|
||||||
|
|
||||||
// Add button after day (different anchor for game days vs rest days)
|
// Custom items for this day (sorted by sortOrder)
|
||||||
if allowCustomItems {
|
if allowCustomItems {
|
||||||
if isRestDay {
|
let dayItems = customItems.filter { $0.day == dayNum }
|
||||||
// Rest day: add button anchored to start of day (no games to anchor to)
|
.sorted { $0.sortOrder < $1.sortOrder }
|
||||||
sections.append(.addButton(day: dayNum, anchorType: .startOfDay, anchorId: nil))
|
for item in dayItems {
|
||||||
} else if let lastGame = gamesOnDay.last {
|
sections.append(.customItem(item))
|
||||||
// Game day: add button anchored after last game
|
|
||||||
sections.append(.addButton(day: dayNum, anchorType: .afterGame, anchorId: lastGame.game.id))
|
|
||||||
|
|
||||||
// Custom items after this game (sorted by sortOrder)
|
|
||||||
let itemsAfterGame = customItems.filter {
|
|
||||||
$0.anchorDay == dayNum &&
|
|
||||||
$0.anchorType == .afterGame &&
|
|
||||||
$0.anchorId == lastGame.game.id
|
|
||||||
}.sorted { $0.sortOrder < $1.sortOrder }
|
|
||||||
for item in itemsAfterGame {
|
|
||||||
sections.append(.customItem(item))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One add button per day
|
||||||
|
sections.append(.addButton(day: dayNum))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1153,13 +1035,13 @@ struct TripDetailView: View {
|
|||||||
/// Route waypoints including both game stops and mappable custom items in itinerary order
|
/// Route waypoints including both game stops and mappable custom items in itinerary order
|
||||||
private var routeWaypoints: [(name: String, coordinate: CLLocationCoordinate2D, isCustomItem: Bool)] {
|
private var routeWaypoints: [(name: String, coordinate: CLLocationCoordinate2D, isCustomItem: Bool)] {
|
||||||
// Build an ordered list combining game stops and mappable custom items
|
// Build an ordered list combining game stops and mappable custom items
|
||||||
// Group custom items by day
|
// Items are ordered by (day, sortOrder) - visual order matches route order
|
||||||
let itemsByDay = Dictionary(grouping: mappableCustomItems) { $0.anchorDay }
|
let itemsByDay = Dictionary(grouping: mappableCustomItems) { $0.day }
|
||||||
|
|
||||||
print("🗺️ [Waypoints] Building waypoints. Mappable items by day:")
|
print("🗺️ [Waypoints] Building waypoints. Mappable items by day:")
|
||||||
for (day, items) in itemsByDay.sorted(by: { $0.key < $1.key }) {
|
for (day, items) in itemsByDay.sorted(by: { $0.key < $1.key }) {
|
||||||
for item in items {
|
for item in items.sorted(by: { $0.sortOrder < $1.sortOrder }) {
|
||||||
print("🗺️ [Waypoints] Day \(day): \(item.title), anchor: \(item.anchorType.rawValue)")
|
print("🗺️ [Waypoints] Day \(day): \(item.title), sortOrder: \(item.sortOrder)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1183,18 +1065,6 @@ struct TripDetailView: View {
|
|||||||
|
|
||||||
print("🗺️ [Waypoints] Day \(dayNumber): city=\(dayCity ?? "none"), games=\(gamesOnDay.count)")
|
print("🗺️ [Waypoints] Day \(dayNumber): city=\(dayCity ?? "none"), games=\(gamesOnDay.count)")
|
||||||
|
|
||||||
// Custom items at start of day (before games)
|
|
||||||
if let items = itemsByDay[dayNumber] {
|
|
||||||
let startItems = items.filter { $0.anchorType == .startOfDay }
|
|
||||||
.sorted { $0.sortOrder < $1.sortOrder }
|
|
||||||
for item in startItems {
|
|
||||||
if let coord = item.coordinate {
|
|
||||||
print("🗺️ [Waypoints] Adding \(item.title) (startOfDay)")
|
|
||||||
waypoints.append((item.title, coord, true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Game stop for this day (only add once per city to avoid duplicates)
|
// Game stop for this day (only add once per city to avoid duplicates)
|
||||||
if let city = dayCity {
|
if let city = dayCity {
|
||||||
// Check if we already have this city in waypoints (by city name or stadium name)
|
// Check if we already have this city in waypoints (by city name or stadium name)
|
||||||
@@ -1227,23 +1097,12 @@ struct TripDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom items after games
|
// Custom items for this day (ordered by sortOrder - visual order matches route)
|
||||||
if let items = itemsByDay[dayNumber] {
|
if let items = itemsByDay[dayNumber] {
|
||||||
let afterGameItems = items.filter { $0.anchorType == .afterGame }
|
let sortedItems = items.sorted { $0.sortOrder < $1.sortOrder }
|
||||||
.sorted { $0.sortOrder < $1.sortOrder }
|
for item in sortedItems {
|
||||||
for item in afterGameItems {
|
|
||||||
if let coord = item.coordinate {
|
if let coord = item.coordinate {
|
||||||
print("🗺️ [Waypoints] Adding \(item.title) (afterGame)")
|
print("🗺️ [Waypoints] Adding \(item.title) (sortOrder: \(item.sortOrder))")
|
||||||
waypoints.append((item.title, coord, true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom items after travel
|
|
||||||
let afterTravelItems = items.filter { $0.anchorType == .afterTravel }
|
|
||||||
.sorted { $0.sortOrder < $1.sortOrder }
|
|
||||||
for item in afterTravelItems {
|
|
||||||
if let coord = item.coordinate {
|
|
||||||
print("🗺️ [Waypoints] Adding \(item.title) (afterTravel)")
|
|
||||||
waypoints.append((item.title, coord, true))
|
waypoints.append((item.title, coord, true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1437,7 +1296,7 @@ struct TripDetailView: View {
|
|||||||
let isUpdate = customItems.contains(where: { $0.id == item.id })
|
let isUpdate = customItems.contains(where: { $0.id == item.id })
|
||||||
print("💾 [CustomItems] Saving item: '\(item.title)' (isUpdate: \(isUpdate))")
|
print("💾 [CustomItems] Saving item: '\(item.title)' (isUpdate: \(isUpdate))")
|
||||||
print(" - tripId: \(item.tripId)")
|
print(" - tripId: \(item.tripId)")
|
||||||
print(" - anchorDay: \(item.anchorDay), anchorType: \(item.anchorType.rawValue)")
|
print(" - day: \(item.day), sortOrder: \(item.sortOrder)")
|
||||||
|
|
||||||
// Update local state immediately for responsive UI
|
// Update local state immediately for responsive UI
|
||||||
if isUpdate {
|
if isUpdate {
|
||||||
@@ -1518,13 +1377,12 @@ struct TripDetailView: View {
|
|||||||
guard let itemId = UUID(uuidString: droppedId),
|
guard let itemId = UUID(uuidString: droppedId),
|
||||||
let item = self.customItems.first(where: { $0.id == itemId }) else { return }
|
let item = self.customItems.first(where: { $0.id == itemId }) else { return }
|
||||||
|
|
||||||
// For game days, anchor to last game; for rest days, anchor to start of day
|
// Append at end of day's items
|
||||||
if let lastGame = gamesOnDay.last {
|
let maxSortOrder = self.customItems
|
||||||
await self.moveItemToBeginning(item, toDay: dayNumber, anchorType: .afterGame, anchorId: lastGame.game.id)
|
.filter { $0.day == dayNumber && $0.id != item.id }
|
||||||
} else {
|
.map { $0.sortOrder }
|
||||||
// Rest day - anchor to start of day
|
.max() ?? 0.0
|
||||||
await self.moveItemToBeginning(item, toDay: dayNumber, anchorType: .startOfDay, anchorId: nil)
|
await self.moveItem(item, toDay: dayNumber, sortOrder: maxSortOrder + 1.0)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -1556,7 +1414,7 @@ enum ItinerarySection {
|
|||||||
case day(dayNumber: Int, date: Date, games: [RichGame])
|
case day(dayNumber: Int, date: Date, games: [RichGame])
|
||||||
case travel(TravelSegment)
|
case travel(TravelSegment)
|
||||||
case customItem(CustomItineraryItem)
|
case customItem(CustomItineraryItem)
|
||||||
case addButton(day: Int, anchorType: CustomItineraryItem.AnchorType, anchorId: String?)
|
case addButton(day: Int)
|
||||||
|
|
||||||
var isCustomItem: Bool {
|
var isCustomItem: Bool {
|
||||||
if case .customItem = self { return true }
|
if case .customItem = self { return true }
|
||||||
@@ -1569,8 +1427,6 @@ enum ItinerarySection {
|
|||||||
struct AddItemAnchor: Identifiable {
|
struct AddItemAnchor: Identifiable {
|
||||||
let id = UUID()
|
let id = UUID()
|
||||||
let day: Int
|
let day: Int
|
||||||
let type: CustomItineraryItem.AnchorType
|
|
||||||
let anchorId: String?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Inline Add Button
|
// MARK: - Inline Add Button
|
||||||
|
|||||||
@@ -16,15 +16,44 @@ struct CustomItineraryItemTests {
|
|||||||
tripId: tripId,
|
tripId: tripId,
|
||||||
category: .restaurant,
|
category: .restaurant,
|
||||||
title: "Joe's BBQ",
|
title: "Joe's BBQ",
|
||||||
anchorDay: 1
|
day: 1
|
||||||
)
|
)
|
||||||
|
|
||||||
#expect(item.tripId == tripId)
|
#expect(item.tripId == tripId)
|
||||||
#expect(item.category == .restaurant)
|
#expect(item.category == .restaurant)
|
||||||
#expect(item.title == "Joe's BBQ")
|
#expect(item.title == "Joe's BBQ")
|
||||||
#expect(item.anchorType == .startOfDay)
|
#expect(item.day == 1)
|
||||||
#expect(item.anchorId == nil)
|
#expect(item.sortOrder == 0.0)
|
||||||
#expect(item.anchorDay == 1)
|
}
|
||||||
|
|
||||||
|
@Test("Item initializes with day and sortOrder")
|
||||||
|
func item_InitializesWithDayAndSortOrder() {
|
||||||
|
let tripId = UUID()
|
||||||
|
let item = CustomItineraryItem(
|
||||||
|
tripId: tripId,
|
||||||
|
category: .restaurant,
|
||||||
|
title: "Joe's BBQ",
|
||||||
|
day: 1,
|
||||||
|
sortOrder: 1.5
|
||||||
|
)
|
||||||
|
|
||||||
|
#expect(item.tripId == tripId)
|
||||||
|
#expect(item.category == .restaurant)
|
||||||
|
#expect(item.title == "Joe's BBQ")
|
||||||
|
#expect(item.day == 1)
|
||||||
|
#expect(item.sortOrder == 1.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("SortOrder defaults to 0.0")
|
||||||
|
func sortOrder_DefaultsToZero() {
|
||||||
|
let item = CustomItineraryItem(
|
||||||
|
tripId: UUID(),
|
||||||
|
category: .activity,
|
||||||
|
title: "City Tour",
|
||||||
|
day: 2
|
||||||
|
)
|
||||||
|
|
||||||
|
#expect(item.sortOrder == 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Item category has correct icons")
|
@Test("Item category has correct icons")
|
||||||
@@ -41,9 +70,8 @@ struct CustomItineraryItemTests {
|
|||||||
tripId: UUID(),
|
tripId: UUID(),
|
||||||
category: .hotel,
|
category: .hotel,
|
||||||
title: "Hilton Downtown",
|
title: "Hilton Downtown",
|
||||||
anchorType: .afterGame,
|
day: 2,
|
||||||
anchorId: "game_123",
|
sortOrder: 3.5
|
||||||
anchorDay: 2
|
|
||||||
)
|
)
|
||||||
|
|
||||||
let encoded = try JSONEncoder().encode(item)
|
let encoded = try JSONEncoder().encode(item)
|
||||||
@@ -51,7 +79,7 @@ struct CustomItineraryItemTests {
|
|||||||
|
|
||||||
#expect(decoded.id == item.id)
|
#expect(decoded.id == item.id)
|
||||||
#expect(decoded.title == item.title)
|
#expect(decoded.title == item.title)
|
||||||
#expect(decoded.anchorType == .afterGame)
|
#expect(decoded.day == 2)
|
||||||
#expect(decoded.anchorId == "game_123")
|
#expect(decoded.sortOrder == 3.5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user