import SwiftUI import PhotosUI import ComposeApp struct CompleteTaskView: View { let task: TaskDetail let onComplete: () -> Void @Environment(\.dismiss) private var dismiss @StateObject private var taskViewModel = TaskViewModel() @StateObject private var contractorViewModel = ContractorViewModel() @State private var completedByName: String = "" @State private var actualCost: String = "" @State private var notes: String = "" @State private var rating: Int = 3 @State private var selectedItems: [PhotosPickerItem] = [] @State private var selectedImages: [UIImage] = [] @State private var isSubmitting: Bool = false @State private var showError: Bool = false @State private var errorMessage: String = "" @State private var showCamera: Bool = false @State private var selectedContractor: ContractorSummary? = nil @State private var showContractorPicker: Bool = false var body: some View { NavigationStack { Form { // Task Info Section Section { VStack(alignment: .leading, spacing: 8) { Text(task.title) .font(.headline) HStack { Label(task.category.name.capitalized, systemImage: "folder") .font(.subheadline) .foregroundStyle(.secondary) Spacer() if let status = task.status { Text(status.displayName) .font(.caption) .foregroundStyle(.secondary) .padding(.horizontal, 8) .padding(.vertical, 4) .background(.quaternary) .clipShape(Capsule()) } } } } header: { Text("Task Details") } // Contractor Selection Section Section { Button(action: { showContractorPicker = true }) { HStack { Label("Select Contractor", systemImage: "wrench.and.screwdriver") .foregroundStyle(.primary) Spacer() if let contractor = selectedContractor { VStack(alignment: .trailing) { Text(contractor.name) .foregroundStyle(.secondary) if let company = contractor.company { Text(company) .font(.caption) .foregroundStyle(.tertiary) } } } else { Text("None") .foregroundStyle(.tertiary) } Image(systemName: "chevron.right") .font(.caption) .foregroundStyle(.tertiary) } } } header: { Text("Contractor (Optional)") } footer: { Text("Select a contractor if they completed this work, or leave blank for manual entry.") } // Completion Details Section Section { LabeledContent { TextField("Your name", text: $completedByName) .multilineTextAlignment(.trailing) .disabled(selectedContractor != nil) } label: { Label("Completed By", systemImage: "person") } LabeledContent { TextField("0.00", text: $actualCost) .keyboardType(.decimalPad) .multilineTextAlignment(.trailing) .overlay(alignment: .leading) { Text("$") .foregroundStyle(.secondary) } .padding(.leading, 12) } label: { Label("Actual Cost", systemImage: "dollarsign.circle") } } header: { Text("Optional Information") } footer: { Text("Add any additional details about completing this task.") } // Notes Section Section { VStack(alignment: .leading, spacing: 8) { Label("Notes", systemImage: "note.text") .font(.subheadline) .foregroundStyle(.secondary) TextEditor(text: $notes) .frame(minHeight: 100) .scrollContentBackground(.hidden) } } footer: { Text("Optional notes about the work completed.") } // Rating Section Section { VStack(spacing: 12) { HStack { Label("Quality Rating", systemImage: "star") .font(.subheadline) Spacer() Text("\(rating) / 5") .font(.subheadline) .foregroundStyle(.secondary) } HStack(spacing: 16) { ForEach(1...5, id: \.self) { star in Image(systemName: star <= rating ? "star.fill" : "star") .font(.title2) .foregroundStyle(star <= rating ? .yellow : .gray) .symbolRenderingMode(.hierarchical) .onTapGesture { withAnimation(.easeInOut(duration: 0.2)) { rating = star } } } } .frame(maxWidth: .infinity) } } footer: { Text("Rate the quality of work from 1 to 5 stars.") } // Images Section Section { VStack(alignment: .leading, spacing: 12) { HStack(spacing: 12) { Button(action: { showCamera = true }) { Label("Take Photo", systemImage: "camera") .frame(maxWidth: .infinity) .foregroundStyle(.blue) } .buttonStyle(.bordered) PhotosPicker( selection: $selectedItems, maxSelectionCount: 5, matching: .images, photoLibrary: .shared() ) { Label("Library", systemImage: "photo.on.rectangle.angled") .frame(maxWidth: .infinity) .foregroundStyle(.blue) } .buttonStyle(.bordered) } .onChange(of: selectedItems) { newItems in Task { selectedImages = [] for item in newItems { if let data = try? await item.loadTransferable(type: Data.self), let image = UIImage(data: data) { selectedImages.append(image) } } } } // Display selected images if !selectedImages.isEmpty { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 12) { ForEach(selectedImages.indices, id: \.self) { index in ImageThumbnailView( image: selectedImages[index], onRemove: { withAnimation { selectedImages.remove(at: index) selectedItems.remove(at: index) } } ) } } .padding(.vertical, 4) } } } } header: { Text("Photos (\(selectedImages.count)/5)") } footer: { Text("Add up to 5 photos documenting the completed work.") } // Complete Button Section Section { Button(action: handleComplete) { HStack { if isSubmitting { ProgressView() .tint(.white) } else { Label("Complete Task", systemImage: "checkmark.circle.fill") } } .frame(maxWidth: .infinity) .fontWeight(.semibold) } .listRowBackground(isSubmitting ? Color.gray : Color.green) .foregroundStyle(.white) .disabled(isSubmitting) } } .navigationTitle("Complete Task") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } .alert("Error", isPresented: $showError) { Button("OK", role: .cancel) {} } message: { Text(errorMessage) } .sheet(isPresented: $showCamera) { CameraPickerView { image in if selectedImages.count < 5 { selectedImages.append(image) } } } .sheet(isPresented: $showContractorPicker) { ContractorPickerView( selectedContractor: $selectedContractor, contractorViewModel: contractorViewModel ) } .onAppear { contractorViewModel.loadContractors() } .handleErrors( error: errorMessage, onRetry: { handleComplete() } ) } } private func handleComplete() { guard TokenStorage.shared.getToken() != nil else { errorMessage = "Not authenticated" showError = true return } isSubmitting = true // Get current date in ISO format let dateFormatter = ISO8601DateFormatter() let currentDate = dateFormatter.string(from: Date()) // Create request let request = TaskCompletionCreateRequest( task: task.id, completedByUser: nil, contractor: selectedContractor != nil ? KotlinInt(int: selectedContractor!.id) : nil, completedByName: completedByName.isEmpty ? nil : completedByName, completedByPhone: selectedContractor?.phone ?? "", completedByEmail: "", companyName: selectedContractor?.company ?? "", completionDate: currentDate, actualCost: actualCost.isEmpty ? nil : actualCost, notes: notes.isEmpty ? nil : notes, rating: KotlinInt(int: Int32(rating)) ) Task { do { let result: ApiResult // If there are images, upload with images if !selectedImages.isEmpty { // Compress images to meet size requirements let imageDataArray = ImageCompression.compressImages(selectedImages) let imageByteArrays = imageDataArray.map { KotlinByteArray(data: $0) } let fileNames = (0.. { self.isSubmitting = false self.dismiss() self.onComplete() } else if let errorResult = result as? ApiResultError { self.errorMessage = errorResult.message self.showError = true self.isSubmitting = false } else { self.errorMessage = "Failed to complete task" self.showError = true self.isSubmitting = false } } } catch { await MainActor.run { self.errorMessage = error.localizedDescription self.showError = true self.isSubmitting = false } } } } } // Helper extension to convert Data to KotlinByteArray extension KotlinByteArray { convenience init(data: Data) { let array = [UInt8](data) self.init(size: Int32(array.count)) for (index, byte) in array.enumerated() { self.set(index: Int32(index), value: Int8(bitPattern: byte)) } } } // MARK: - Contractor Picker View struct ContractorPickerView: View { @Environment(\.dismiss) private var dismiss @Binding var selectedContractor: ContractorSummary? @ObservedObject var contractorViewModel: ContractorViewModel var body: some View { NavigationStack { List { // None option Button(action: { selectedContractor = nil dismiss() }) { HStack { VStack(alignment: .leading) { Text("None (Manual Entry)") .foregroundStyle(.primary) Text("Enter name manually") .font(.caption) .foregroundStyle(.secondary) } Spacer() if selectedContractor == nil { Image(systemName: "checkmark") .foregroundStyle(.blue) } } } // Contractors list if contractorViewModel.isLoading { HStack { Spacer() ProgressView() Spacer() } } else if let errorMessage = contractorViewModel.errorMessage { Text(errorMessage) .foregroundStyle(.red) .font(.caption) } else { ForEach(contractorViewModel.contractors, id: \.id) { contractor in Button(action: { selectedContractor = contractor dismiss() }) { HStack { VStack(alignment: .leading, spacing: 4) { Text(contractor.name) .foregroundStyle(.primary) if let company = contractor.company { Text(company) .font(.caption) .foregroundStyle(.secondary) } if let specialty = contractor.specialty { HStack(spacing: 4) { Image(systemName: "wrench.and.screwdriver") .font(.caption2) Text(specialty) .font(.caption2) } .foregroundStyle(.tertiary) } } Spacer() if selectedContractor?.id == contractor.id { Image(systemName: "checkmark") .foregroundStyle(.blue) } } } } } } .navigationTitle("Select Contractor") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } } } }