import PhotosUI import SwiftUI struct AddEditProblemView: View { let problemId: UUID? let gymId: UUID? @EnvironmentObject var dataManager: ClimbingDataManager @Environment(\.dismiss) private var dismiss @State private var selectedGym: Gym? @State private var name = "" @State private var description = "" @State private var selectedClimbType: ClimbType = .boulder @State private var selectedDifficultySystem: DifficultySystem = .vScale @State private var difficultyGrade = "" @State private var setter = "" @State private var location = "" @State private var tags = "" @State private var notes = "" @State private var isActive = true @State private var dateSet = Date() @State private var imagePaths: [String] = [] @State private var selectedPhotos: [PhotosPickerItem] = [] @State private var imageData: [Data] = [] @State private var isEditing = false private var existingProblem: Problem? { guard let problemId = problemId else { return nil } return dataManager.problem(withId: problemId) } private var availableClimbTypes: [ClimbType] { selectedGym?.supportedClimbTypes ?? ClimbType.allCases } var availableDifficultySystems: [DifficultySystem] { guard let gym = selectedGym else { return DifficultySystem.systemsForClimbType(selectedClimbType) } let compatibleSystems = DifficultySystem.systemsForClimbType(selectedClimbType) let gymSupportedSystems = gym.difficultySystems.filter { system in compatibleSystems.contains(system) } return gymSupportedSystems.isEmpty ? compatibleSystems : gymSupportedSystems } private var availableGrades: [String] { selectedDifficultySystem.availableGrades } init(problemId: UUID? = nil, gymId: UUID? = nil) { self.problemId = problemId self.gymId = gymId } var body: some View { NavigationView { Form { GymSelectionSection() BasicInfoSection() PhotosSection() ClimbTypeSection() DifficultySection() LocationAndSetterSection() TagsSection() AdditionalInfoSection() } .navigationTitle(isEditing ? "Edit Problem" : "Add Problem") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { Button("Save") { saveProblem() } .disabled(!canSave) } } } .onAppear { loadExistingProblem() setupInitialGym() } .onChange(of: selectedGym) { updateAvailableOptions() } .onChange(of: selectedClimbType) { updateDifficultySystem() } .onChange(of: selectedDifficultySystem) { resetGradeIfNeeded() } .onChange(of: selectedPhotos) { Task { await loadSelectedPhotos() } } } @ViewBuilder private func GymSelectionSection() -> some View { Section("Select Gym") { if dataManager.gyms.isEmpty { Text("No gyms available. Add a gym first.") .foregroundColor(.secondary) } else { ForEach(dataManager.gyms, id: \.id) { gym in HStack { VStack(alignment: .leading, spacing: 4) { Text(gym.name) .font(.headline) if let location = gym.location, !location.isEmpty { Text(location) .font(.caption) .foregroundColor(.secondary) } } Spacer() if selectedGym?.id == gym.id { Image(systemName: "checkmark.circle.fill") .foregroundColor(.blue) } } .contentShape(Rectangle()) .onTapGesture { selectedGym = gym } } } } } @ViewBuilder private func BasicInfoSection() -> some View { Section("Problem Details") { TextField("Problem Name (Optional)", text: $name) VStack(alignment: .leading, spacing: 8) { Text("Description (Optional)") .font(.headline) TextEditor(text: $description) .frame(minHeight: 80) .padding(8) .background( RoundedRectangle(cornerRadius: 8) .fill(.quaternary) ) } TextField("Route Setter (Optional)", text: $setter) } } @ViewBuilder private func ClimbTypeSection() -> some View { if selectedGym != nil { 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 } } } } } @ViewBuilder private func DifficultySection() -> some View { Section("Difficulty") { // Difficulty System VStack(alignment: .leading, spacing: 8) { Text("Difficulty System") .font(.headline) 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 } } } // Grade Selection VStack(alignment: .leading, spacing: 8) { Text("Grade (Required)") .font(.headline) if selectedDifficultySystem == .custom || availableGrades.isEmpty { TextField("Enter custom grade (numbers only)", text: $difficultyGrade) .textFieldStyle(.roundedBorder) .keyboardType(.numberPad) .onChange(of: difficultyGrade) { // Filter out non-numeric characters difficultyGrade = difficultyGrade.filter { $0.isNumber } } } else { Menu { if !difficultyGrade.isEmpty { Button("Clear Selection") { difficultyGrade = "" } Divider() } ForEach(availableGrades, id: \.self) { grade in Button(grade) { difficultyGrade = grade } } } label: { HStack { Text(difficultyGrade.isEmpty ? "Select Grade" : difficultyGrade) .foregroundColor(difficultyGrade.isEmpty ? .secondary : .primary) .fontWeight(difficultyGrade.isEmpty ? .regular : .semibold) Spacer() Image(systemName: "chevron.down") .foregroundColor(.secondary) .font(.caption) } .padding() .background( RoundedRectangle(cornerRadius: 8) .fill(.gray.opacity(0.1)) .stroke( difficultyGrade.isEmpty ? .red.opacity(0.5) : .gray.opacity(0.3), lineWidth: 1) ) } .buttonStyle(.plain) } if difficultyGrade.isEmpty { Text("Please select a grade to continue") .font(.caption) .foregroundColor(.red) .italic() } else { Text("Selected: \(difficultyGrade)") .font(.caption) .foregroundColor(.blue) } } } } @ViewBuilder private func LocationAndSetterSection() -> some View { Section("Location & Details") { TextField( "Location (Optional)", text: $location, prompt: Text("e.g., 'Cave area', 'Wall 3'")) DatePicker( "Date Set", selection: $dateSet, displayedComponents: [.date] ) } } @ViewBuilder private func TagsSection() -> some View { Section("Tags (Optional)") { TextField("Tags", text: $tags, prompt: Text("e.g., crimpy, dynamic (comma-separated)")) } } @ViewBuilder private func PhotosSection() -> some View { Section("Photos (Optional)") { PhotosPicker( selection: $selectedPhotos, maxSelectionCount: 5, matching: .images ) { HStack { Image(systemName: "camera.fill") .foregroundColor(.blue) .font(.title2) VStack(alignment: .leading, spacing: 2) { Text("Add Photos") .font(.headline) .foregroundColor(.blue) Text("\(imageData.count) of 5 photos added") .font(.caption) .foregroundColor(.secondary) } Spacer() Image(systemName: "chevron.right") .foregroundColor(.secondary) .font(.caption) } .padding(.vertical, 4) } if !imageData.isEmpty { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 12) { ForEach(imageData.indices, id: \.self) { index in if let uiImage = UIImage(data: imageData[index]) { Image(uiImage: uiImage) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 80, height: 80) .clipped() .cornerRadius(8) .overlay(alignment: .topTrailing) { Button(action: { imageData.remove(at: index) if index < imagePaths.count { imagePaths.remove(at: index) } }) { Image(systemName: "xmark.circle.fill") .foregroundColor(.red) .background(Circle().fill(.white)) } .offset(x: 8, y: -8) } } else { RoundedRectangle(cornerRadius: 8) .fill(.gray.opacity(0.3)) .frame(width: 80, height: 80) .overlay { Image(systemName: "photo") .foregroundColor(.gray) } } } } .padding(.horizontal, 1) } } } } @ViewBuilder private func AdditionalInfoSection() -> some View { Section("Additional Information") { VStack(alignment: .leading, spacing: 8) { Text("Notes (Optional)") .font(.headline) TextEditor(text: $notes) .frame(minHeight: 80) .padding(8) .background( RoundedRectangle(cornerRadius: 8) .fill(.quaternary) ) } Toggle("Problem is currently active", isOn: $isActive) } } private var canSave: Bool { selectedGym != nil && !difficultyGrade.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } private func setupInitialGym() { if let gymId = gymId, selectedGym == nil { selectedGym = dataManager.gym(withId: gymId) } } private func loadExistingProblem() { if let problem = existingProblem { isEditing = true selectedGym = dataManager.gym(withId: problem.gymId) name = problem.name ?? "" description = problem.description ?? "" selectedClimbType = problem.climbType selectedDifficultySystem = problem.difficulty.system difficultyGrade = problem.difficulty.grade setter = problem.setter ?? "" location = problem.location ?? "" tags = problem.tags.joined(separator: ", ") notes = problem.notes ?? "" isActive = problem.isActive imagePaths = problem.imagePaths // Load image data for preview imageData = [] for imagePath in problem.imagePaths { if let data = try? Data(contentsOf: URL(fileURLWithPath: imagePath)) { imageData.append(data) } } if let dateSet = problem.dateSet { self.dateSet = dateSet } } } private func updateAvailableOptions() { guard let gym = selectedGym else { return } // Auto-select climb type if there's only one available if gym.supportedClimbTypes.count == 1, selectedClimbType != gym.supportedClimbTypes.first! { 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! { selectedDifficultySystem = available.first! } } private func resetGradeIfNeeded() { let availableGrades = selectedDifficultySystem.availableGrades if !availableGrades.isEmpty && !availableGrades.contains(difficultyGrade) { difficultyGrade = "" } } private func loadSelectedPhotos() async { for item in selectedPhotos { if let data = try? await item.loadTransferable(type: Data.self) { // Use ImageManager to save image if let relativePath = ImageManager.shared.saveImageData(data) { imagePaths.append(relativePath) imageData.append(data) } } } selectedPhotos.removeAll() } private func saveProblem() { guard let gym = selectedGym else { return } let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedDescription = description.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedSetter = setter.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedLocation = location.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedNotes = notes.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedTags = tags.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty } let difficulty = DifficultyGrade(system: selectedDifficultySystem, grade: difficultyGrade) if isEditing, let problem = existingProblem { let updatedProblem = problem.updated( name: trimmedName.isEmpty ? nil : trimmedName, description: trimmedDescription.isEmpty ? nil : trimmedDescription, climbType: selectedClimbType, difficulty: difficulty, setter: trimmedSetter.isEmpty ? nil : trimmedSetter, tags: trimmedTags, location: trimmedLocation.isEmpty ? nil : trimmedLocation, imagePaths: imagePaths, isActive: isActive, dateSet: dateSet, notes: trimmedNotes.isEmpty ? nil : trimmedNotes ) dataManager.updateProblem(updatedProblem) } else { let newProblem = Problem( gymId: gym.id, name: trimmedName.isEmpty ? nil : trimmedName, description: trimmedDescription.isEmpty ? nil : trimmedDescription, climbType: selectedClimbType, difficulty: difficulty, setter: trimmedSetter.isEmpty ? nil : trimmedSetter, tags: trimmedTags, location: trimmedLocation.isEmpty ? nil : trimmedLocation, imagePaths: imagePaths, dateSet: dateSet, notes: trimmedNotes.isEmpty ? nil : trimmedNotes ) dataManager.addProblem(newProblem) } dismiss() } } #Preview { AddEditProblemView() .environmentObject(ClimbingDataManager.preview) }