From b534ca771ba0b3ba0b6e7cde5eebebd358ddb7a0 Mon Sep 17 00:00:00 2001 From: Trey t Date: Thu, 15 Jan 2026 23:07:18 -0600 Subject: [PATCH] docs: add custom itinerary items design Design for allowing users to add personal items (restaurants, hotels, activities, notes) to saved trip itineraries with drag-to-reorder and CloudKit sync. Co-Authored-By: Claude Opus 4.5 --- ...026-01-15-custom-itinerary-items-design.md | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 docs/plans/2026-01-15-custom-itinerary-items-design.md diff --git a/docs/plans/2026-01-15-custom-itinerary-items-design.md b/docs/plans/2026-01-15-custom-itinerary-items-design.md new file mode 100644 index 0000000..1367441 --- /dev/null +++ b/docs/plans/2026-01-15-custom-itinerary-items-design.md @@ -0,0 +1,195 @@ +# 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.