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

@@ -66,7 +66,7 @@ fun AllTasksScreen(
} }
) { paddingValues -> ) { paddingValues ->
when (tasksState) { when (tasksState) {
is ApiResult.Loading -> { is ApiResult.Idle, is ApiResult.Loading -> {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()

View File

@@ -111,7 +111,7 @@ fun HomeScreen(
} }
} }
} }
is ApiResult.Loading -> { is ApiResult.Idle, is ApiResult.Loading -> {
Card(modifier = Modifier.fillMaxWidth()) { Card(modifier = Modifier.fillMaxWidth()) {
Box( Box(
modifier = Modifier modifier = Modifier

View File

@@ -1,14 +1,11 @@
package com.mycrib.android.ui.screens package com.mycrib.android.ui.screens
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
@@ -32,21 +29,9 @@ fun MainScreen(
Scaffold( Scaffold(
bottomBar = { bottomBar = {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
contentAlignment = Alignment.Center
) {
NavigationBar( NavigationBar(
modifier = Modifier containerColor = MaterialTheme.colorScheme.surfaceContainer,
.widthIn(max = 500.dp) tonalElevation = 3.dp
.shadow(
elevation = 4.dp,
shape = RoundedCornerShape(20.dp)
),
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
tonalElevation = 0.dp
) { ) {
NavigationBarItem( NavigationBarItem(
icon = { Icon(Icons.Default.Home, contentDescription = "Residences") }, icon = { Icon(Icons.Default.Home, contentDescription = "Residences") },
@@ -104,7 +89,6 @@ fun MainScreen(
) )
} }
} }
}
) { paddingValues -> ) { paddingValues ->
NavHost( NavHost(
navController = navController, navController = navController,

View File

@@ -77,6 +77,9 @@ fun ProfileScreen(
errorMessage = "" errorMessage = ""
successMessage = "" successMessage = ""
} }
is ApiResult.Idle -> {
// Do nothing - initial state, no loading indicator needed
}
else -> {} else -> {}
} }
} }

View File

@@ -172,7 +172,7 @@ fun ResidenceDetailScreen(
} }
) { paddingValues -> ) { paddingValues ->
when (residenceState) { when (residenceState) {
is ApiResult.Loading -> { is ApiResult.Idle, is ApiResult.Loading -> {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -364,7 +364,7 @@ fun ResidenceDetailScreen(
} }
when (tasksState) { when (tasksState) {
is ApiResult.Loading -> { is ApiResult.Idle, is ApiResult.Loading -> {
item { item {
Box( Box(
modifier = Modifier modifier = Modifier

View File

@@ -67,7 +67,7 @@ fun ResidencesScreen(
} }
) { paddingValues -> ) { paddingValues ->
when (myResidencesState) { when (myResidencesState) {
is ApiResult.Loading -> { is ApiResult.Idle, is ApiResult.Loading -> {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()

View File

@@ -65,7 +65,7 @@ fun TasksScreen(
} }
) { paddingValues -> ) { paddingValues ->
when (tasksState) { when (tasksState) {
is ApiResult.Loading -> { is ApiResult.Idle, is ApiResult.Loading -> {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()

View File

@@ -44,7 +44,7 @@ fun VerifyEmailScreen(
errorMessage = (verifyState as ApiResult.Error).message errorMessage = (verifyState as ApiResult.Error).message
isLoading = false isLoading = false
} }
is ApiResult.Loading -> { is ApiResult.Idle, is ApiResult.Loading -> {
isLoading = true isLoading = true
errorMessage = "" errorMessage = ""
} }

View File

@@ -13,19 +13,19 @@ import kotlinx.coroutines.launch
class LookupsViewModel : ViewModel() { class LookupsViewModel : ViewModel() {
private val lookupsApi = LookupsApi() private val lookupsApi = LookupsApi()
private val _residenceTypesState = MutableStateFlow<ApiResult<ResidenceTypeResponse>>(ApiResult.Loading) private val _residenceTypesState = MutableStateFlow<ApiResult<ResidenceTypeResponse>>(ApiResult.Idle)
val residenceTypesState: StateFlow<ApiResult<ResidenceTypeResponse>> = _residenceTypesState val residenceTypesState: StateFlow<ApiResult<ResidenceTypeResponse>> = _residenceTypesState
private val _taskFrequenciesState = MutableStateFlow<ApiResult<TaskFrequencyResponse>>(ApiResult.Loading) private val _taskFrequenciesState = MutableStateFlow<ApiResult<TaskFrequencyResponse>>(ApiResult.Idle)
val taskFrequenciesState: StateFlow<ApiResult<TaskFrequencyResponse>> = _taskFrequenciesState val taskFrequenciesState: StateFlow<ApiResult<TaskFrequencyResponse>> = _taskFrequenciesState
private val _taskPrioritiesState = MutableStateFlow<ApiResult<TaskPriorityResponse>>(ApiResult.Loading) private val _taskPrioritiesState = MutableStateFlow<ApiResult<TaskPriorityResponse>>(ApiResult.Idle)
val taskPrioritiesState: StateFlow<ApiResult<TaskPriorityResponse>> = _taskPrioritiesState val taskPrioritiesState: StateFlow<ApiResult<TaskPriorityResponse>> = _taskPrioritiesState
private val _taskStatusesState = MutableStateFlow<ApiResult<TaskStatusResponse>>(ApiResult.Loading) private val _taskStatusesState = MutableStateFlow<ApiResult<TaskStatusResponse>>(ApiResult.Idle)
val taskStatusesState: StateFlow<ApiResult<TaskStatusResponse>> = _taskStatusesState val taskStatusesState: StateFlow<ApiResult<TaskStatusResponse>> = _taskStatusesState
private val _taskCategoriesState = MutableStateFlow<ApiResult<TaskCategoryResponse>>(ApiResult.Loading) private val _taskCategoriesState = MutableStateFlow<ApiResult<TaskCategoryResponse>>(ApiResult.Idle)
val taskCategoriesState: StateFlow<ApiResult<TaskCategoryResponse>> = _taskCategoriesState val taskCategoriesState: StateFlow<ApiResult<TaskCategoryResponse>> = _taskCategoriesState
// Cache flags to avoid refetching // Cache flags to avoid refetching

View File

@@ -19,31 +19,31 @@ class ResidenceViewModel : ViewModel() {
private val residenceApi = ResidenceApi() private val residenceApi = ResidenceApi()
private val taskApi = TaskApi() private val taskApi = TaskApi()
private val _residencesState = MutableStateFlow<ApiResult<List<Residence>>>(ApiResult.Loading) private val _residencesState = MutableStateFlow<ApiResult<List<Residence>>>(ApiResult.Idle)
val residencesState: StateFlow<ApiResult<List<Residence>>> = _residencesState val residencesState: StateFlow<ApiResult<List<Residence>>> = _residencesState
private val _residenceSummaryState = MutableStateFlow<ApiResult<ResidenceSummaryResponse>>(ApiResult.Loading) private val _residenceSummaryState = MutableStateFlow<ApiResult<ResidenceSummaryResponse>>(ApiResult.Idle)
val residenceSummaryState: StateFlow<ApiResult<ResidenceSummaryResponse>> = _residenceSummaryState val residenceSummaryState: StateFlow<ApiResult<ResidenceSummaryResponse>> = _residenceSummaryState
private val _createResidenceState = MutableStateFlow<ApiResult<Residence>>(ApiResult.Loading) private val _createResidenceState = MutableStateFlow<ApiResult<Residence>>(ApiResult.Idle)
val createResidenceState: StateFlow<ApiResult<Residence>> = _createResidenceState val createResidenceState: StateFlow<ApiResult<Residence>> = _createResidenceState
private val _updateResidenceState = MutableStateFlow<ApiResult<Residence>>(ApiResult.Loading) private val _updateResidenceState = MutableStateFlow<ApiResult<Residence>>(ApiResult.Idle)
val updateResidenceState: StateFlow<ApiResult<Residence>> = _updateResidenceState val updateResidenceState: StateFlow<ApiResult<Residence>> = _updateResidenceState
private val _residenceTasksState = MutableStateFlow<ApiResult<TasksByResidenceResponse>>(ApiResult.Loading) private val _residenceTasksState = MutableStateFlow<ApiResult<TasksByResidenceResponse>>(ApiResult.Idle)
val residenceTasksState: StateFlow<ApiResult<TasksByResidenceResponse>> = _residenceTasksState val residenceTasksState: StateFlow<ApiResult<TasksByResidenceResponse>> = _residenceTasksState
private val _myResidencesState = MutableStateFlow<ApiResult<MyResidencesResponse>>(ApiResult.Loading) private val _myResidencesState = MutableStateFlow<ApiResult<MyResidencesResponse>>(ApiResult.Idle)
val myResidencesState: StateFlow<ApiResult<MyResidencesResponse>> = _myResidencesState val myResidencesState: StateFlow<ApiResult<MyResidencesResponse>> = _myResidencesState
private val _cancelTaskState = MutableStateFlow<ApiResult<com.mycrib.shared.models.TaskCancelResponse>>(ApiResult.Loading) private val _cancelTaskState = MutableStateFlow<ApiResult<com.mycrib.shared.models.TaskCancelResponse>>(ApiResult.Idle)
val cancelTaskState: StateFlow<ApiResult<com.mycrib.shared.models.TaskCancelResponse>> = _cancelTaskState val cancelTaskState: StateFlow<ApiResult<com.mycrib.shared.models.TaskCancelResponse>> = _cancelTaskState
private val _uncancelTaskState = MutableStateFlow<ApiResult<com.mycrib.shared.models.TaskCancelResponse>>(ApiResult.Loading) private val _uncancelTaskState = MutableStateFlow<ApiResult<com.mycrib.shared.models.TaskCancelResponse>>(ApiResult.Idle)
val uncancelTaskState: StateFlow<ApiResult<com.mycrib.shared.models.TaskCancelResponse>> = _uncancelTaskState val uncancelTaskState: StateFlow<ApiResult<com.mycrib.shared.models.TaskCancelResponse>> = _uncancelTaskState
private val _updateTaskState = MutableStateFlow<ApiResult<com.mycrib.shared.models.CustomTask>>(ApiResult.Loading) private val _updateTaskState = MutableStateFlow<ApiResult<com.mycrib.shared.models.CustomTask>>(ApiResult.Idle)
val updateTaskState: StateFlow<ApiResult<com.mycrib.shared.models.CustomTask>> = _updateTaskState val updateTaskState: StateFlow<ApiResult<com.mycrib.shared.models.CustomTask>> = _updateTaskState
fun loadResidences() { fun loadResidences() {
@@ -95,7 +95,7 @@ class ResidenceViewModel : ViewModel() {
} }
fun resetResidenceTasksState() { fun resetResidenceTasksState() {
_residenceTasksState.value = ApiResult.Loading _residenceTasksState.value = ApiResult.Idle
} }
fun loadResidenceTasks(residenceId: Int) { fun loadResidenceTasks(residenceId: Int) {
@@ -123,11 +123,11 @@ class ResidenceViewModel : ViewModel() {
} }
fun resetCreateState() { fun resetCreateState() {
_createResidenceState.value = ApiResult.Loading _createResidenceState.value = ApiResult.Idle
} }
fun resetUpdateState() { fun resetUpdateState() {
_updateResidenceState.value = ApiResult.Loading _updateResidenceState.value = ApiResult.Idle
} }
fun loadMyResidences() { fun loadMyResidences() {
@@ -179,14 +179,14 @@ class ResidenceViewModel : ViewModel() {
} }
fun resetCancelTaskState() { fun resetCancelTaskState() {
_cancelTaskState.value = ApiResult.Loading _cancelTaskState.value = ApiResult.Idle
} }
fun resetUncancelTaskState() { fun resetUncancelTaskState() {
_uncancelTaskState.value = ApiResult.Loading _uncancelTaskState.value = ApiResult.Idle
} }
fun resetUpdateTaskState() { fun resetUpdateTaskState() {
_updateTaskState.value = ApiResult.Loading _updateTaskState.value = ApiResult.Idle
} }
} }

View File

@@ -14,7 +14,7 @@ import kotlinx.coroutines.launch
class TaskCompletionViewModel : ViewModel() { class TaskCompletionViewModel : ViewModel() {
private val taskCompletionApi = TaskCompletionApi() private val taskCompletionApi = TaskCompletionApi()
private val _createCompletionState = MutableStateFlow<ApiResult<TaskCompletion>>(ApiResult.Loading) private val _createCompletionState = MutableStateFlow<ApiResult<TaskCompletion>>(ApiResult.Idle)
val createCompletionState: StateFlow<ApiResult<TaskCompletion>> = _createCompletionState val createCompletionState: StateFlow<ApiResult<TaskCompletion>> = _createCompletionState
fun createTaskCompletion(request: TaskCompletionCreateRequest) { fun createTaskCompletion(request: TaskCompletionCreateRequest) {
@@ -58,6 +58,6 @@ class TaskCompletionViewModel : ViewModel() {
} }
fun resetCreateState() { fun resetCreateState() {
_createCompletionState.value = ApiResult.Loading _createCompletionState.value = ApiResult.Idle
} }
} }

View File

@@ -16,13 +16,13 @@ import kotlinx.coroutines.launch
class TaskViewModel : ViewModel() { class TaskViewModel : ViewModel() {
private val taskApi = TaskApi() private val taskApi = TaskApi()
private val _tasksState = MutableStateFlow<ApiResult<AllTasksResponse>>(ApiResult.Loading) private val _tasksState = MutableStateFlow<ApiResult<AllTasksResponse>>(ApiResult.Idle)
val tasksState: StateFlow<ApiResult<AllTasksResponse>> = _tasksState val tasksState: StateFlow<ApiResult<AllTasksResponse>> = _tasksState
private val _tasksByResidenceState = MutableStateFlow<ApiResult<TasksByResidenceResponse>>(ApiResult.Loading) private val _tasksByResidenceState = MutableStateFlow<ApiResult<TasksByResidenceResponse>>(ApiResult.Idle)
val tasksByResidenceState: StateFlow<ApiResult<TasksByResidenceResponse>> = _tasksByResidenceState val tasksByResidenceState: StateFlow<ApiResult<TasksByResidenceResponse>> = _tasksByResidenceState
private val _taskAddNewCustomTaskState = MutableStateFlow<ApiResult<CustomTask>>(ApiResult.Loading) private val _taskAddNewCustomTaskState = MutableStateFlow<ApiResult<CustomTask>>(ApiResult.Idle)
val taskAddNewCustomTaskState: StateFlow<ApiResult<CustomTask>> = _taskAddNewCustomTaskState val taskAddNewCustomTaskState: StateFlow<ApiResult<CustomTask>> = _taskAddNewCustomTaskState
fun loadTasks() { fun loadTasks() {
@@ -62,7 +62,7 @@ class TaskViewModel : ViewModel() {
fun resetAddTaskState() { fun resetAddTaskState() {
_taskAddNewCustomTaskState.value = ApiResult.Loading // or ApiResult.Idle if you have it _taskAddNewCustomTaskState.value = ApiResult.Idle
} }
fun markInProgress(taskId: Int, onComplete: (Boolean) -> Unit) { fun markInProgress(taskId: Int, onComplete: (Boolean) -> Unit) {

View 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
}

View File

@@ -14,7 +14,6 @@ struct ResidenceDetailView: View {
@State private var selectedTaskForEdit: TaskDetail? @State private var selectedTaskForEdit: TaskDetail?
@State private var showInProgressTasks = false @State private var showInProgressTasks = false
@State private var showDoneTasks = false @State private var showDoneTasks = false
@State private var showCompleteTask = false
@State private var selectedTaskForComplete: TaskDetail? @State private var selectedTaskForComplete: TaskDetail?
var body: some View { var body: some View {
@@ -65,7 +64,6 @@ struct ResidenceDetailView: View {
}, },
onCompleteTask: { task in onCompleteTask: { task in
selectedTaskForComplete = task selectedTaskForComplete = task
showCompleteTask = true
} }
) )
.padding(.horizontal) .padding(.horizontal)
@@ -115,13 +113,12 @@ struct ResidenceDetailView: View {
EditTaskView(task: task, isPresented: $showEditTask) EditTaskView(task: task, isPresented: $showEditTask)
} }
} }
.sheet(isPresented: $showCompleteTask) { .sheet(item: $selectedTaskForComplete) { task in
if let task = selectedTaskForComplete { CompleteTaskView(task: task, isPresented: .constant(true)) {
CompleteTaskView(task: task, isPresented: $showCompleteTask) { selectedTaskForComplete = nil
loadResidenceTasks() loadResidenceTasks()
} }
} }
}
.onChange(of: showAddTask) { isShowing in .onChange(of: showAddTask) { isShowing in
if !isShowing { if !isShowing {
loadResidenceTasks() loadResidenceTasks()

View File

@@ -11,7 +11,6 @@ struct AllTasksView: View {
@State private var selectedTaskForEdit: TaskDetail? @State private var selectedTaskForEdit: TaskDetail?
@State private var showInProgressTasks = false @State private var showInProgressTasks = false
@State private var showDoneTasks = false @State private var showDoneTasks = false
@State private var showCompleteTask = false
@State private var selectedTaskForComplete: TaskDetail? @State private var selectedTaskForComplete: TaskDetail?
var body: some View { var body: some View {
@@ -78,7 +77,6 @@ struct AllTasksView: View {
}, },
onCompleteTask: { task in onCompleteTask: { task in
selectedTaskForComplete = task selectedTaskForComplete = task
showCompleteTask = true
} }
) )
.padding(.horizontal) .padding(.horizontal)
@@ -94,13 +92,12 @@ struct AllTasksView: View {
EditTaskView(task: task, isPresented: $showEditTask) EditTaskView(task: task, isPresented: $showEditTask)
} }
} }
.sheet(isPresented: $showCompleteTask) { .sheet(item: $selectedTaskForComplete) { task in
if let task = selectedTaskForComplete { CompleteTaskView(task: task, isPresented: .constant(true)) {
CompleteTaskView(task: task, isPresented: $showCompleteTask) { selectedTaskForComplete = nil
loadAllTasks() loadAllTasks()
} }
} }
}
.onChange(of: showEditTask) { isShowing in .onChange(of: showEditTask) { isShowing in
if !isShowing { if !isShowing {
loadAllTasks() loadAllTasks()

View File

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