wip
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ fun ProfileScreen(
|
|||||||
errorMessage = ""
|
errorMessage = ""
|
||||||
successMessage = ""
|
successMessage = ""
|
||||||
}
|
}
|
||||||
|
is ApiResult.Idle -> {
|
||||||
|
// Do nothing - initial state, no loading indicator needed
|
||||||
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 = ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
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 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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user