// // VisitDetailView.swift // SportsTime // // View for displaying and editing a stadium visit's details. // import SwiftUI import SwiftData struct VisitDetailView: View { @Bindable var visit: StadiumVisit let stadium: Stadium @Environment(\.modelContext) private var modelContext @Environment(\.colorScheme) private var colorScheme @Environment(\.dismiss) private var dismiss @State private var isEditing = false @State private var showDeleteConfirmation = false // Edit state @State private var editVisitDate: Date @State private var editVisitType: VisitType @State private var editHomeTeamName: String @State private var editAwayTeamName: String @State private var editHomeScore: String @State private var editAwayScore: String @State private var editSeatLocation: String @State private var editNotes: String init(visit: StadiumVisit, stadium: Stadium) { self.visit = visit self.stadium = stadium // Initialize edit state from visit _editVisitDate = State(initialValue: visit.visitDate) _editVisitType = State(initialValue: visit.visitType) _editHomeTeamName = State(initialValue: visit.homeTeamName ?? "") _editAwayTeamName = State(initialValue: visit.awayTeamName ?? "") // Parse score if available if let score = visit.finalScore { let parts = score.split(separator: "-") if parts.count == 2 { _editAwayScore = State(initialValue: String(parts[0])) _editHomeScore = State(initialValue: String(parts[1])) } else { _editAwayScore = State(initialValue: "") _editHomeScore = State(initialValue: "") } } else { _editAwayScore = State(initialValue: "") _editHomeScore = State(initialValue: "") } _editSeatLocation = State(initialValue: visit.seatLocation ?? "") _editNotes = State(initialValue: visit.notes ?? "") } var body: some View { ScrollView { VStack(spacing: Theme.Spacing.lg) { // Header visitHeader .staggeredAnimation(index: 0) // Game info (if applicable) if visit.visitType == .game { gameInfoCard .staggeredAnimation(index: 1) } // Visit details detailsCard .staggeredAnimation(index: 2) // Notes if !isEditing && (visit.notes?.isEmpty == false) { notesCard .staggeredAnimation(index: 3) } // Edit form (when editing) if isEditing { editForm .staggeredAnimation(index: 4) } // Delete button if !isEditing { deleteButton .staggeredAnimation(index: 5) } } .padding(Theme.Spacing.md) } .themedBackground() .navigationTitle(isEditing ? "Edit Visit" : "Visit Details") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .primaryAction) { if isEditing { Button("Save") { saveChanges() } .fontWeight(.semibold) } else { Button("Edit") { withAnimation { isEditing = true } } } } if isEditing { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { cancelEditing() } } } } .confirmationDialog( "Delete Visit", isPresented: $showDeleteConfirmation, titleVisibility: .visible ) { Button("Delete Visit", role: .destructive) { deleteVisit() } Button("Cancel", role: .cancel) {} } message: { Text("Are you sure you want to delete this visit? This action cannot be undone.") } } // MARK: - Header private var visitHeader: some View { VStack(spacing: Theme.Spacing.md) { // Sport icon ZStack { Circle() .fill(sportColor.opacity(0.15)) .frame(width: 80, height: 80) Image(systemName: visit.sportEnum?.iconName ?? "sportscourt") .font(.largeTitle) .foregroundStyle(sportColor) } VStack(spacing: Theme.Spacing.xs) { Text(stadium.name) .font(.headline) .foregroundStyle(Theme.textPrimary(colorScheme)) .multilineTextAlignment(.center) Text(stadium.fullAddress) .font(.body) .foregroundStyle(Theme.textSecondary(colorScheme)) // Visit type badge Text(visit.visitType.displayName) .font(.subheadline) .foregroundStyle(.white) .padding(.horizontal, Theme.Spacing.sm) .padding(.vertical, 4) .background(sportColor) .clipShape(Capsule()) } } .frame(maxWidth: .infinity) .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) } } // MARK: - Game Info Card private var gameInfoCard: some View { VStack(alignment: .leading, spacing: Theme.Spacing.md) { HStack { Image(systemName: "sportscourt.fill") .foregroundStyle(sportColor) Text("Game Info") .font(.body) .foregroundStyle(Theme.textPrimary(colorScheme)) } if let matchup = visit.matchupDescription { VStack(alignment: .leading, spacing: 4) { Text("Matchup") .foregroundStyle(Theme.textSecondary(colorScheme)) Text(matchup) .foregroundStyle(Theme.textPrimary(colorScheme)) .fontWeight(.medium) } .frame(maxWidth: .infinity, alignment: .leading) } if let score = visit.finalScore { VStack(alignment: .leading, spacing: 4) { Text("Final Score") .foregroundStyle(Theme.textSecondary(colorScheme)) Text(score) .foregroundStyle(Theme.textPrimary(colorScheme)) .fontWeight(.bold) } .frame(maxWidth: .infinity, alignment: .leading) } } .font(.body) .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) } } // MARK: - Details Card private var detailsCard: some View { VStack(alignment: .leading, spacing: Theme.Spacing.md) { HStack { Image(systemName: "info.circle.fill") .foregroundStyle(Theme.warmOrange) Text("Details") .font(.body) .foregroundStyle(Theme.textPrimary(colorScheme)) } // Date VStack(alignment: .leading, spacing: 4) { Text("Date") .foregroundStyle(Theme.textSecondary(colorScheme)) Text(formattedDate) .foregroundStyle(Theme.textPrimary(colorScheme)) } .frame(maxWidth: .infinity, alignment: .leading) // Seat location if let seat = visit.seatLocation, !seat.isEmpty { VStack(alignment: .leading, spacing: 4) { Text("Seat") .foregroundStyle(Theme.textSecondary(colorScheme)) Text(seat) .foregroundStyle(Theme.textPrimary(colorScheme)) } .frame(maxWidth: .infinity, alignment: .leading) } // Source VStack(alignment: .leading, spacing: 4) { Text("Source") .foregroundStyle(Theme.textSecondary(colorScheme)) Text(visit.source.displayName) .foregroundStyle(Theme.textMuted(colorScheme)) } .frame(maxWidth: .infinity, alignment: .leading) // Created date VStack(alignment: .leading, spacing: 4) { Text("Logged") .foregroundStyle(Theme.textSecondary(colorScheme)) Text(formattedCreatedDate) .foregroundStyle(Theme.textMuted(colorScheme)) } .frame(maxWidth: .infinity, alignment: .leading) } .font(.body) .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) } } // MARK: - Notes Card private var notesCard: some View { VStack(alignment: .leading, spacing: Theme.Spacing.sm) { HStack { Image(systemName: "note.text") .foregroundStyle(Theme.routeGold) Text("Notes") .font(.body) .foregroundStyle(Theme.textPrimary(colorScheme)) } Text(visit.notes ?? "") .font(.body) .foregroundStyle(Theme.textSecondary(colorScheme)) } .frame(maxWidth: .infinity, alignment: .leading) .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) } } // MARK: - Edit Form private var editForm: some View { VStack(alignment: .leading, spacing: Theme.Spacing.lg) { // Date VStack(alignment: .leading, spacing: Theme.Spacing.xs) { Text("Date") .font(.subheadline) .foregroundStyle(Theme.textSecondary(colorScheme)) DatePicker("", selection: $editVisitDate, displayedComponents: .date) .labelsHidden() } // Visit Type VStack(alignment: .leading, spacing: Theme.Spacing.xs) { Text("Visit Type") .font(.subheadline) .foregroundStyle(Theme.textSecondary(colorScheme)) Picker("", selection: $editVisitType) { ForEach(VisitType.allCases, id: \.self) { type in Text(type.displayName).tag(type) } } .pickerStyle(.segmented) } // Game info (if game type) if editVisitType == .game { VStack(alignment: .leading, spacing: Theme.Spacing.sm) { Text("Game Info") .font(.subheadline) .foregroundStyle(Theme.textSecondary(colorScheme)) HStack { TextField("Away Team", text: $editAwayTeamName) .textFieldStyle(.roundedBorder) Text("@") .foregroundStyle(Theme.textMuted(colorScheme)) TextField("Home Team", text: $editHomeTeamName) .textFieldStyle(.roundedBorder) } HStack { TextField("Away Score", text: $editAwayScore) .textFieldStyle(.roundedBorder) .keyboardType(.numberPad) .frame(width: 80) Text("-") .foregroundStyle(Theme.textMuted(colorScheme)) TextField("Home Score", text: $editHomeScore) .textFieldStyle(.roundedBorder) .keyboardType(.numberPad) .frame(width: 80) Spacer() } } } // Seat location VStack(alignment: .leading, spacing: Theme.Spacing.xs) { Text("Seat Location") .font(.subheadline) .foregroundStyle(Theme.textSecondary(colorScheme)) TextField("e.g., Section 120, Row 5", text: $editSeatLocation) .textFieldStyle(.roundedBorder) } // Notes VStack(alignment: .leading, spacing: Theme.Spacing.xs) { Text("Notes") .font(.subheadline) .foregroundStyle(Theme.textSecondary(colorScheme)) TextEditor(text: $editNotes) .frame(minHeight: 100) .scrollContentBackground(.hidden) .background(Theme.cardBackgroundElevated(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.small)) .overlay { RoundedRectangle(cornerRadius: Theme.CornerRadius.small) .stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1) } } } .padding(Theme.Spacing.lg) .background(Theme.cardBackground(colorScheme)) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large)) .overlay { RoundedRectangle(cornerRadius: Theme.CornerRadius.large) .stroke(Theme.warmOrange.opacity(0.5), lineWidth: 2) } } // MARK: - Delete Button private var deleteButton: some View { Button(role: .destructive) { showDeleteConfirmation = true } label: { HStack { Image(systemName: "trash") Text("Delete Visit") } .frame(maxWidth: .infinity) .padding(Theme.Spacing.md) .background(Color.red.opacity(0.1)) .foregroundStyle(.red) .clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)) } } // MARK: - Computed Properties private var sportColor: Color { visit.sportEnum?.themeColor ?? Theme.warmOrange } private var formattedDate: String { let formatter = DateFormatter() formatter.dateStyle = .long return formatter.string(from: visit.visitDate) } private var formattedCreatedDate: String { let formatter = DateFormatter() formatter.dateStyle = .medium formatter.timeStyle = .short return formatter.string(from: visit.createdAt) } // MARK: - Actions private func saveChanges() { visit.visitDate = editVisitDate visit.visitType = editVisitType visit.homeTeamName = editHomeTeamName.isEmpty ? nil : editHomeTeamName visit.awayTeamName = editAwayTeamName.isEmpty ? nil : editAwayTeamName visit.seatLocation = editSeatLocation.isEmpty ? nil : editSeatLocation visit.notes = editNotes.isEmpty ? nil : editNotes // Update score if let away = Int(editAwayScore), let home = Int(editHomeScore) { visit.finalScore = "\(away)-\(home)" visit.scoreSource = .user } else { visit.finalScore = nil visit.scoreSource = nil } // Mark as user corrected if it wasn't fully manual if visit.dataSource != .fullyManual { visit.dataSource = .userCorrected } try? modelContext.save() withAnimation { isEditing = false } } private func cancelEditing() { // Reset to original values editVisitDate = visit.visitDate editVisitType = visit.visitType editHomeTeamName = visit.homeTeamName ?? "" editAwayTeamName = visit.awayTeamName ?? "" editSeatLocation = visit.seatLocation ?? "" editNotes = visit.notes ?? "" if let score = visit.finalScore { let parts = score.split(separator: "-") if parts.count == 2 { editAwayScore = String(parts[0]) editHomeScore = String(parts[1]) } } else { editAwayScore = "" editHomeScore = "" } withAnimation { isEditing = false } } private func deleteVisit() { modelContext.delete(visit) try? modelContext.save() dismiss() } } // MARK: - Visit Source Display Name extension VisitSource { var displayName: String { switch self { case .trip: return "From Trip" case .manual: return "Manual Entry" case .photoImport: return "Photo Import" } } } // MARK: - Preview #Preview { let _ = Stadium( id: "stadium_preview_oracle_park", name: "Oracle Park", city: "San Francisco", state: "CA", latitude: 37.7786, longitude: -122.3893, capacity: 41915, sport: .mlb ) NavigationStack { Text("Preview placeholder") } }