diff --git a/SportsTime/Features/Trip/ViewModels/TripWizardViewModel.swift b/SportsTime/Features/Trip/ViewModels/TripWizardViewModel.swift new file mode 100644 index 0000000..533b43a --- /dev/null +++ b/SportsTime/Features/Trip/ViewModels/TripWizardViewModel.swift @@ -0,0 +1,148 @@ +// +// TripWizardViewModel.swift +// SportsTime +// +// ViewModel for progressive-reveal trip wizard. +// + +import Foundation +import SwiftUI + +@Observable +final class TripWizardViewModel { + + // MARK: - Planning Mode + + var planningMode: PlanningMode? = nil { + didSet { + if oldValue != nil && oldValue != planningMode { + resetDownstreamFromPlanningMode() + } + } + } + + // MARK: - Sports Selection + + var selectedSports: Set = [] + + // MARK: - Dates + + var startDate: Date = Date() + var endDate: Date = Date().addingTimeInterval(86400 * 7) + var hasSetDates: Bool = false + + // MARK: - Regions + + var selectedRegions: Set = [] + + // MARK: - Route Preferences + + var routePreference: RoutePreference = .balanced + var hasSetRoutePreference: Bool = false + + // MARK: - Repeat Cities + + var allowRepeatCities: Bool = false + var hasSetRepeatCities: Bool = false + + // MARK: - Must Stops + + var mustStopLocations: [LocationInput] = [] + + // MARK: - Planning State + + var isPlanning: Bool = false + + // MARK: - Sport Availability + + var sportAvailability: [Sport: Bool] = [:] + var isLoadingSportAvailability: Bool = false + + // MARK: - Reveal State (computed) + + var isPlanningModeStepVisible: Bool { true } + + var isSportsStepVisible: Bool { + planningMode != nil + } + + var isDatesStepVisible: Bool { + isSportsStepVisible && !selectedSports.isEmpty + } + + var isRegionsStepVisible: Bool { + isDatesStepVisible && hasSetDates + } + + var isRoutePreferenceStepVisible: Bool { + isRegionsStepVisible && !selectedRegions.isEmpty + } + + var isRepeatCitiesStepVisible: Bool { + isRoutePreferenceStepVisible && hasSetRoutePreference + } + + var isMustStopsStepVisible: Bool { + isRepeatCitiesStepVisible && hasSetRepeatCities + } + + var isReviewStepVisible: Bool { + isMustStopsStepVisible + } + + /// Combined state for animation tracking + var revealState: Int { + var state = 0 + if isSportsStepVisible { state += 1 } + if isDatesStepVisible { state += 2 } + if isRegionsStepVisible { state += 4 } + if isRoutePreferenceStepVisible { state += 8 } + if isRepeatCitiesStepVisible { state += 16 } + if isMustStopsStepVisible { state += 32 } + if isReviewStepVisible { state += 64 } + return state + } + + // MARK: - Sport Availability + + func canSelectSport(_ sport: Sport) -> Bool { + sportAvailability[sport] ?? true // Default to available if not checked + } + + func fetchSportAvailability() async { + guard hasSetDates else { return } + + isLoadingSportAvailability = true + defer { isLoadingSportAvailability = false } + + var availability: [Sport: Bool] = [:] + + for sport in Sport.supported { + do { + let games = try await AppDataProvider.shared.filterGames( + sports: [sport], + startDate: startDate, + endDate: endDate + ) + availability[sport] = !games.isEmpty + } catch { + availability[sport] = true // Default to available on error + } + } + + await MainActor.run { + self.sportAvailability = availability + } + } + + // MARK: - Reset Logic + + private func resetDownstreamFromPlanningMode() { + selectedSports = [] + hasSetDates = false + selectedRegions = [] + hasSetRoutePreference = false + hasSetRepeatCities = false + mustStopLocations = [] + } +} diff --git a/SportsTimeTests/Trip/TripWizardViewModelTests.swift b/SportsTimeTests/Trip/TripWizardViewModelTests.swift new file mode 100644 index 0000000..ce0a186 --- /dev/null +++ b/SportsTimeTests/Trip/TripWizardViewModelTests.swift @@ -0,0 +1,48 @@ +// +// TripWizardViewModelTests.swift +// SportsTimeTests +// + +import XCTest +@testable import SportsTime + +final class TripWizardViewModelTests: XCTestCase { + + func test_initialState_onlyPlanningModeStepVisible() { + let viewModel = TripWizardViewModel() + + XCTAssertTrue(viewModel.isPlanningModeStepVisible) + XCTAssertFalse(viewModel.isSportsStepVisible) + XCTAssertFalse(viewModel.isDatesStepVisible) + XCTAssertFalse(viewModel.isRegionsStepVisible) + } + + func test_selectingPlanningMode_revealsSportsStep() { + let viewModel = TripWizardViewModel() + + viewModel.planningMode = .dateRange + + XCTAssertTrue(viewModel.isSportsStepVisible) + } + + func test_selectingSport_revealsDatesStep() { + let viewModel = TripWizardViewModel() + viewModel.planningMode = .dateRange + + viewModel.selectedSports = [.mlb] + + XCTAssertTrue(viewModel.isDatesStepVisible) + } + + func test_changingPlanningMode_resetsDownstreamSelections() { + let viewModel = TripWizardViewModel() + viewModel.planningMode = .dateRange + viewModel.selectedSports = [.mlb, .nba] + viewModel.hasSetDates = true + + viewModel.planningMode = .gameFirst + + XCTAssertTrue(viewModel.selectedSports.isEmpty) + XCTAssertFalse(viewModel.hasSetDates) + } +}