import SwiftUI struct AddAttemptView: View { let session: ClimbSession let gym: Gym @EnvironmentObject var dataManager: ClimbingDataManager @Environment(\.dismiss) private var dismiss @State private var selectedProblem: Problem? @State private var selectedResult: AttemptResult = .fall @State private var highestHold = "" @State private var notes = "" @State private var duration: Int = 0 @State private var restTime: Int = 0 @State private var showingCreateProblem = false // New problem creation state @State private var newProblemName = "" @State private var newProblemGrade = "" @State private var selectedClimbType: ClimbType = .boulder @State private var selectedDifficultySystem: DifficultySystem = .vScale private var activeProblems: [Problem] { dataManager.activeProblems(forGym: gym.id) } private var availableClimbTypes: [ClimbType] { gym.supportedClimbTypes } private var availableDifficultySystems: [DifficultySystem] { DifficultySystem.systemsForClimbType(selectedClimbType).filter { system in gym.difficultySystems.contains(system) } } private var availableGrades: [String] { selectedDifficultySystem.availableGrades } var body: some View { NavigationView { Form { if !showingCreateProblem { ProblemSelectionSection() } else { CreateProblemSection() } AttemptDetailsSection() } .navigationTitle("Add Attempt") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { Button("Add") { saveAttempt() } .disabled(!canSave) } } } .onAppear { setupInitialValues() } .onChange(of: selectedClimbType) { updateDifficultySystem() } .onChange(of: selectedDifficultySystem) { resetGradeIfNeeded() } } @ViewBuilder private func ProblemSelectionSection() -> some View { Section("Select Problem") { if activeProblems.isEmpty { VStack(alignment: .leading, spacing: 12) { Text("No active problems in this gym") .foregroundColor(.secondary) Button("Create New Problem") { showingCreateProblem = true } .buttonStyle(.borderedProminent) } .padding(.vertical, 8) } else { LazyVGrid( columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2), spacing: 8 ) { ForEach(activeProblems, id: \.id) { problem in ProblemSelectionCard( problem: problem, isSelected: selectedProblem?.id == problem.id ) { selectedProblem = problem } } } .padding(.vertical, 8) Button("Create New Problem") { showingCreateProblem = true } .foregroundColor(.blue) } } } @ViewBuilder private func CreateProblemSection() -> some View { Section { HStack { Text("Create New Problem") .font(.headline) Spacer() Button("Back") { showingCreateProblem = false } .foregroundColor(.blue) } } Section("Problem Details") { TextField("Problem Name", text: $newProblemName) } Section("Climb Type") { ForEach(availableClimbTypes, id: \.self) { climbType in HStack { Text(climbType.displayName) Spacer() if selectedClimbType == climbType { Image(systemName: "checkmark.circle.fill") .foregroundColor(.blue) } else { Image(systemName: "circle") .foregroundColor(.gray) } } .contentShape(Rectangle()) .onTapGesture { selectedClimbType = climbType } } } Section("Difficulty") { VStack(alignment: .leading, spacing: 12) { Text("Difficulty System") .font(.subheadline) .fontWeight(.medium) ForEach(availableDifficultySystems, id: \.self) { system in HStack { Text(system.displayName) Spacer() if selectedDifficultySystem == system { Image(systemName: "checkmark.circle.fill") .foregroundColor(.blue) } else { Image(systemName: "circle") .foregroundColor(.gray) } } .contentShape(Rectangle()) .onTapGesture { selectedDifficultySystem = system } } } if selectedDifficultySystem == .custom { TextField("Grade (Required - numbers only)", text: $newProblemGrade) .keyboardType(.numberPad) .onChange(of: newProblemGrade) { // Filter out non-numeric characters newProblemGrade = newProblemGrade.filter { $0.isNumber } } } else { VStack(alignment: .leading, spacing: 8) { Text("Grade (Required)") .font(.subheadline) .fontWeight(.medium) ScrollView(.horizontal, showsIndicators: false) { LazyHStack(spacing: 8) { ForEach(availableGrades, id: \.self) { grade in Button(grade) { newProblemGrade = grade } .buttonStyle(.bordered) .controlSize(.small) .tint(newProblemGrade == grade ? .blue : .gray) } } .padding(.horizontal, 1) } } } } } @ViewBuilder private func AttemptDetailsSection() -> some View { Section("Attempt Result") { ForEach(AttemptResult.allCases, id: \.self) { result in HStack { Text(result.displayName) Spacer() if selectedResult == result { Image(systemName: "checkmark.circle.fill") .foregroundColor(.blue) } else { Image(systemName: "circle") .foregroundColor(.gray) } } .contentShape(Rectangle()) .onTapGesture { selectedResult = result } } } Section("Additional Details") { TextField("Highest Hold (Optional)", text: $highestHold) VStack(alignment: .leading, spacing: 8) { Text("Notes (Optional)") .font(.headline) TextEditor(text: $notes) .frame(minHeight: 80) .padding(8) .background( RoundedRectangle(cornerRadius: 8) .fill(.quaternary) ) } HStack { Text("Duration (seconds)") Spacer() TextField("0", value: $duration, format: .number) .keyboardType(.numberPad) .textFieldStyle(.roundedBorder) .frame(width: 80) } HStack { Text("Rest Time (seconds)") Spacer() TextField("0", value: $restTime, format: .number) .keyboardType(.numberPad) .textFieldStyle(.roundedBorder) .frame(width: 80) } } } private var canSave: Bool { if showingCreateProblem { return !newProblemGrade.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } else { return selectedProblem != nil } } private func setupInitialValues() { // Auto-select climb type if there's only one available if gym.supportedClimbTypes.count == 1 { selectedClimbType = gym.supportedClimbTypes.first! } updateDifficultySystem() } private func updateDifficultySystem() { let available = availableDifficultySystems if !available.contains(selectedDifficultySystem) { selectedDifficultySystem = available.first ?? .custom } if available.count == 1 { selectedDifficultySystem = available.first! } } private func resetGradeIfNeeded() { let availableGrades = selectedDifficultySystem.availableGrades if !availableGrades.isEmpty && !availableGrades.contains(newProblemGrade) { newProblemGrade = "" } } private func saveAttempt() { if showingCreateProblem { let difficulty = DifficultyGrade( system: selectedDifficultySystem, grade: newProblemGrade) let newProblem = Problem( gymId: gym.id, name: newProblemName.isEmpty ? nil : newProblemName, climbType: selectedClimbType, difficulty: difficulty ) dataManager.addProblem(newProblem) let attempt = Attempt( sessionId: session.id, problemId: newProblem.id, result: selectedResult, highestHold: highestHold.isEmpty ? nil : highestHold, notes: notes.isEmpty ? nil : notes, duration: duration == 0 ? nil : duration, restTime: restTime == 0 ? nil : restTime, timestamp: Date() ) dataManager.addAttempt(attempt) } else { guard let problem = selectedProblem else { return } let attempt = Attempt( sessionId: session.id, problemId: problem.id, result: selectedResult, highestHold: highestHold.isEmpty ? nil : highestHold, notes: notes.isEmpty ? nil : notes, duration: duration > 0 ? duration : nil, restTime: restTime > 0 ? restTime : nil ) dataManager.addAttempt(attempt) } dismiss() } } struct ProblemSelectionRow: View { let problem: Problem let isSelected: Bool let action: () -> Void var body: some View { HStack { VStack(alignment: .leading, spacing: 4) { Text(problem.name ?? "Unnamed Problem") .font(.headline) .fontWeight(.medium) Text("\(problem.difficulty.system.displayName): \(problem.difficulty.grade)") .font(.subheadline) .foregroundColor(.blue) if let location = problem.location { Text(location) .font(.caption) .foregroundColor(.secondary) } } Spacer() if isSelected { Image(systemName: "checkmark.circle.fill") .foregroundColor(.blue) } else { Image(systemName: "circle") .foregroundColor(.gray) } } .contentShape(Rectangle()) .onTapGesture(perform: action) .padding(.vertical, 4) } } struct ProblemSelectionCard: View { let problem: Problem let isSelected: Bool let action: () -> Void @State private var showingExpandedView = false var body: some View { VStack(spacing: 8) { // Image section ZStack { if let firstImagePath = problem.imagePaths.first { ProblemSelectionImageView(imagePath: firstImagePath) } else { RoundedRectangle(cornerRadius: 8) .fill(.gray.opacity(0.2)) .frame(height: 80) .overlay { Image(systemName: "mountain.2.fill") .foregroundColor(.gray) .font(.title2) } } // Selection indicator VStack { HStack { Spacer() if isSelected { Image(systemName: "checkmark.circle.fill") .foregroundColor(.white) .background(Circle().fill(.blue)) .font(.title3) } } Spacer() } .padding(6) // Multiple images indicator if problem.imagePaths.count > 1 { VStack { Spacer() HStack { Spacer() Text("+\(problem.imagePaths.count - 1)") .font(.caption2) .fontWeight(.bold) .foregroundColor(.white) .padding(.horizontal, 6) .padding(.vertical, 2) .background( RoundedRectangle(cornerRadius: 4) .fill(.black.opacity(0.6)) ) } } .padding(6) } } // Problem info VStack(alignment: .leading, spacing: 4) { Text(problem.name ?? "Unnamed") .font(.caption) .fontWeight(.medium) .lineLimit(1) Text(problem.difficulty.grade) .font(.caption2) .fontWeight(.bold) .foregroundColor(.blue) if let location = problem.location { Text(location) .font(.caption2) .foregroundColor(.secondary) .lineLimit(1) } } .frame(maxWidth: .infinity, alignment: .leading) } .padding(8) .background( RoundedRectangle(cornerRadius: 12) .fill(isSelected ? .blue.opacity(0.1) : .gray.opacity(0.05)) .stroke(isSelected ? .blue : .clear, lineWidth: 2) ) .contentShape(Rectangle()) .onTapGesture { if isSelected { showingExpandedView = true } else { action() } } .sheet(isPresented: $showingExpandedView) { ProblemExpandedView(problem: problem) } } } struct ProblemExpandedView: View { let problem: Problem @Environment(\.dismiss) private var dismiss @State private var selectedImageIndex = 0 var body: some View { NavigationView { ScrollView { VStack(alignment: .leading, spacing: 16) { // Images if !problem.imagePaths.isEmpty { TabView(selection: $selectedImageIndex) { ForEach(problem.imagePaths.indices, id: \.self) { index in ProblemSelectionImageFullView(imagePath: problem.imagePaths[index]) .tag(index) } } .frame(height: 250) .tabViewStyle(.page(indexDisplayMode: .always)) } // Problem details VStack(alignment: .leading, spacing: 12) { Text(problem.name ?? "Unnamed Problem") .font(.title2) .fontWeight(.bold) HStack { Text(problem.difficulty.grade) .font(.title3) .fontWeight(.bold) .foregroundColor(.blue) Text(problem.climbType.displayName) .font(.subheadline) .foregroundColor(.secondary) } if let location = problem.location, !location.isEmpty { Label(location, systemImage: "location") .font(.subheadline) .foregroundColor(.secondary) } if let setter = problem.setter, !setter.isEmpty { Label(setter, systemImage: "person") .font(.subheadline) .foregroundColor(.secondary) } if let description = problem.description, !description.isEmpty { Text(description) .font(.body) } if !problem.tags.isEmpty { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(problem.tags, id: \.self) { tag in Text(tag) .font(.caption) .padding(.horizontal, 8) .padding(.vertical, 4) .background( RoundedRectangle(cornerRadius: 8) .fill(.blue.opacity(0.1)) ) .foregroundColor(.blue) } } .padding(.horizontal) } } } .padding(.horizontal) } } .navigationTitle("Problem Details") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("Done") { dismiss() } } } } } } struct EditAttemptView: View { let attempt: Attempt @EnvironmentObject var dataManager: ClimbingDataManager @Environment(\.dismiss) private var dismiss @State private var selectedProblem: Problem? @State private var selectedResult: AttemptResult @State private var highestHold: String @State private var notes: String @State private var duration: Int @State private var restTime: Int private var availableProblems: [Problem] { dataManager.problems.filter { $0.isActive } } init(attempt: Attempt) { self.attempt = attempt self._selectedResult = State(initialValue: attempt.result) self._highestHold = State(initialValue: attempt.highestHold ?? "") self._notes = State(initialValue: attempt.notes ?? "") self._duration = State(initialValue: attempt.duration ?? 0) self._restTime = State(initialValue: attempt.restTime ?? 0) } var body: some View { NavigationView { Form { Section("Select Problem") { if availableProblems.isEmpty { Text("No problems available") .foregroundColor(.secondary) } else { LazyVGrid( columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2), spacing: 8 ) { ForEach(availableProblems, id: \.id) { problem in ProblemSelectionCard( problem: problem, isSelected: selectedProblem?.id == problem.id ) { selectedProblem = problem } } } .padding(.vertical, 8) } } Section("Result") { ForEach(AttemptResult.allCases, id: \.self) { result in HStack { Text(result.displayName) Spacer() if selectedResult == result { Image(systemName: "checkmark.circle.fill") .foregroundColor(.blue) } else { Image(systemName: "circle") .foregroundColor(.gray) } } .contentShape(Rectangle()) .onTapGesture { selectedResult = result } } } Section("Details") { TextField("Highest Hold (Optional)", text: $highestHold) VStack(alignment: .leading, spacing: 8) { Text("Notes (Optional)") .font(.headline) TextEditor(text: $notes) .frame(minHeight: 80) .padding(8) .background( RoundedRectangle(cornerRadius: 8) .fill(.quaternary) ) } HStack { Text("Duration (seconds)") Spacer() TextField("0", value: $duration, format: .number) .keyboardType(.numberPad) .textFieldStyle(.roundedBorder) .frame(width: 80) } HStack { Text("Rest Time (seconds)") Spacer() TextField("0", value: $restTime, format: .number) .keyboardType(.numberPad) .textFieldStyle(.roundedBorder) .frame(width: 80) } } } .navigationTitle("Edit Attempt") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { Button("Update") { updateAttempt() } .disabled(selectedProblem == nil) } } } .onAppear { selectedProblem = dataManager.problem(withId: attempt.problemId) } } private func updateAttempt() { guard selectedProblem != nil else { return } let updatedAttempt = attempt.updated( problemId: selectedProblem?.id, result: selectedResult, highestHold: highestHold.isEmpty ? nil : highestHold, notes: notes.isEmpty ? nil : notes, duration: duration > 0 ? duration : nil, restTime: restTime > 0 ? restTime : nil ) dataManager.updateAttempt(updatedAttempt) dismiss() } } #Preview { AddAttemptView( session: ClimbSession(gymId: UUID()), gym: Gym( name: "Sample Gym", supportedClimbTypes: [.boulder], difficultySystems: [.vScale] ) ) .environmentObject(ClimbingDataManager.preview) } struct ProblemSelectionImageView: View { let imagePath: String @State private var uiImage: UIImage? @State private var isLoading = true @State private var hasFailed = false var body: some View { Group { if let uiImage = uiImage { Image(uiImage: uiImage) .resizable() .aspectRatio(contentMode: .fill) .frame(height: 80) .clipped() .cornerRadius(8) } else if hasFailed { RoundedRectangle(cornerRadius: 8) .fill(.gray.opacity(0.2)) .frame(height: 80) .overlay { Image(systemName: "photo") .foregroundColor(.gray) .font(.title3) } } else { RoundedRectangle(cornerRadius: 8) .fill(.gray.opacity(0.3)) .frame(height: 80) .overlay { ProgressView() .scaleEffect(0.8) } } } .onAppear { loadImage() } } private func loadImage() { guard !imagePath.isEmpty else { hasFailed = true isLoading = false return } DispatchQueue.global(qos: .userInitiated).async { if let data = ImageManager.shared.loadImageData(fromPath: imagePath), let image = UIImage(data: data) { DispatchQueue.main.async { self.uiImage = image self.isLoading = false } } else { DispatchQueue.main.async { self.hasFailed = true self.isLoading = false } } } } } struct ProblemSelectionImageFullView: View { let imagePath: String @State private var uiImage: UIImage? @State private var isLoading = true @State private var hasFailed = false var body: some View { Group { if let uiImage = uiImage { Image(uiImage: uiImage) .resizable() .aspectRatio(contentMode: .fit) } else if hasFailed { RoundedRectangle(cornerRadius: 12) .fill(.gray.opacity(0.2)) .frame(height: 250) .overlay { VStack(spacing: 8) { Image(systemName: "photo") .foregroundColor(.gray) .font(.largeTitle) Text("Image not available") .foregroundColor(.gray) } } } else { RoundedRectangle(cornerRadius: 12) .fill(.gray.opacity(0.3)) .frame(height: 250) .overlay { ProgressView() } } } .onAppear { loadImage() } } private func loadImage() { guard !imagePath.isEmpty else { hasFailed = true isLoading = false return } DispatchQueue.global(qos: .userInitiated).async { if let data = ImageManager.shared.loadImageData(fromPath: imagePath), let image = UIImage(data: data) { DispatchQueue.main.async { self.uiImage = image self.isLoading = false } } else { DispatchQueue.main.async { self.hasFailed = true self.isLoading = false } } } } }