Files
SportstimeAPI/.planning/phases/02-constraint-validation/CONSTRAINT-API.md
Trey t 73ed3150ed 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 <noreply@anthropic.com>
2026-01-18 14:54:28 -06:00

165 lines
4.5 KiB
Markdown

# 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<Int>?`
Get the valid day range for a travel item (for visual feedback).
```swift
func validDayRange(for item: ItineraryItem) -> ClosedRange<Int>?
```
**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<Int>`: 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)*