From 73ed3150ed9764ac1545168b0076ca6e32077b31 Mon Sep 17 00:00:00 2001 From: Trey t Date: Sun, 18 Jan 2026 14:54:28 -0600 Subject: [PATCH] docs(02-02): document ItineraryConstraints API for Phase 4 Document public API for drag-drop integration: - isValidPosition() for position validation - validDayRange() for precomputing valid days - barrierGames() for visual highlighting - Integration patterns for ItineraryTableViewController Co-Authored-By: Claude Opus 4.5 --- .../CONSTRAINT-API.md | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 .planning/phases/02-constraint-validation/CONSTRAINT-API.md diff --git a/.planning/phases/02-constraint-validation/CONSTRAINT-API.md b/.planning/phases/02-constraint-validation/CONSTRAINT-API.md new file mode 100644 index 0000000..7d1836a --- /dev/null +++ b/.planning/phases/02-constraint-validation/CONSTRAINT-API.md @@ -0,0 +1,164 @@ +# ItineraryConstraints API + +**Location:** `SportsTime/Core/Models/Domain/ItineraryConstraints.swift` +**Verified by:** 22 tests in `SportsTimeTests/Domain/ItineraryConstraintsTests.swift` + +## Overview + +`ItineraryConstraints` validates item positions during drag-drop operations. It enforces: + +- **Games cannot move** (CONS-01) +- **Travel segments have day range limits** (CONS-02) +- **Travel segments must respect game sortOrder on same day** (CONS-03) +- **Custom items have no constraints** (CONS-04) + +## Construction + +```swift +let constraints = ItineraryConstraints( + tripDayCount: days.count, + items: allItineraryItems // All items including games +) +``` + +**Parameters:** +- `tripDayCount`: Total days in trip (1-indexed, so a 5-day trip has days 1-5) +- `items`: All itinerary items (games, travel, custom). Games are used to calculate constraints for travel items. + +## Public API + +### `isValidPosition(for:day:sortOrder:) -> Bool` + +Check if a specific position is valid for an item. + +```swift +func isValidPosition(for item: ItineraryItem, day: Int, sortOrder: Double) -> Bool +``` + +**Usage during drag:** +```swift +// On each drag position update +let dropPosition = calculateDropPosition(at: touchLocation) +let isValid = constraints.isValidPosition( + for: draggedItem, + day: dropPosition.day, + sortOrder: dropPosition.sortOrder +) + +if isValid { + showValidDropIndicator() +} else { + showInvalidDropIndicator() +} +``` + +**Returns:** +- `true`: Position is valid, allow drop +- `false`: Position is invalid, reject drop (snap back) + +**Rules by item type:** +| Item Type | Day Constraint | SortOrder Constraint | +|-----------|----------------|----------------------| +| `.game` | Always `false` | Always `false` | +| `.travel` | Within valid day range | After departure games, before arrival games | +| `.custom` | Any day 1...tripDayCount | Any sortOrder | + +### `validDayRange(for:) -> ClosedRange?` + +Get the valid day range for a travel item (for visual feedback). + +```swift +func validDayRange(for item: ItineraryItem) -> ClosedRange? +``` + +**Usage at drag start:** +```swift +// When drag begins, precompute valid range +guard case .travel = draggedItem.kind, + let validRange = constraints.validDayRange(for: draggedItem) else { + // Not a travel item or impossible constraints + return +} + +// Use range to dim invalid days +for day in 1...tripDayCount { + if !validRange.contains(day) { + dimDay(day) + } +} +``` + +**Returns:** +- `ClosedRange`: Valid day range (e.g., `2...4`) +- `nil`: Constraints are impossible (e.g., departure game after arrival game) + +### `barrierGames(for:) -> [ItineraryItem]` + +Get games that constrain a travel item (for visual highlighting). + +```swift +func barrierGames(for item: ItineraryItem) -> [ItineraryItem] +``` + +**Usage for visual feedback:** +```swift +// Highlight barrier games during drag +let barriers = constraints.barrierGames(for: travelItem) +for barrier in barriers { + highlightAsBarrier(barrier) // e.g., gold border +} +``` + +**Returns:** +- Array of game items: Last departure city game + first arrival city game +- Empty array: Not a travel item or no constraining games + +## Integration Points + +### ItineraryTableViewController (existing) + +```swift +// In reloadData() +self.constraints = ItineraryConstraints(tripDayCount: tripDayCount, items: itineraryItems) + +// In drag handling +if constraints.isValidPosition(for: draggedItem, day: targetDay, sortOrder: targetSortOrder) { + // Allow drop +} else { + // Reject drop, snap back +} +``` + +### Phase 4 Implementation Notes + +1. **Drag Start:** + - Check `item.isReorderable` (games return `false`) + - Call `validDayRange(for:)` to precompute valid days + - Call `barrierGames(for:)` to identify visual barriers + +2. **Drag Move:** + - Calculate target (day, sortOrder) from touch position + - Call `isValidPosition(for:day:sortOrder:)` for real-time feedback + - Update insertion line (valid) or red indicator (invalid) + +3. **Drag End:** + - Final `isValidPosition(for:day:sortOrder:)` check + - Valid: Update item's day/sortOrder, animate settle + - Invalid: Animate snap back, haptic feedback + +## Test Coverage + +| Requirement | Tests | Verified | +|-------------|-------|----------| +| CONS-01 (games cannot move) | 2 | Yes | +| CONS-02 (travel day range) | 3 | Yes | +| CONS-03 (travel sortOrder) | 5 | Yes | +| CONS-04 (custom flexibility) | 2 | Yes | +| Edge cases | 8 | Yes | +| Success criteria | 3 | Yes | +| Barrier games | 1 | Yes | +| **Total** | **22** | **100%** | + +--- +*API documented: Phase 02* +*Ready for: Phase 04 (Drag Interaction)*