This commit is contained in:
Trey t
2025-11-06 09:25:21 -06:00
parent e272e45689
commit e24d1d8559
29 changed files with 1806 additions and 103 deletions

View File

@@ -0,0 +1,300 @@
import SwiftUI
import PhotosUI
import ComposeApp
struct CompleteTaskView: View {
let task: TaskDetail
@Binding var isPresented: Bool
let onComplete: () -> Void
@StateObject private var taskViewModel = TaskViewModel()
@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 = ""
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 20) {
// Task Info Header
VStack(alignment: .leading, spacing: 8) {
Text(task.title)
.font(.title2)
.fontWeight(.bold)
Text(task.category.name.capitalized)
.font(.subheadline)
.foregroundColor(.secondary)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Color.blue.opacity(0.1))
.cornerRadius(8)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
// Completed By
VStack(alignment: .leading, spacing: 8) {
Text("Completed By (Optional)")
.font(.subheadline)
.foregroundColor(.secondary)
TextField("Enter name or leave blank", text: $completedByName)
.textFieldStyle(.roundedBorder)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
// Actual Cost
VStack(alignment: .leading, spacing: 8) {
Text("Actual Cost (Optional)")
.font(.subheadline)
.foregroundColor(.secondary)
HStack {
Text("$")
.foregroundColor(.secondary)
TextField("0.00", text: $actualCost)
.keyboardType(.decimalPad)
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(8)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
// Notes
VStack(alignment: .leading, spacing: 8) {
Text("Notes (Optional)")
.font(.subheadline)
.foregroundColor(.secondary)
TextEditor(text: $notes)
.frame(minHeight: 100)
.padding(8)
.background(Color(.systemGray6))
.cornerRadius(8)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
// Rating
VStack(alignment: .leading, spacing: 12) {
Text("Rating")
.font(.subheadline)
.foregroundColor(.secondary)
HStack(spacing: 16) {
ForEach(1...5, id: \.self) { star in
Image(systemName: star <= rating ? "star.fill" : "star")
.font(.title2)
.foregroundColor(star <= rating ? .yellow : .gray)
.onTapGesture {
rating = star
}
}
}
Text("\(rating) out of 5")
.font(.caption)
.foregroundColor(.secondary)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
// Image Picker
VStack(alignment: .leading, spacing: 12) {
Text("Add Images (up to 5)")
.font(.subheadline)
.foregroundColor(.secondary)
PhotosPicker(
selection: $selectedItems,
maxSelectionCount: 5,
matching: .images
) {
HStack {
Image(systemName: "photo.on.rectangle.angled")
Text("Select Images")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue.opacity(0.1))
.foregroundColor(.blue)
.cornerRadius(8)
}
.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
ZStack(alignment: .topTrailing) {
Image(uiImage: selectedImages[index])
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipShape(RoundedRectangle(cornerRadius: 8))
Button(action: {
selectedImages.remove(at: index)
selectedItems.remove(at: index)
}) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.white)
.background(Circle().fill(Color.black.opacity(0.6)))
}
.padding(4)
}
}
}
}
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
// Complete Button
Button(action: handleComplete) {
HStack {
if isSubmitting {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
} else {
Image(systemName: "checkmark.circle.fill")
Text("Complete Task")
.fontWeight(.semibold)
}
}
.frame(maxWidth: .infinity)
.padding()
.background(isSubmitting ? Color.gray : Color.green)
.foregroundColor(.white)
.cornerRadius(12)
}
.disabled(isSubmitting)
.padding()
}
.padding()
}
.background(Color(.systemGroupedBackground))
.navigationTitle("Complete Task")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
isPresented = false
}
}
}
.alert("Error", isPresented: $showError) {
Button("OK") {
showError = false
}
} message: {
Text(errorMessage)
}
}
}
private func handleComplete() {
isSubmitting = true
guard let token = TokenStorage.shared.getToken() else {
errorMessage = "Not authenticated"
showError = true
isSubmitting = false
return
}
// 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,
completedByName: completedByName.isEmpty ? nil : completedByName,
completionDate: currentDate,
actualCost: actualCost.isEmpty ? nil : actualCost,
notes: notes.isEmpty ? nil : notes,
rating: KotlinInt(int: Int32(rating))
)
let completionApi = TaskCompletionApi(client: ApiClient_iosKt.createHttpClient())
// If there are images, upload with images
if !selectedImages.isEmpty {
let imageDataArray = selectedImages.compactMap { $0.jpegData(compressionQuality: 0.8) }
let imageByteArrays = imageDataArray.map { KotlinByteArray(data: $0) }
let fileNames = (0..<imageDataArray.count).map { "image_\($0).jpg" }
completionApi.createCompletionWithImages(
token: token,
request: request,
images: imageByteArrays,
imageFileNames: fileNames
) { result, error in
handleCompletionResult(result: result, error: error)
}
} else {
// Upload without images
completionApi.createCompletion(token: token, request: request) { result, error in
handleCompletionResult(result: result, error: error)
}
}
}
private func handleCompletionResult(result: ApiResult<TaskCompletion>?, error: Error?) {
if result is ApiResultSuccess<TaskCompletion> {
isSubmitting = false
isPresented = false
onComplete()
} else if let errorResult = result as? ApiResultError {
errorMessage = errorResult.message
showError = true
isSubmitting = false
} else if let error = error {
errorMessage = error.localizedDescription
showError = true
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))
}
}
}