// // AddAttemptView.swift // OpenClimb // // Created by OpenClimb on 2025-01-17. // 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) { _ in updateDifficultySystem() } .onChange(of: selectedDifficultySystem) { _ in 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 { ForEach(activeProblems, id: \.id) { problem in ProblemSelectionRow( problem: problem, isSelected: selectedProblem?.id == problem.id ) { selectedProblem = problem } } 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)", text: $newProblemGrade) .keyboardType(.numberPad) } 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 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("Problem") { if availableProblems.isEmpty { Text("No problems available") .foregroundColor(.secondary) } else { ForEach(availableProblems, id: \.id) { problem in HStack { VStack(alignment: .leading, spacing: 4) { Text(problem.name ?? "Unnamed Problem") .font(.headline) Text( "\(problem.difficulty.system.displayName): \(problem.difficulty.grade)" ) .font(.subheadline) .foregroundColor(.blue) } Spacer() if selectedProblem?.id == problem.id { Image(systemName: "checkmark.circle.fill") .foregroundColor(.blue) } } .contentShape(Rectangle()) .onTapGesture { selectedProblem = problem } } } } 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 let problem = selectedProblem else { return } let updatedAttempt = attempt.updated( 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) }