This commit is contained in:
Trey t
2025-11-06 17:53:41 -06:00
parent e24d1d8559
commit 66fe773398
16 changed files with 214 additions and 201 deletions

View File

@@ -19,123 +19,125 @@ struct CompleteTaskView: View {
@State private var errorMessage: String = ""
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 20) {
// Task Info Header
NavigationStack {
Form {
// Task Info Section
Section {
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)
.font(.headline)
HStack {
Text("$")
.foregroundColor(.secondary)
TextField("0.00", text: $actualCost)
.keyboardType(.decimalPad)
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(8)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
Label(task.category.name.capitalized, systemImage: "folder")
.font(.subheadline)
.foregroundStyle(.secondary)
// Notes
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")
}
// Completion Details Section
Section {
LabeledContent {
TextField("Your name", text: $completedByName)
.multilineTextAlignment(.trailing)
} 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) {
Text("Notes (Optional)")
Label("Notes", systemImage: "note.text")
.font(.subheadline)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
TextEditor(text: $notes)
.frame(minHeight: 100)
.padding(8)
.background(Color(.systemGray6))
.cornerRadius(8)
.scrollContentBackground(.hidden)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
} footer: {
Text("Optional notes about the work completed.")
}
// Rating
VStack(alignment: .leading, spacing: 12) {
Text("Rating")
.font(.subheadline)
.foregroundColor(.secondary)
// 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)
.foregroundColor(star <= rating ? .yellow : .gray)
.foregroundStyle(star <= rating ? .yellow : .gray)
.symbolRenderingMode(.hierarchical)
.onTapGesture {
rating = star
withAnimation(.easeInOut(duration: 0.2)) {
rating = star
}
}
}
}
Text("\(rating) out of 5")
.font(.caption)
.foregroundColor(.secondary)
.frame(maxWidth: .infinity)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
} footer: {
Text("Rate the quality of work from 1 to 5 stars.")
}
// Image Picker
// Images Section
Section {
VStack(alignment: .leading, spacing: 12) {
Text("Add Images (up to 5)")
.font(.subheadline)
.foregroundColor(.secondary)
PhotosPicker(
selection: $selectedItems,
maxSelectionCount: 5,
matching: .images
matching: .images,
photoLibrary: .shared()
) {
HStack {
Image(systemName: "photo.on.rectangle.angled")
Text("Select Images")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue.opacity(0.1))
.foregroundColor(.blue)
.cornerRadius(8)
Label("Add Photos", systemImage: "photo.on.rectangle.angled")
.frame(maxWidth: .infinity)
.foregroundStyle(.blue)
}
.buttonStyle(.bordered)
.onChange(of: selectedItems) { newItems in
Task {
selectedImages = []
@@ -153,69 +155,57 @@ struct CompleteTaskView: View {
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)))
ImageThumbnailView(
image: selectedImages[index],
onRemove: {
withAnimation {
selectedImages.remove(at: index)
selectedItems.remove(at: index)
}
}
.padding(4)
}
)
}
}
.padding(.vertical, 4)
}
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
} header: {
Text("Photos (\(selectedImages.count)/5)")
} footer: {
Text("Add up to 5 photos documenting the completed work.")
}
// Complete Button
// Complete Button Section
Section {
Button(action: handleComplete) {
HStack {
if isSubmitting {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.tint(.white)
} else {
Image(systemName: "checkmark.circle.fill")
Text("Complete Task")
.fontWeight(.semibold)
Label("Complete Task", systemImage: "checkmark.circle.fill")
}
}
.frame(maxWidth: .infinity)
.padding()
.background(isSubmitting ? Color.gray : Color.green)
.foregroundColor(.white)
.cornerRadius(12)
.fontWeight(.semibold)
}
.listRowBackground(isSubmitting ? Color.gray : Color.green)
.foregroundStyle(.white)
.disabled(isSubmitting)
.padding()
}
.padding()
}
.background(Color(.systemGroupedBackground))
.navigationTitle("Complete Task")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
isPresented = false
}
}
}
.alert("Error", isPresented: $showError) {
Button("OK") {
showError = false
}
Button("OK", role: .cancel) {}
} message: {
Text(errorMessage)
}
@@ -272,18 +262,20 @@ struct CompleteTaskView: View {
}
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
DispatchQueue.main.async {
if result is ApiResultSuccess<TaskCompletion> {
self.isSubmitting = false
self.isPresented = false
self.onComplete()
} else if let errorResult = result as? ApiResultError {
self.errorMessage = errorResult.message
self.showError = true
self.isSubmitting = false
} else if let error = error {
self.errorMessage = error.localizedDescription
self.showError = true
self.isSubmitting = false
}
}
}
}
@@ -298,3 +290,35 @@ extension KotlinByteArray {
}
}
}
// Image Thumbnail View Component
struct ImageThumbnailView: View {
let image: UIImage
let onRemove: () -> Void
var body: some View {
ZStack(alignment: .topTrailing) {
Image(uiImage: image)
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.strokeBorder(.quaternary, lineWidth: 1)
}
Button(action: onRemove) {
Image(systemName: "xmark.circle.fill")
.font(.title3)
.foregroundStyle(.white)
.background {
Circle()
.fill(.black.opacity(0.6))
.padding(4)
}
}
.offset(x: 8, y: -8)
}
}
}