feat(wizard): add mode-specific trip wizard inputs

- Add GamePickerStep with sheet-based Sport → Team → Game selection
- Add TeamPickerStep with sheet-based Sport → Team selection
- Add LocationsStep for start/end location selection with round trip toggle
- Update TripWizardViewModel with mode-specific fields and validation
- Update TripWizardView with conditional step rendering per mode
- Update ReviewStep with mode-aware validation display
- Fix gameFirst mode to derive date range from selected games

Each planning mode now shows only relevant steps:
- By Dates: Dates → Sports → Regions → Route → Repeat → Must Stops
- By Games: Game Picker → Route → Repeat → Must Stops
- By Route: Locations → Dates → Sports → Route → Repeat → Must Stops
- Follow Team: Team Picker → Dates → Route → Repeat → Must Stops

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-14 22:21:57 -06:00
parent 1301442604
commit aa34c6585a
7 changed files with 1277 additions and 63 deletions

View File

@@ -22,6 +22,12 @@ struct ReviewStep: View {
let fieldValidation: FieldValidation
let onPlan: () -> Void
// Mode-specific display values (passed from parent)
var selectedGameCount: Int = 0
var selectedTeamName: String? = nil
var startLocationName: String? = nil
var endLocationName: String? = nil
var body: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.md) {
StepHeader(
@@ -30,33 +36,19 @@ struct ReviewStep: View {
)
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
// Mode (always shown)
ReviewRow(label: "Mode", value: planningMode.displayName)
ReviewRow(
label: "Sports",
value: selectedSports.isEmpty ? "Not selected" : selectedSports.map(\.rawValue).sorted().joined(separator: ", "),
isMissing: fieldValidation.sports == .missing
)
ReviewRow(
label: "Dates",
value: dateRangeText,
isMissing: fieldValidation.dates == .missing
)
ReviewRow(
label: "Regions",
value: selectedRegions.isEmpty ? "Not selected" : selectedRegions.map(\.shortName).sorted().joined(separator: ", "),
isMissing: fieldValidation.regions == .missing
)
ReviewRow(
label: "Route",
value: routePreference.displayName,
isMissing: fieldValidation.routePreference == .missing
)
ReviewRow(
label: "Repeat cities",
value: allowRepeatCities ? "Yes" : "No",
isMissing: fieldValidation.repeatCities == .missing
)
// Mode-specific required fields
ForEach(fieldValidation.requiredFields, id: \.name) { field in
ReviewRow(
label: field.name,
value: displayValue(for: field.name),
isMissing: field.status == .missing
)
}
// Optional: Must-stops (shown if any selected)
if !mustStopLocations.isEmpty {
ReviewRow(label: "Must-stops", value: mustStopLocations.map(\.name).joined(separator: ", "))
}
@@ -65,6 +57,17 @@ struct ReviewStep: View {
.background(Theme.cardBackgroundElevated(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium))
// Missing fields warning
if !fieldValidation.missingFields.isEmpty {
HStack(spacing: Theme.Spacing.xs) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.orange)
Text("Complete all required fields to continue")
.font(.caption)
.foregroundStyle(Theme.textMuted(colorScheme))
}
}
Button(action: onPlan) {
HStack(spacing: Theme.Spacing.sm) {
if isPlanning {
@@ -91,6 +94,31 @@ struct ReviewStep: View {
}
}
private func displayValue(for fieldName: String) -> String {
switch fieldName {
case "Sports":
return selectedSports.isEmpty ? "Not selected" : selectedSports.map(\.rawValue).sorted().joined(separator: ", ")
case "Dates":
return dateRangeText
case "Regions":
return selectedRegions.isEmpty ? "Not selected" : selectedRegions.map(\.shortName).sorted().joined(separator: ", ")
case "Route Preference":
return routePreference.displayName
case "Repeat Cities":
return allowRepeatCities ? "Yes" : "No"
case "Games":
return selectedGameCount > 0 ? "\(selectedGameCount) game\(selectedGameCount == 1 ? "" : "s") selected" : "Not selected"
case "Team":
return selectedTeamName ?? "Not selected"
case "Start Location":
return startLocationName ?? "Not selected"
case "End Location":
return endLocationName ?? "Not selected"
default:
return ""
}
}
private var dateRangeText: String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
@@ -143,11 +171,16 @@ private struct ReviewRow: View {
isPlanning: false,
canPlanTrip: true,
fieldValidation: FieldValidation(
planningMode: .dateRange,
sports: .valid,
dates: .valid,
regions: .valid,
routePreference: .valid,
repeatCities: .valid
repeatCities: .valid,
selectedGames: .valid,
selectedTeam: .valid,
startLocation: .valid,
endLocation: .valid
),
onPlan: {}
)
@@ -168,11 +201,16 @@ private struct ReviewRow: View {
isPlanning: false,
canPlanTrip: false,
fieldValidation: FieldValidation(
planningMode: .dateRange,
sports: .missing,
dates: .valid,
regions: .missing,
routePreference: .valid,
repeatCities: .missing
repeatCities: .missing,
selectedGames: .valid,
selectedTeam: .valid,
startLocation: .valid,
endLocation: .valid
),
onPlan: {}
)