wip
This commit is contained in:
8
iosApp/iosApp/Extensions/TaskDetailExtensions.swift
Normal file
8
iosApp/iosApp/Extensions/TaskDetailExtensions.swift
Normal file
@@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
import ComposeApp
|
||||
|
||||
// Extension to make TaskDetail conform to Identifiable for SwiftUI
|
||||
extension TaskDetail: Identifiable {
|
||||
// TaskDetail already has an `id` property from Kotlin,
|
||||
// so we just need to declare conformance to Identifiable
|
||||
}
|
||||
@@ -14,7 +14,6 @@ struct ResidenceDetailView: View {
|
||||
@State private var selectedTaskForEdit: TaskDetail?
|
||||
@State private var showInProgressTasks = false
|
||||
@State private var showDoneTasks = false
|
||||
@State private var showCompleteTask = false
|
||||
@State private var selectedTaskForComplete: TaskDetail?
|
||||
|
||||
var body: some View {
|
||||
@@ -65,7 +64,6 @@ struct ResidenceDetailView: View {
|
||||
},
|
||||
onCompleteTask: { task in
|
||||
selectedTaskForComplete = task
|
||||
showCompleteTask = true
|
||||
}
|
||||
)
|
||||
.padding(.horizontal)
|
||||
@@ -115,11 +113,10 @@ struct ResidenceDetailView: View {
|
||||
EditTaskView(task: task, isPresented: $showEditTask)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showCompleteTask) {
|
||||
if let task = selectedTaskForComplete {
|
||||
CompleteTaskView(task: task, isPresented: $showCompleteTask) {
|
||||
loadResidenceTasks()
|
||||
}
|
||||
.sheet(item: $selectedTaskForComplete) { task in
|
||||
CompleteTaskView(task: task, isPresented: .constant(true)) {
|
||||
selectedTaskForComplete = nil
|
||||
loadResidenceTasks()
|
||||
}
|
||||
}
|
||||
.onChange(of: showAddTask) { isShowing in
|
||||
|
||||
@@ -11,7 +11,6 @@ struct AllTasksView: View {
|
||||
@State private var selectedTaskForEdit: TaskDetail?
|
||||
@State private var showInProgressTasks = false
|
||||
@State private var showDoneTasks = false
|
||||
@State private var showCompleteTask = false
|
||||
@State private var selectedTaskForComplete: TaskDetail?
|
||||
|
||||
var body: some View {
|
||||
@@ -78,7 +77,6 @@ struct AllTasksView: View {
|
||||
},
|
||||
onCompleteTask: { task in
|
||||
selectedTaskForComplete = task
|
||||
showCompleteTask = true
|
||||
}
|
||||
)
|
||||
.padding(.horizontal)
|
||||
@@ -94,11 +92,10 @@ struct AllTasksView: View {
|
||||
EditTaskView(task: task, isPresented: $showEditTask)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showCompleteTask) {
|
||||
if let task = selectedTaskForComplete {
|
||||
CompleteTaskView(task: task, isPresented: $showCompleteTask) {
|
||||
loadAllTasks()
|
||||
}
|
||||
.sheet(item: $selectedTaskForComplete) { task in
|
||||
CompleteTaskView(task: task, isPresented: .constant(true)) {
|
||||
selectedTaskForComplete = nil
|
||||
loadAllTasks()
|
||||
}
|
||||
}
|
||||
.onChange(of: showEditTask) { isShowing in
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user