# Custom Itinerary Items Design **Date:** 2026-01-15 **Status:** Approved ## Overview Allow users to add custom items (restaurants, hotels, activities, notes) to saved itineraries. Items can be repositioned anywhere among the system-generated content (games, travel segments) but system items remain fixed. ## Requirements - Categorized items with visual icons (Restaurant, Hotel, Activity, Note) - Inline "Add" buttons between each itinerary section - Always-visible drag handles on custom items (system items have no handles) - CloudKit sync across devices - Swipe to delete, tap to edit ## Data Model ```swift struct CustomItineraryItem: Identifiable, Codable, Hashable { let id: UUID let tripId: UUID var category: ItemCategory var title: String var anchorType: AnchorType // What this item is positioned after var anchorId: String? // ID of game or travel segment (nil = start of day) var anchorDay: Int // Day number (1-based) let createdAt: Date var modifiedAt: Date enum ItemCategory: String, Codable, CaseIterable { case restaurant, hotel, activity, 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" } } } enum AnchorType: String, Codable { case startOfDay // Before any games/travel on this day case afterGame // After a specific game case afterTravel // After a specific travel segment } } ``` **Why anchor-based positioning?** If a game gets rescheduled or removed, index-based positions would break. Anchoring to a specific game/travel ID is more stable. If the anchor is deleted, the item falls back to `startOfDay`. ## UI Components ### Inline "Add Item" Buttons Small, subtle `+ Add` buttons appear between each itinerary section: ``` Day 1 - Arlington ├─ 🎾 SFG @ TEX (12:05 AM) ├─ [+ Add] ├─ 🚗 Drive to Milwaukee ├─ [+ Add] └─ 🎾 PIT @ MIL (7:10 PM) [+ Add] Day 2 - Chicago ├─ [+ Add] ← Start of day ├─ 🎾 LAD @ CHC (1:20 PM) [+ Add] ``` ### Add Item Sheet A simple modal with: - Category picker (4 icons in a horizontal row, tap to select) - Text field for title - "Add" button ### Custom Item Row Displays in the itinerary with: - Category icon on the left - Title text - Drag handle (≡) on the right - always visible - Swipe left reveals red "Delete" action - Tap opens edit sheet (same as add, pre-filled) - Subtle warm background tint for visual distinction ## Reordering Logic Games and travel segments are fixed. Only custom items can be dragged. ### How it works 1. The itinerary renders as a flat list of mixed item types 2. When a custom item is dragged, it can only be dropped into valid "slots" between system items 3. On drop, the item's anchor is updated to reference what it's now positioned after ### Implementation approach Use custom drag-and-drop with `draggable()` and `dropDestination()` modifiers rather than `.onMove`. This gives precise control over what can be dragged and where it can land. ```swift ForEach(itinerarySections) { section in switch section { case .game, .travel: // Render normally, no drag handle // But IS a drop destination case .customItem: // Render with drag handle // draggable() + dropDestination() case .addButton: // Inline add button (also a drop destination) } } ``` ## CloudKit Sync ### New record type: `CustomItineraryItem` Fields: - `itemId` (String) - UUID - `tripId` (String) - References the SavedTrip - `category` (String) - restaurant/hotel/activity/note - `title` (String) - `anchorType` (String) - `anchorId` (String, optional) - `anchorDay` (Int) - `createdAt` (Date) - `modifiedAt` (Date) ### Sync approach Follows the same pattern as `PollVote`: - `CKCustomItineraryItem` wrapper struct in `CKModels.swift` - CRUD methods in a new `CustomItemService` actor - Items are fetched when loading a saved trip - Changes sync immediately on add/edit/delete/reorder ### Conflict resolution Last-write-wins based on `modifiedAt` for both content edits and position changes. ### Offline handling Custom items are also stored locally in SwiftData alongside `SavedTrip`. When offline, changes queue locally and sync when connection restores. ## Files to Create | File | Purpose | |------|---------| | `Core/Models/Domain/CustomItineraryItem.swift` | Domain model | | `Core/Models/CloudKit/CKCustomItineraryItem.swift` | CloudKit wrapper | | `Core/Services/CustomItemService.swift` | CloudKit CRUD operations | | `Features/Trip/Views/AddItemSheet.swift` | Modal for adding/editing items | | `Features/Trip/Views/CustomItemRow.swift` | Row component with drag handle | ## Files to Modify | File | Changes | |------|---------| | `TripDetailView.swift` | Add inline buttons, render custom items, handle drag/drop | | `SavedTrip.swift` | Add relationship to custom items (local cache) | | `CanonicalSyncService.swift` | Sync custom items with CloudKit | ## Out of Scope (YAGNI) - Time/duration fields on custom items - Location/address fields - Photos/attachments - Sharing custom items in polls - Notifications/reminders for custom items These can be added in future iterations if needed.