// // StadiumVisitSheet.swift // SportsTime // // Sheet for manually logging a stadium visit. // import SwiftUI import SwiftData struct StadiumVisitSheet: View { @Environment(\.modelContext) private var modelContext @Environment(\.colorScheme) private var colorScheme @Environment(\.dismiss) private var dismiss // Optional pre-selected values var initialStadium: Stadium? var initialSport: Sport? var onSave: ((StadiumVisit) -> Void)? // Form state @State private var selectedSport: Sport @State private var selectedStadium: Stadium? @State private var visitDate: Date = Date() @State private var visitType: VisitType = .game @State private var homeTeamName: String = "" @State private var awayTeamName: String = "" @State private var homeScore: String = "" @State private var awayScore: String = "" @State private var seatLocation: String = "" @State private var notes: String = "" // UI state @State private var showStadiumPicker = false @State private var isSaving = false @State private var isLookingUpGame = false @State private var scoreFromScraper = false // Track if score was auto-filled @State private var errorMessage: String? @State private var showAwayTeamSuggestions = false @State private var showHomeTeamSuggestions = false @FocusState private var awayTeamFocused: Bool @FocusState private var homeTeamFocused: Bool // Data @ObservedObject private var dataProvider = AppDataProvider.shared // Teams for autocomplete private var teamsForSport: [Team] { dataProvider.teams.filter { $0.sport == selectedSport } .sorted { $0.name < $1.name } } private var awayTeamSuggestions: [Team] { guard !awayTeamName.isEmpty else { return teamsForSport } return teamsForSport.filter { $0.name.localizedCaseInsensitiveContains(awayTeamName) || $0.city.localizedCaseInsensitiveContains(awayTeamName) } } private var homeTeamSuggestions: [Team] { guard !homeTeamName.isEmpty else { return teamsForSport } return teamsForSport.filter { $0.name.localizedCaseInsensitiveContains(homeTeamName) || $0.city.localizedCaseInsensitiveContains(homeTeamName) } } init( initialStadium: Stadium? = nil, initialSport: Sport? = nil, onSave: ((StadiumVisit) -> Void)? = nil ) { self.initialStadium = initialStadium self.initialSport = initialSport self.onSave = onSave _selectedSport = State(initialValue: initialSport ?? .mlb) _selectedStadium = State(initialValue: initialStadium) } var body: some View { NavigationStack { Form { // Sport & Stadium Section Section { // Sport Picker Picker("Sport", selection: $selectedSport) { ForEach(Sport.supported) { sport in HStack { Image(systemName: sport.iconName) Text(sport.displayName) } .tag(sport) } } // Stadium Selection Button { showStadiumPicker = true } label: { HStack { Text("Stadium") .foregroundStyle(Theme.textPrimary(colorScheme)) Spacer() if let stadium = selectedStadium { Text(stadium.name) .foregroundStyle(Theme.textSecondary(colorScheme)) } else { Text("Select Stadium") .foregroundStyle(Theme.textMuted(colorScheme)) } Image(systemName: "chevron.right") .font(.caption) .foregroundStyle(Theme.textMuted(colorScheme)) } } } header: { Text("Location") } .listRowBackground(Theme.cardBackground(colorScheme)) // Visit Details Section Section { DatePicker("Date", selection: $visitDate, displayedComponents: .date) Picker("Visit Type", selection: $visitType) { ForEach(VisitType.allCases, id: \.self) { type in Text(type.displayName).tag(type) } } } header: { Text("Visit Details") } .listRowBackground(Theme.cardBackground(colorScheme)) // Game Info Section (only for game visits) if visitType == .game { Section { // Away Team with autocomplete VStack(alignment: .leading, spacing: 0) { HStack { Text("Away Team") Spacer() TextField("Team Name", text: $awayTeamName) .multilineTextAlignment(.trailing) .focused($awayTeamFocused) .onChange(of: awayTeamFocused) { _, focused in showAwayTeamSuggestions = focused } } if showAwayTeamSuggestions && !awayTeamSuggestions.isEmpty { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(awayTeamSuggestions.prefix(10)) { team in Button { awayTeamName = team.name awayTeamFocused = false } label: { Text(team.name) .font(.subheadline) .foregroundStyle(Theme.textPrimary(colorScheme)) .padding(.horizontal, 10) .padding(.vertical, 6) .background(Theme.cardBackgroundElevated(colorScheme)) .clipShape(Capsule()) } } } .padding(.top, 8) } } } // Home Team with autocomplete VStack(alignment: .leading, spacing: 0) { HStack { Text("Home Team") Spacer() TextField("Team Name", text: $homeTeamName) .multilineTextAlignment(.trailing) .focused($homeTeamFocused) .onChange(of: homeTeamFocused) { _, focused in showHomeTeamSuggestions = focused } } if showHomeTeamSuggestions && !homeTeamSuggestions.isEmpty { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(homeTeamSuggestions.prefix(10)) { team in Button { homeTeamName = team.name homeTeamFocused = false } label: { Text(team.name) .font(.subheadline) .foregroundStyle(Theme.textPrimary(colorScheme)) .padding(.horizontal, 10) .padding(.vertical, 6) .background(Theme.cardBackgroundElevated(colorScheme)) .clipShape(Capsule()) } } } .padding(.top, 8) } } } HStack { Text("Final Score") Spacer() TextField("Away", text: $awayScore) .keyboardType(.numberPad) .frame(width: 50) .multilineTextAlignment(.center) Text("-") .foregroundStyle(Theme.textMuted(colorScheme)) TextField("Home", text: $homeScore) .keyboardType(.numberPad) .frame(width: 50) .multilineTextAlignment(.center) } // Look Up Game Button if selectedStadium != nil { Button { Task { await lookUpGame() } } label: { HStack { if isLookingUpGame { LoadingSpinner(size: .small) } else { Image(systemName: "magnifyingglass") } Text("Look Up Game") } .frame(maxWidth: .infinity) .foregroundStyle(Theme.warmOrange) } .disabled(isLookingUpGame) } } header: { Text("Game Info") } footer: { if selectedStadium != nil { Text("Tap 'Look Up Game' to auto-fill teams and score from historical data") } else { Text("Select a stadium to enable game lookup") } } .listRowBackground(Theme.cardBackground(colorScheme)) } // Optional Details Section Section { HStack { Text("Seat Location") Spacer() TextField("e.g., Section 120", text: $seatLocation) .multilineTextAlignment(.trailing) } VStack(alignment: .leading, spacing: Theme.Spacing.xs) { Text("Notes") TextEditor(text: $notes) .frame(minHeight: 80) .scrollContentBackground(.hidden) .background(Theme.cardBackgroundElevated(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.small)) } } header: { Text("Additional Info") } .listRowBackground(Theme.cardBackground(colorScheme)) // Error Message if let error = errorMessage { Section { HStack { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(.orange) Text(error) .foregroundStyle(.red) } } .listRowBackground(Theme.cardBackground(colorScheme)) } } .scrollDismissesKeyboard(.interactively) .scrollContentBackground(.hidden) .themedBackground() .navigationTitle("Log Visit") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("Save") { saveVisit() } .disabled(!canSave || isSaving) .fontWeight(.semibold) } } .sheet(isPresented: $showStadiumPicker) { StadiumPickerSheet( sport: selectedSport, selectedStadium: $selectedStadium ) } .onChange(of: selectedSport) { _, _ in // Clear selections when sport changes if let stadium = selectedStadium { // Check if stadium belongs to new sport let sportTeams = dataProvider.teams.filter { $0.sport == selectedSport } if !sportTeams.contains(where: { $0.stadiumId == stadium.id }) { selectedStadium = nil } } // Clear team names when sport changes homeTeamName = "" awayTeamName = "" } } } // MARK: - Computed Properties private var canSave: Bool { selectedStadium != nil } private var finalScoreString: String? { guard let away = Int(awayScore), let home = Int(homeScore) else { return nil } return "\(away)-\(home)" } // MARK: - Actions private func lookUpGame() async { guard let stadium = selectedStadium else { return } isLookingUpGame = true errorMessage = nil // Use the historical game scraper if let scrapedGame = await HistoricalGameScraper.shared.scrapeGame( stadium: stadium, date: visitDate ) { // Fill in the form with scraped data awayTeamName = scrapedGame.awayTeam homeTeamName = scrapedGame.homeTeam if let away = scrapedGame.awayScore { awayScore = String(away) scoreFromScraper = true } if let home = scrapedGame.homeScore { homeScore = String(home) scoreFromScraper = true } } else { errorMessage = "No game found for \(stadium.name) on this date" } isLookingUpGame = false } private func saveVisit() { guard let stadium = selectedStadium else { errorMessage = "Please select a stadium" return } isSaving = true errorMessage = nil // Create the visit let visit = StadiumVisit( stadiumId: stadium.id, stadiumNameAtVisit: stadium.name, visitDate: visitDate, sport: selectedSport, visitType: visitType, homeTeamName: homeTeamName.isEmpty ? nil : homeTeamName, awayTeamName: awayTeamName.isEmpty ? nil : awayTeamName, finalScore: finalScoreString, scoreSource: finalScoreString != nil ? (scoreFromScraper ? .scraped : .user) : nil, dataSource: scoreFromScraper ? .automatic : .fullyManual, seatLocation: seatLocation.isEmpty ? nil : seatLocation, notes: notes.isEmpty ? nil : notes, source: .manual ) // Save to SwiftData modelContext.insert(visit) do { try modelContext.save() onSave?(visit) dismiss() } catch { errorMessage = "Failed to save visit: \(error.localizedDescription)" isSaving = false } } } // MARK: - Stadium Picker Sheet struct StadiumPickerSheet: View { let sport: Sport @Binding var selectedStadium: Stadium? @Environment(\.colorScheme) private var colorScheme @Environment(\.dismiss) private var dismiss @State private var searchText = "" @ObservedObject private var dataProvider = AppDataProvider.shared private var stadiums: [Stadium] { let sportTeams = dataProvider.teams.filter { $0.sport == sport } let stadiumIds = Set(sportTeams.map { $0.stadiumId }) return dataProvider.stadiums.filter { stadiumIds.contains($0.id) } } private var filteredStadiums: [Stadium] { if searchText.isEmpty { return stadiums.sorted { $0.name < $1.name } } return stadiums.filter { $0.name.localizedCaseInsensitiveContains(searchText) || $0.city.localizedCaseInsensitiveContains(searchText) }.sorted { $0.name < $1.name } } var body: some View { NavigationStack { Group { if stadiums.isEmpty { ContentUnavailableView( "No Stadiums", systemImage: "building.2", description: Text("No stadiums found for \(sport.displayName)") ) } else if filteredStadiums.isEmpty { ContentUnavailableView.search(text: searchText) } else { List(filteredStadiums) { stadium in Button { selectedStadium = stadium dismiss() } label: { HStack { VStack(alignment: .leading, spacing: 4) { Text(stadium.name) .font(.body) .foregroundStyle(Theme.textPrimary(colorScheme)) Text(stadium.fullAddress) .font(.subheadline) .foregroundStyle(Theme.textSecondary(colorScheme)) } Spacer() if selectedStadium?.id == stadium.id { Image(systemName: "checkmark") .foregroundStyle(Theme.warmOrange) } } } .listRowBackground(Theme.cardBackground(colorScheme)) } .scrollDismissesKeyboard(.interactively) .scrollContentBackground(.hidden) } } .themedBackground() .searchable(text: $searchText, prompt: "Search stadiums") .navigationTitle("Select Stadium") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } } } } // MARK: - Preview #Preview { StadiumVisitSheet() .modelContainer(for: StadiumVisit.self, inMemory: true) }