refactor(itinerary): replace CustomItineraryItem with ItineraryItem across codebase

- Update CKModels.swift to remove deleted type references
- Migrate LocalCustomItem to LocalItineraryItem in SavedTrip.swift
- Update AppDelegate to handle subscription removal
- Refactor AddItemSheet to create ItineraryItem with CustomInfo
- Update ItineraryTableViewController and Wrapper for new model
- Refactor TripDetailView state, methods and callbacks
- Fix TripMapView to display custom items with new model structure

This completes the migration from the legacy CustomItineraryItem/TravelDayOverride
model to the unified ItineraryItem model with ItemKind enum.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-17 21:51:32 -06:00
parent b008af1c71
commit cd00384010
7 changed files with 273 additions and 354 deletions

View File

@@ -151,11 +151,11 @@
// - Parameters: itemId, newDay (1-indexed), newSortOrder
// - Parent updates item and syncs to CloudKit
//
// onCustomItemTapped: ((CustomItineraryItem) -> Void)?
// onCustomItemTapped: ((ItineraryItem) -> Void)?
// - Called when user taps custom item row
// - Parent presents edit sheet
//
// onCustomItemDeleted: ((CustomItineraryItem) -> Void)?
// onCustomItemDeleted: ((ItineraryItem) -> Void)?
// - Called from context menu delete action
// - Parent deletes from CloudKit
//
@@ -209,8 +209,8 @@
//
// - ItineraryTableViewWrapper.swift: SwiftUI bridge, data transformation
// - TripDetailView.swift: Parent view, owns state, handles callbacks
// - CustomItineraryItem.swift: Domain model with (day, sortOrder) positioning
// - CustomItemService.swift: CloudKit persistence for custom items
// - ItineraryItem.swift: Domain model with (day, sortOrder, kind) positioning
// - ItineraryItemService.swift: CloudKit persistence for itinerary items
//
//
@@ -250,7 +250,7 @@ enum ItineraryRowItem: Identifiable, Equatable {
case dayHeader(dayNumber: Int, date: Date) // Fixed: structural anchor (includes Add button)
case games([RichGame], dayNumber: Int) // Fixed: games are trip-determined
case travel(TravelSegment, dayNumber: Int) // Reorderable: within valid range
case customItem(CustomItineraryItem) // Reorderable: anywhere
case customItem(ItineraryItem) // Reorderable: anywhere
/// Stable identifier for table view diffing and external references.
/// Travel IDs are lowercase to ensure consistency across sessions.
@@ -297,8 +297,8 @@ final class ItineraryTableViewController: UITableViewController {
// Callbacks
var onTravelMoved: ((String, Int) -> Void)? // travelId, newDay
var onCustomItemMoved: ((UUID, Int, Double) -> Void)? // itemId, newDay, newSortOrder
var onCustomItemTapped: ((CustomItineraryItem) -> Void)?
var onCustomItemDeleted: ((CustomItineraryItem) -> Void)?
var onCustomItemTapped: ((ItineraryItem) -> Void)?
var onCustomItemDeleted: ((ItineraryItem) -> Void)?
var onAddButtonTapped: ((Int) -> Void)? // Just day number
// Cell reuse identifiers
@@ -981,7 +981,7 @@ final class ItineraryTableViewController: UITableViewController {
/// Custom item cell - shows user-added item with category icon.
/// Selectable (opens edit sheet on tap) and draggable.
private func configureCustomItemCell(_ cell: UITableViewCell, item: CustomItineraryItem) {
private func configureCustomItemCell(_ cell: UITableViewCell, item: ItineraryItem) {
cell.contentConfiguration = UIHostingConfiguration {
CustomItemRowView(item: item, colorScheme: colorScheme)
}
@@ -1225,36 +1225,42 @@ struct TravelRowView: View {
///
/// This row is both tappable (opens edit sheet) and draggable.
struct CustomItemRowView: View {
let item: CustomItineraryItem
let item: ItineraryItem
let colorScheme: ColorScheme
private var customInfo: CustomInfo? {
item.customInfo
}
var body: some View {
HStack(spacing: Theme.Spacing.sm) {
// Category icon (emoji)
Text(item.category.icon)
.font(.title3)
if let info = customInfo {
Text(info.icon)
.font(.title3)
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 4) {
Text(item.title)
.font(.subheadline)
.foregroundStyle(Theme.textPrimary(colorScheme))
.lineLimit(1)
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 4) {
Text(info.title)
.font(.subheadline)
.foregroundStyle(Theme.textPrimary(colorScheme))
.lineLimit(1)
// Map pin indicator for items with coordinates
if item.isMappable {
Image(systemName: "mappin.circle.fill")
.font(.caption)
.foregroundStyle(Theme.warmOrange)
// Map pin indicator for items with coordinates
if info.isMappable {
Image(systemName: "mappin.circle.fill")
.font(.caption)
.foregroundStyle(Theme.warmOrange)
}
}
}
// Address subtitle (shown only if present)
if let address = item.address, !address.isEmpty {
Text(address)
.font(.caption)
.foregroundStyle(Theme.textMuted(colorScheme))
.lineLimit(1)
// Address subtitle (shown only if present)
if let address = info.address, !address.isEmpty {
Text(address)
.font(.caption)
.foregroundStyle(Theme.textMuted(colorScheme))
.lineLimit(1)
}
}
}