# Flexible Itinerary Ordering Design

## Overview

Enable fully flexible ordering of itinerary items while maintaining logical constraints. Users can arrange their day however they want - custom items can go before or after games, travel can be positioned anywhere within its valid day range.

## Current State

The current implementation has a fixed rendering order within each day:
1. Travel (if arriving this day) - appears BEFORE day header
2. Day Header + Add button
3. Games (bundled together)
4. Custom Items

Custom items are constrained to appear after games and cannot be placed before travel.

## New Behavior

### Ordering Rules

| Item Type | Drag Handle | Can Move To |
|-----------|-------------|-------------|
| Day Header | No | Fixed at top of day |
| Game | No | Has sortOrder for position, but immovable |
| Travel | Yes | Any position, day constrained to game cities' range |
| Custom Item | Yes | Any position, any day |

### Rendering Order Within a Day

```
1. Day Header (fixed, always first)
2. All orderable items sorted by sortOrder:
   - Games (sortOrder assigned, drag disabled)
   - Travel (sortOrder, drag enabled with day constraints)
   - Custom Items (sortOrder, drag enabled anywhere)
```

### Visual Example

User arranges their day:

```
Day 3 · Sunday, Mar 8              + Add
🏨 Hotel checkout              (sortOrder: 50.0)   ← custom, before game
🚗 Detroit → Milwaukee         (sortOrder: 75.0)   ← travel, before game
Milwaukee: NBA ORL @ MIL 8PM   (sortOrder: 100.0)  ← game (no drag handle)
🍽️ Post-game dinner            (sortOrder: 150.0)  ← custom, after game
```

## Data Model Changes

### Travel Position Storage

Replace simple day override with full position:

```swift
// Before:
@State private var travelDayOverrides: [String: Int] = [:]

// After:
@State private var travelPositions: [String: TravelPosition] = [:]

struct TravelPosition: Codable {
    var day: Int
    var sortOrder: Double
}
```

### Game SortOrder Assignment

Games receive automatic sortOrder values when the trip is loaded/created:

```swift
// Games for each day get sortOrder 100.0, 101.0, 102.0...
// This leaves room below (0-99) and above (103+) for user items
```

### SwiftData Model for Persistence

```swift
@Model
final class TravelPositionModel {
    @Attribute(.unique) var id: String  // "travel:detroit->milwaukee"
    var tripId: UUID
    var day: Int
    var sortOrder: Double
    var modifiedAt: Date

    // CloudKit sync fields
    var ckRecordID: String?
    var ckModifiedAt: Date?
}
```

### CloudKit Sync

Travel positions sync to CloudKit so all trip participants see updates:

- Record Type: `TravelPosition`
- Fields: `id`, `tripId`, `day`, `sortOrder`, `modifiedAt`
- Reference to trip's `CKRecord.ID` for sharing

## Travel Movement Constraints

Travel segments can be dragged to any position, but their **day** is constrained to the range between the departure and arrival cities' games.

### Example

```
Day 1: Game in Detroit
Day 2: Rest day
Day 3: Rest day
Day 4: Game in Milwaukee
```

Travel "Detroit → Milwaukee" valid day range: **Day 2, 3, or 4**
- Can't be Day 1 (game in Detroit hasn't happened yet)
- Can be Day 4 (arrive morning of Milwaukee game)

### Constraint Behavior

1. User drags travel to a new position
2. Calculate which day that position falls under
3. If day is within valid range → allow, update day + sortOrder
4. If day is outside valid range → snap to nearest valid day

## Implementation Changes

### 1. ItineraryTableViewWrapper.swift

Update `buildItineraryData()` to:
- Assign sortOrder to games (100.0, 101.0, etc. per day)
- Include travel in the sortable items list
- Sort all items (games + travel + custom) by sortOrder within each day

### 2. ItineraryTableViewController.swift

Update flattening in `reloadData()`:
```swift
for day in days {
    // 1. Day header (fixed)
    flatItems.append(.dayHeader(...))

    // 2. All orderable items sorted together
    let orderableItems = (day.games + day.travel + day.customItems)
        .sorted(by: \.sortOrder)
    for item in orderableItems {
        flatItems.append(item)
    }
}
```

Update `targetIndexPathForMoveFromRowAt`:
- Remove constraints that prevent custom items from going before games/travel
- Keep travel day-range constraints

Update `calculateSortOrder()`:
- Scan for ANY orderable item (not just custom items) when finding neighbors

### 3. TravelPositionService.swift (new)

```swift
actor TravelPositionService {
    func save(_ position: TravelPosition, for tripId: UUID) async throws
    func fetch(for tripId: UUID) async throws -> [TravelPosition]
    func sync() async throws  // Push local changes to CloudKit
}
```

## Edge Cases

| Scenario | Behavior |
|----------|----------|
| Trip replanned, games change | Reassign game sortOrders (100, 101...). Custom items keep their sortOrder |
| Game removed from trip | Items stay at their sortOrder positions |
| Game added to trip | Gets next available sortOrder (102, 103...) |
| Travel no longer valid (cities changed) | Remove orphaned TravelPosition records during replan |
| Conflict: two items same sortOrder | Stable sort by `modifiedAt` as tiebreaker |

## Migration

Existing trips have `travelDayOverrides: [String: Int]`. On first load:

```swift
for (travelId, day) in travelDayOverrides {
    let position = TravelPosition(
        day: day,
        sortOrder: 50.0  // Before games (which start at 100)
    )
    travelPositions[travelId] = position
}
```

Existing custom items already have `day` and `sortOrder` - no migration needed.

## Summary

This design enables fully flexible itinerary ordering where:

1. **Custom items** can be placed anywhere - before games, after games, between travel and games, any day
2. **Travel** can be reordered within a day and moved between days, constrained to valid day range
3. **Games** have sortOrder for positioning but cannot be dragged
4. **Day headers** remain fixed anchors at the top of each day

The key insight is that all orderable content (games, travel, custom items) shares a single sortOrder namespace within each day, allowing arbitrary interleaving while maintaining the structural constraint that games can't be moved by users.
