// // ReviewStep.swift // SportsTime // // Step 8 of the trip wizard - review and submit. // import SwiftUI struct ReviewStep: View { @Environment(\.colorScheme) private var colorScheme let planningMode: PlanningMode let selectedSports: Set let startDate: Date let endDate: Date let selectedRegions: Set let routePreference: RoutePreference let allowRepeatCities: Bool let mustStopLocations: [LocationInput] let isPlanning: Bool let canPlanTrip: Bool 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 teamFirstTeamCount: Int = 0 var body: some View { VStack(alignment: .leading, spacing: Theme.Spacing.md) { StepHeader( title: "Ready to plan your trip!", subtitle: "Review your selections" ) VStack(alignment: .leading, spacing: Theme.Spacing.sm) { // Mode (always shown) ReviewRow(label: "Mode", value: planningMode.displayName) // 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: ", ")) } } .padding(Theme.Spacing.sm) .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 { LoadingSpinner(size: .small) .colorScheme(.dark) // Force white on orange button } Text(isPlanning ? "Planning..." : "Plan My Trip") .fontWeight(.semibold) } .frame(maxWidth: .infinity) .padding(Theme.Spacing.md) .background(canPlanTrip ? Theme.warmOrange : Theme.textMuted(colorScheme)) .foregroundStyle(.white) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) } .accessibilityIdentifier("wizard.planTripButton") .disabled(!canPlanTrip || isPlanning) } .padding(Theme.Spacing.lg) .background(Theme.cardBackground(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) .overlay { RoundedRectangle(cornerRadius: Theme.CornerRadius.large) .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) } } 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" case "Teams": return teamFirstTeamCount >= 2 ? "\(teamFirstTeamCount) teams selected" : "Select at least 2 teams" default: return "—" } } private var dateRangeText: String { let formatter = DateFormatter() formatter.dateStyle = .medium return "\(formatter.string(from: startDate)) - \(formatter.string(from: endDate))" } } // MARK: - Review Row private struct ReviewRow: View { @Environment(\.colorScheme) private var colorScheme let label: String let value: String var isMissing: Bool = false var body: some View { HStack(alignment: .top) { Text(label) .font(.caption) .foregroundStyle(isMissing ? .red : Theme.textMuted(colorScheme)) .frame(width: 80, alignment: .leading) Text(value) .font(.subheadline) .foregroundStyle(isMissing ? .red : Theme.textPrimary(colorScheme)) Spacer() if isMissing { Image(systemName: "exclamationmark.circle.fill") .font(.caption) .foregroundStyle(.red) } } } } // MARK: - Preview #Preview("All Valid") { ReviewStep( planningMode: .dateRange, selectedSports: [.mlb, .nba], startDate: Date(), endDate: Date().addingTimeInterval(86400 * 7), selectedRegions: [.east, .central], routePreference: .balanced, allowRepeatCities: false, mustStopLocations: [], isPlanning: false, canPlanTrip: true, fieldValidation: FieldValidation( planningMode: .dateRange, sports: .valid, dates: .valid, regions: .valid, routePreference: .valid, repeatCities: .valid, selectedGames: .valid, selectedTeam: .valid, startLocation: .valid, endLocation: .valid, teamFirstTeams: .valid, teamFirstTeamCount: 0 ), onPlan: {} ) .padding() .themedBackground() } #Preview("Missing Fields") { ReviewStep( planningMode: .dateRange, selectedSports: [], startDate: Date(), endDate: Date().addingTimeInterval(86400 * 7), selectedRegions: [], routePreference: .balanced, allowRepeatCities: false, mustStopLocations: [], isPlanning: false, canPlanTrip: false, fieldValidation: FieldValidation( planningMode: .dateRange, sports: .missing, dates: .valid, regions: .missing, routePreference: .valid, repeatCities: .missing, selectedGames: .valid, selectedTeam: .valid, startLocation: .valid, endLocation: .valid, teamFirstTeams: .valid, teamFirstTeamCount: 0 ), onPlan: {} ) .padding() .themedBackground() }