# 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

<!--
REORDERING OPTIONS (for future reference):
1. Edit mode toggle - User taps "Edit" to show drag handles, then "Done" (like Reminders)
2. Long-press to drag - No edit mode, just long-press and drag
3. Always show handles - Custom items always have visible drag handle (CURRENT CHOICE)
-->

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.
