diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/AllTasksScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/AllTasksScreen.kt index 84715ab..e844c23 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/AllTasksScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/AllTasksScreen.kt @@ -66,7 +66,7 @@ fun AllTasksScreen( } ) { paddingValues -> when (tasksState) { - is ApiResult.Loading -> { + is ApiResult.Idle, is ApiResult.Loading -> { Box( modifier = Modifier .fillMaxSize() diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/HomeScreen.kt index 0bf33cf..4d9f66b 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/HomeScreen.kt @@ -111,7 +111,7 @@ fun HomeScreen( } } } - is ApiResult.Loading -> { + is ApiResult.Idle, is ApiResult.Loading -> { Card(modifier = Modifier.fillMaxWidth()) { Box( modifier = Modifier diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/MainScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/MainScreen.kt index 466e31a..0b96827 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/MainScreen.kt @@ -1,14 +1,11 @@ package com.mycrib.android.ui.screens import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow import androidx.compose.ui.unit.dp import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -32,22 +29,10 @@ fun MainScreen( Scaffold( bottomBar = { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), - contentAlignment = Alignment.Center + NavigationBar( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + tonalElevation = 3.dp ) { - NavigationBar( - modifier = Modifier - .widthIn(max = 500.dp) - .shadow( - elevation = 4.dp, - shape = RoundedCornerShape(20.dp) - ), - containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - tonalElevation = 0.dp - ) { NavigationBarItem( icon = { Icon(Icons.Default.Home, contentDescription = "Residences") }, label = { Text("Residences") }, @@ -102,7 +87,6 @@ fun MainScreen( unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant ) ) - } } } ) { paddingValues -> diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ProfileScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ProfileScreen.kt index dec176b..6e9a151 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ProfileScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ProfileScreen.kt @@ -77,6 +77,9 @@ fun ProfileScreen( errorMessage = "" successMessage = "" } + is ApiResult.Idle -> { + // Do nothing - initial state, no loading indicator needed + } else -> {} } } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt index 46fa863..5921288 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt @@ -172,7 +172,7 @@ fun ResidenceDetailScreen( } ) { paddingValues -> when (residenceState) { - is ApiResult.Loading -> { + is ApiResult.Idle, is ApiResult.Loading -> { Box( modifier = Modifier .fillMaxSize() @@ -364,7 +364,7 @@ fun ResidenceDetailScreen( } when (tasksState) { - is ApiResult.Loading -> { + is ApiResult.Idle, is ApiResult.Loading -> { item { Box( modifier = Modifier diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidencesScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidencesScreen.kt index 360e1fb..9d360ef 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidencesScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidencesScreen.kt @@ -67,7 +67,7 @@ fun ResidencesScreen( } ) { paddingValues -> when (myResidencesState) { - is ApiResult.Loading -> { + is ApiResult.Idle, is ApiResult.Loading -> { Box( modifier = Modifier .fillMaxSize() diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/TasksScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/TasksScreen.kt index 81aa68d..b2b6359 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/TasksScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/TasksScreen.kt @@ -65,7 +65,7 @@ fun TasksScreen( } ) { paddingValues -> when (tasksState) { - is ApiResult.Loading -> { + is ApiResult.Idle, is ApiResult.Loading -> { Box( modifier = Modifier .fillMaxSize() diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/VerifyEmailScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/VerifyEmailScreen.kt index 0eb1591..364e4d5 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/VerifyEmailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/VerifyEmailScreen.kt @@ -44,7 +44,7 @@ fun VerifyEmailScreen( errorMessage = (verifyState as ApiResult.Error).message isLoading = false } - is ApiResult.Loading -> { + is ApiResult.Idle, is ApiResult.Loading -> { isLoading = true errorMessage = "" } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/LookupsViewModel.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/LookupsViewModel.kt index f4d6f54..8f44e8e 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/LookupsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/LookupsViewModel.kt @@ -13,19 +13,19 @@ import kotlinx.coroutines.launch class LookupsViewModel : ViewModel() { private val lookupsApi = LookupsApi() - private val _residenceTypesState = MutableStateFlow>(ApiResult.Loading) + private val _residenceTypesState = MutableStateFlow>(ApiResult.Idle) val residenceTypesState: StateFlow> = _residenceTypesState - private val _taskFrequenciesState = MutableStateFlow>(ApiResult.Loading) + private val _taskFrequenciesState = MutableStateFlow>(ApiResult.Idle) val taskFrequenciesState: StateFlow> = _taskFrequenciesState - private val _taskPrioritiesState = MutableStateFlow>(ApiResult.Loading) + private val _taskPrioritiesState = MutableStateFlow>(ApiResult.Idle) val taskPrioritiesState: StateFlow> = _taskPrioritiesState - private val _taskStatusesState = MutableStateFlow>(ApiResult.Loading) + private val _taskStatusesState = MutableStateFlow>(ApiResult.Idle) val taskStatusesState: StateFlow> = _taskStatusesState - private val _taskCategoriesState = MutableStateFlow>(ApiResult.Loading) + private val _taskCategoriesState = MutableStateFlow>(ApiResult.Idle) val taskCategoriesState: StateFlow> = _taskCategoriesState // Cache flags to avoid refetching diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/ResidenceViewModel.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/ResidenceViewModel.kt index 645dde5..933536d 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/ResidenceViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/ResidenceViewModel.kt @@ -19,31 +19,31 @@ class ResidenceViewModel : ViewModel() { private val residenceApi = ResidenceApi() private val taskApi = TaskApi() - private val _residencesState = MutableStateFlow>>(ApiResult.Loading) + private val _residencesState = MutableStateFlow>>(ApiResult.Idle) val residencesState: StateFlow>> = _residencesState - private val _residenceSummaryState = MutableStateFlow>(ApiResult.Loading) + private val _residenceSummaryState = MutableStateFlow>(ApiResult.Idle) val residenceSummaryState: StateFlow> = _residenceSummaryState - private val _createResidenceState = MutableStateFlow>(ApiResult.Loading) + private val _createResidenceState = MutableStateFlow>(ApiResult.Idle) val createResidenceState: StateFlow> = _createResidenceState - private val _updateResidenceState = MutableStateFlow>(ApiResult.Loading) + private val _updateResidenceState = MutableStateFlow>(ApiResult.Idle) val updateResidenceState: StateFlow> = _updateResidenceState - private val _residenceTasksState = MutableStateFlow>(ApiResult.Loading) + private val _residenceTasksState = MutableStateFlow>(ApiResult.Idle) val residenceTasksState: StateFlow> = _residenceTasksState - private val _myResidencesState = MutableStateFlow>(ApiResult.Loading) + private val _myResidencesState = MutableStateFlow>(ApiResult.Idle) val myResidencesState: StateFlow> = _myResidencesState - private val _cancelTaskState = MutableStateFlow>(ApiResult.Loading) + private val _cancelTaskState = MutableStateFlow>(ApiResult.Idle) val cancelTaskState: StateFlow> = _cancelTaskState - private val _uncancelTaskState = MutableStateFlow>(ApiResult.Loading) + private val _uncancelTaskState = MutableStateFlow>(ApiResult.Idle) val uncancelTaskState: StateFlow> = _uncancelTaskState - private val _updateTaskState = MutableStateFlow>(ApiResult.Loading) + private val _updateTaskState = MutableStateFlow>(ApiResult.Idle) val updateTaskState: StateFlow> = _updateTaskState fun loadResidences() { @@ -95,7 +95,7 @@ class ResidenceViewModel : ViewModel() { } fun resetResidenceTasksState() { - _residenceTasksState.value = ApiResult.Loading + _residenceTasksState.value = ApiResult.Idle } fun loadResidenceTasks(residenceId: Int) { @@ -123,11 +123,11 @@ class ResidenceViewModel : ViewModel() { } fun resetCreateState() { - _createResidenceState.value = ApiResult.Loading + _createResidenceState.value = ApiResult.Idle } fun resetUpdateState() { - _updateResidenceState.value = ApiResult.Loading + _updateResidenceState.value = ApiResult.Idle } fun loadMyResidences() { @@ -179,14 +179,14 @@ class ResidenceViewModel : ViewModel() { } fun resetCancelTaskState() { - _cancelTaskState.value = ApiResult.Loading + _cancelTaskState.value = ApiResult.Idle } fun resetUncancelTaskState() { - _uncancelTaskState.value = ApiResult.Loading + _uncancelTaskState.value = ApiResult.Idle } fun resetUpdateTaskState() { - _updateTaskState.value = ApiResult.Loading + _updateTaskState.value = ApiResult.Idle } } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/TaskCompletionViewModel.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/TaskCompletionViewModel.kt index 10ffc2c..a026312 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/TaskCompletionViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/TaskCompletionViewModel.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.launch class TaskCompletionViewModel : ViewModel() { private val taskCompletionApi = TaskCompletionApi() - private val _createCompletionState = MutableStateFlow>(ApiResult.Loading) + private val _createCompletionState = MutableStateFlow>(ApiResult.Idle) val createCompletionState: StateFlow> = _createCompletionState fun createTaskCompletion(request: TaskCompletionCreateRequest) { @@ -58,6 +58,6 @@ class TaskCompletionViewModel : ViewModel() { } fun resetCreateState() { - _createCompletionState.value = ApiResult.Loading + _createCompletionState.value = ApiResult.Idle } } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/TaskViewModel.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/TaskViewModel.kt index fe89a54..2ca9467 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/TaskViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/TaskViewModel.kt @@ -16,13 +16,13 @@ import kotlinx.coroutines.launch class TaskViewModel : ViewModel() { private val taskApi = TaskApi() - private val _tasksState = MutableStateFlow>(ApiResult.Loading) + private val _tasksState = MutableStateFlow>(ApiResult.Idle) val tasksState: StateFlow> = _tasksState - private val _tasksByResidenceState = MutableStateFlow>(ApiResult.Loading) + private val _tasksByResidenceState = MutableStateFlow>(ApiResult.Idle) val tasksByResidenceState: StateFlow> = _tasksByResidenceState - private val _taskAddNewCustomTaskState = MutableStateFlow>(ApiResult.Loading) + private val _taskAddNewCustomTaskState = MutableStateFlow>(ApiResult.Idle) val taskAddNewCustomTaskState: StateFlow> = _taskAddNewCustomTaskState fun loadTasks() { @@ -62,7 +62,7 @@ class TaskViewModel : ViewModel() { fun resetAddTaskState() { - _taskAddNewCustomTaskState.value = ApiResult.Loading // or ApiResult.Idle if you have it + _taskAddNewCustomTaskState.value = ApiResult.Idle } fun markInProgress(taskId: Int, onComplete: (Boolean) -> Unit) { diff --git a/iosApp/iosApp/Extensions/TaskDetailExtensions.swift b/iosApp/iosApp/Extensions/TaskDetailExtensions.swift new file mode 100644 index 0000000..34fbc95 --- /dev/null +++ b/iosApp/iosApp/Extensions/TaskDetailExtensions.swift @@ -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 +} diff --git a/iosApp/iosApp/Residence/ResidenceDetailView.swift b/iosApp/iosApp/Residence/ResidenceDetailView.swift index 294c4c8..c1a81a9 100644 --- a/iosApp/iosApp/Residence/ResidenceDetailView.swift +++ b/iosApp/iosApp/Residence/ResidenceDetailView.swift @@ -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 diff --git a/iosApp/iosApp/Task/AllTasksView.swift b/iosApp/iosApp/Task/AllTasksView.swift index 6ba9842..dc63698 100644 --- a/iosApp/iosApp/Task/AllTasksView.swift +++ b/iosApp/iosApp/Task/AllTasksView.swift @@ -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 diff --git a/iosApp/iosApp/Task/CompleteTaskView.swift b/iosApp/iosApp/Task/CompleteTaskView.swift index 5023677..9563a50 100644 --- a/iosApp/iosApp/Task/CompleteTaskView.swift +++ b/iosApp/iosApp/Task/CompleteTaskView.swift @@ -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?, error: Error?) { - if result is ApiResultSuccess { - 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 { + 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) + } + } +}