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>
This commit is contained in:
164
.planning/phases/02-constraint-validation/CONSTRAINT-API.md
Normal file
164
.planning/phases/02-constraint-validation/CONSTRAINT-API.md
Normal file
@@ -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<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)*
|
||||
Reference in New Issue
Block a user