wip
This commit is contained in:
@@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.mycrib.android.ui.screens.AddResidenceScreen
|
||||
import com.mycrib.android.ui.screens.EditResidenceScreen
|
||||
import com.mycrib.android.ui.screens.EditTaskScreen
|
||||
import com.mycrib.android.ui.screens.HomeScreen
|
||||
import com.mycrib.android.ui.screens.LoginScreen
|
||||
import com.mycrib.android.ui.screens.RegisterScreen
|
||||
@@ -218,9 +219,60 @@ fun App() {
|
||||
owner = residence.owner
|
||||
)
|
||||
)
|
||||
},
|
||||
onNavigateToEditTask = { task ->
|
||||
navController.navigate(
|
||||
EditTaskRoute(
|
||||
taskId = task.id,
|
||||
residenceId = task.residence,
|
||||
title = task.title,
|
||||
description = task.description,
|
||||
categoryId = task.category.id,
|
||||
categoryName = task.category.name,
|
||||
frequencyId = task.frequency.id,
|
||||
frequencyName = task.frequency.name,
|
||||
priorityId = task.priority.id,
|
||||
priorityName = task.priority.name,
|
||||
statusId = task.status?.id,
|
||||
statusName = task.status?.name,
|
||||
dueDate = task.dueDate,
|
||||
estimatedCost = task.estimatedCost,
|
||||
createdAt = task.createdAt,
|
||||
updatedAt = task.updatedAt
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable<EditTaskRoute> { backStackEntry ->
|
||||
val route = backStackEntry.toRoute<EditTaskRoute>()
|
||||
EditTaskScreen(
|
||||
task = com.mycrib.shared.models.TaskDetail(
|
||||
id = route.taskId,
|
||||
residence = route.residenceId,
|
||||
title = route.title,
|
||||
description = route.description,
|
||||
category = com.mycrib.shared.models.TaskCategory(route.categoryId, route.categoryName),
|
||||
frequency = com.mycrib.shared.models.TaskFrequency(route.frequencyId, route.frequencyName, ""),
|
||||
priority = com.mycrib.shared.models.TaskPriority(route.priorityId, route.priorityName, displayName = route.statusName ?: ""),
|
||||
status = route.statusId?.let {
|
||||
com.mycrib.shared.models.TaskStatus(it, route.statusName ?: "", displayName = route.statusName ?: "")
|
||||
},
|
||||
dueDate = route.dueDate,
|
||||
estimatedCost = route.estimatedCost,
|
||||
actualCost = null,
|
||||
notes = null,
|
||||
createdAt = route.createdAt,
|
||||
updatedAt = route.updatedAt,
|
||||
nextScheduledDate = null,
|
||||
showCompletedButton = false,
|
||||
completions = emptyList()
|
||||
),
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onTaskUpdated = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,5 +75,12 @@ data class TaskDetail(
|
||||
data class TasksByResidenceResponse(
|
||||
@SerialName("residence_id") val residenceId: String,
|
||||
val summary: TaskSummary,
|
||||
val tasks: List<TaskDetail>
|
||||
val tasks: List<TaskDetail>,
|
||||
@SerialName("cancelled_tasks") val cancelledTasks: List<TaskDetail> = emptyList()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskCancelResponse(
|
||||
val message: String,
|
||||
val task: TaskDetail
|
||||
)
|
||||
|
||||
@@ -45,5 +45,25 @@ data class EditResidenceRoute(
|
||||
@Serializable
|
||||
data class ResidenceDetailRoute(val residenceId: Int)
|
||||
|
||||
@Serializable
|
||||
data class EditTaskRoute(
|
||||
val taskId: Int,
|
||||
val residenceId: Int,
|
||||
val title: String,
|
||||
val description: String?,
|
||||
val categoryId: Int,
|
||||
val categoryName: String,
|
||||
val frequencyId: Int,
|
||||
val frequencyName: String,
|
||||
val priorityId: Int,
|
||||
val priorityName: String,
|
||||
val statusId: Int?,
|
||||
val statusName: String?,
|
||||
val dueDate: String,
|
||||
val estimatedCost: String?,
|
||||
val createdAt: String,
|
||||
val updatedAt: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
object TasksRoute
|
||||
|
||||
@@ -109,4 +109,36 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun cancelTask(token: String, id: Int): ApiResult<TaskCancelResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/tasks/$id/cancel/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to cancel task", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun uncancelTask(token: String, id: Int): ApiResult<TaskCancelResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/tasks/$id/uncancel/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to uncancel task", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
package com.mycrib.android.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.mycrib.android.viewmodel.ResidenceViewModel
|
||||
import com.mycrib.repository.LookupsRepository
|
||||
import com.mycrib.shared.models.*
|
||||
import com.mycrib.shared.network.ApiResult
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun EditTaskScreen(
|
||||
task: TaskDetail,
|
||||
onNavigateBack: () -> Unit,
|
||||
onTaskUpdated: () -> Unit,
|
||||
viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }
|
||||
) {
|
||||
var title by remember { mutableStateOf(task.title) }
|
||||
var description by remember { mutableStateOf(task.description ?: "") }
|
||||
var selectedCategory by remember { mutableStateOf<TaskCategory?>(task.category) }
|
||||
var selectedFrequency by remember { mutableStateOf<TaskFrequency?>(task.frequency) }
|
||||
var selectedPriority by remember { mutableStateOf<TaskPriority?>(task.priority) }
|
||||
var selectedStatus by remember { mutableStateOf<TaskStatus?>(task.status) }
|
||||
var dueDate by remember { mutableStateOf(task.dueDate) }
|
||||
var estimatedCost by remember { mutableStateOf(task.estimatedCost ?: "") }
|
||||
|
||||
var categoryExpanded by remember { mutableStateOf(false) }
|
||||
var frequencyExpanded by remember { mutableStateOf(false) }
|
||||
var priorityExpanded by remember { mutableStateOf(false) }
|
||||
var statusExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val updateTaskState by viewModel.updateTaskState.collectAsState()
|
||||
val categories by LookupsRepository.taskCategories.collectAsState()
|
||||
val frequencies by LookupsRepository.taskFrequencies.collectAsState()
|
||||
val priorities by LookupsRepository.taskPriorities.collectAsState()
|
||||
val statuses by LookupsRepository.taskStatuses.collectAsState()
|
||||
|
||||
// Validation errors
|
||||
var titleError by remember { mutableStateOf("") }
|
||||
var dueDateError by remember { mutableStateOf("") }
|
||||
|
||||
// Handle update state changes
|
||||
LaunchedEffect(updateTaskState) {
|
||||
when (updateTaskState) {
|
||||
is ApiResult.Success -> {
|
||||
viewModel.resetUpdateTaskState()
|
||||
onTaskUpdated()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
fun validateForm(): Boolean {
|
||||
var isValid = true
|
||||
|
||||
if (title.isBlank()) {
|
||||
titleError = "Title is required"
|
||||
isValid = false
|
||||
} else {
|
||||
titleError = ""
|
||||
}
|
||||
|
||||
if (dueDate.isBlank()) {
|
||||
dueDateError = "Due date is required"
|
||||
isValid = false
|
||||
} else {
|
||||
dueDateError = ""
|
||||
}
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Edit Task") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Required fields section
|
||||
Text(
|
||||
text = "Task Details",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = title,
|
||||
onValueChange = { title = it },
|
||||
label = { Text("Title *") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isError = titleError.isNotEmpty(),
|
||||
supportingText = if (titleError.isNotEmpty()) {
|
||||
{ Text(titleError) }
|
||||
} else null
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
label = { Text("Description") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = 3,
|
||||
maxLines = 5
|
||||
)
|
||||
|
||||
// Category dropdown
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = categoryExpanded,
|
||||
onExpandedChange = { categoryExpanded = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = selectedCategory?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Category *") },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = categoryExpanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
enabled = categories.isNotEmpty()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = categoryExpanded,
|
||||
onDismissRequest = { categoryExpanded = false }
|
||||
) {
|
||||
categories.forEach { category ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(category.name.replaceFirstChar { it.uppercase() }) },
|
||||
onClick = {
|
||||
selectedCategory = category
|
||||
categoryExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Frequency dropdown
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = frequencyExpanded,
|
||||
onExpandedChange = { frequencyExpanded = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = selectedFrequency?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Frequency *") },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = frequencyExpanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
enabled = frequencies.isNotEmpty()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = frequencyExpanded,
|
||||
onDismissRequest = { frequencyExpanded = false }
|
||||
) {
|
||||
frequencies.forEach { frequency ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(frequency.name.replaceFirstChar { it.uppercase() }) },
|
||||
onClick = {
|
||||
selectedFrequency = frequency
|
||||
frequencyExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority dropdown
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = priorityExpanded,
|
||||
onExpandedChange = { priorityExpanded = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = selectedPriority?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Priority *") },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = priorityExpanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
enabled = priorities.isNotEmpty()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = priorityExpanded,
|
||||
onDismissRequest = { priorityExpanded = false }
|
||||
) {
|
||||
priorities.forEach { priority ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(priority.name.replaceFirstChar { it.uppercase() }) },
|
||||
onClick = {
|
||||
selectedPriority = priority
|
||||
priorityExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Status dropdown
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = statusExpanded,
|
||||
onExpandedChange = { statusExpanded = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = selectedStatus?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Status *") },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = statusExpanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
enabled = statuses.isNotEmpty()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = statusExpanded,
|
||||
onDismissRequest = { statusExpanded = false }
|
||||
) {
|
||||
statuses.forEach { status ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(status.name.replaceFirstChar { it.uppercase() }) },
|
||||
onClick = {
|
||||
selectedStatus = status
|
||||
statusExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = dueDate,
|
||||
onValueChange = { dueDate = it },
|
||||
label = { Text("Due Date (YYYY-MM-DD) *") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isError = dueDateError.isNotEmpty(),
|
||||
supportingText = if (dueDateError.isNotEmpty()) {
|
||||
{ Text(dueDateError) }
|
||||
} else null,
|
||||
placeholder = { Text("2025-01-31") }
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = estimatedCost,
|
||||
onValueChange = { estimatedCost = it },
|
||||
label = { Text("Estimated Cost") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
prefix = { Text("$") }
|
||||
)
|
||||
|
||||
// Error message
|
||||
if (updateTaskState is ApiResult.Error) {
|
||||
Text(
|
||||
text = (updateTaskState as ApiResult.Error).message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
|
||||
// Submit button
|
||||
Button(
|
||||
onClick = {
|
||||
if (validateForm() && selectedCategory != null &&
|
||||
selectedFrequency != null && selectedPriority != null &&
|
||||
selectedStatus != null) {
|
||||
viewModel.updateTask(
|
||||
taskId = task.id,
|
||||
request = TaskCreateRequest(
|
||||
residence = task.residence,
|
||||
title = title,
|
||||
description = description.ifBlank { null },
|
||||
category = selectedCategory!!.id,
|
||||
frequency = selectedFrequency!!.id,
|
||||
priority = selectedPriority!!.id,
|
||||
status = selectedStatus!!.id,
|
||||
dueDate = dueDate,
|
||||
estimatedCost = estimatedCost.ifBlank { null }
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = validateForm() && selectedCategory != null &&
|
||||
selectedFrequency != null && selectedPriority != null &&
|
||||
selectedStatus != null
|
||||
) {
|
||||
if (updateTaskState is ApiResult.Loading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
} else {
|
||||
Text("Update Task")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ fun ResidenceDetailScreen(
|
||||
residenceId: Int,
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToEditResidence: (Residence) -> Unit,
|
||||
onNavigateToEditTask: (TaskDetail) -> Unit,
|
||||
residenceViewModel: ResidenceViewModel = viewModel { ResidenceViewModel() },
|
||||
taskCompletionViewModel: TaskCompletionViewModel = viewModel { TaskCompletionViewModel() },
|
||||
taskViewModel: TaskViewModel = viewModel { TaskViewModel() }
|
||||
@@ -36,10 +37,13 @@ fun ResidenceDetailScreen(
|
||||
val tasksState by residenceViewModel.residenceTasksState.collectAsState()
|
||||
val completionState by taskCompletionViewModel.createCompletionState.collectAsState()
|
||||
val taskAddNewTaskState by taskViewModel.taskAddNewCustomTaskState.collectAsState()
|
||||
val cancelTaskState by residenceViewModel.cancelTaskState.collectAsState()
|
||||
val uncancelTaskState by residenceViewModel.uncancelTaskState.collectAsState()
|
||||
|
||||
var showCompleteDialog by remember { mutableStateOf(false) }
|
||||
var selectedTask by remember { mutableStateOf<TaskDetail?>(null) }
|
||||
var showNewTaskDialog by remember { mutableStateOf(false) }
|
||||
var showCancelledTasks by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(residenceId) {
|
||||
residenceViewModel.getResidence(residenceId) { result ->
|
||||
@@ -72,6 +76,28 @@ fun ResidenceDetailScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Handle cancel task success
|
||||
LaunchedEffect(cancelTaskState) {
|
||||
when (cancelTaskState) {
|
||||
is ApiResult.Success -> {
|
||||
residenceViewModel.resetCancelTaskState()
|
||||
residenceViewModel.loadResidenceTasks(residenceId)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle uncancel task success
|
||||
LaunchedEffect(uncancelTaskState) {
|
||||
when (uncancelTaskState) {
|
||||
is ApiResult.Success -> {
|
||||
residenceViewModel.resetUncancelTaskState()
|
||||
residenceViewModel.loadResidenceTasks(residenceId)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
if (showCompleteDialog && selectedTask != null) {
|
||||
CompleteTaskDialog(
|
||||
taskId = selectedTask!!.id,
|
||||
@@ -362,7 +388,7 @@ fun ResidenceDetailScreen(
|
||||
}
|
||||
is ApiResult.Success -> {
|
||||
val taskData = (tasksState as ApiResult.Success).data
|
||||
if (taskData.tasks.isEmpty()) {
|
||||
if (taskData.tasks.isEmpty() && taskData.cancelledTasks.isEmpty()) {
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -401,9 +427,65 @@ fun ResidenceDetailScreen(
|
||||
onCompleteClick = {
|
||||
selectedTask = task
|
||||
showCompleteDialog = true
|
||||
}
|
||||
},
|
||||
onEditClick = {
|
||||
onNavigateToEditTask(task)
|
||||
},
|
||||
onCancelClick = {
|
||||
residenceViewModel.cancelTask(task.id)
|
||||
},
|
||||
onUncancelClick = null
|
||||
)
|
||||
}
|
||||
|
||||
// Cancelled tasks section
|
||||
if (taskData.cancelledTasks.isNotEmpty()) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
Icons.Default.Cancel,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Cancelled Tasks (${taskData.cancelledTasks.size})",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
TextButton(onClick = { showCancelledTasks = !showCancelledTasks }) {
|
||||
Text(if (showCancelledTasks) "Hide" else "Show")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showCancelledTasks) {
|
||||
items(taskData.cancelledTasks) { task ->
|
||||
TaskCard(
|
||||
task = task,
|
||||
onCompleteClick = null,
|
||||
onEditClick = {
|
||||
onNavigateToEditTask(task)
|
||||
},
|
||||
onCancelClick = null,
|
||||
onUncancelClick = {
|
||||
residenceViewModel.uncancelTask(task.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,7 +597,10 @@ private fun DetailRow(
|
||||
@Composable
|
||||
fun TaskCard(
|
||||
task: TaskDetail,
|
||||
onCompleteClick: () -> Unit
|
||||
onCompleteClick: (() -> Unit)?,
|
||||
onEditClick: () -> Unit,
|
||||
onCancelClick: (() -> Unit)?,
|
||||
onUncancelClick: (() -> Unit)?
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -733,7 +818,7 @@ fun TaskCard(
|
||||
}
|
||||
|
||||
// Show complete task button based on API logic
|
||||
if (task.showCompletedButton) {
|
||||
if (task.showCompletedButton && onCompleteClick != null) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(
|
||||
onClick = onCompleteClick,
|
||||
@@ -753,6 +838,68 @@ fun TaskCard(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Action buttons row
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Edit button
|
||||
OutlinedButton(
|
||||
onClick = onEditClick,
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Edit,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("Edit")
|
||||
}
|
||||
|
||||
// Cancel or Uncancel button
|
||||
when {
|
||||
onCancelClick != null -> {
|
||||
OutlinedButton(
|
||||
onClick = onCancelClick,
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Cancel,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
onUncancelClick != null -> {
|
||||
Button(
|
||||
onClick = onUncancelClick,
|
||||
modifier = Modifier.weight(1f),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Undo,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("Restore")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,15 @@ class ResidenceViewModel : ViewModel() {
|
||||
private val _myResidencesState = MutableStateFlow<ApiResult<MyResidencesResponse>>(ApiResult.Loading)
|
||||
val myResidencesState: StateFlow<ApiResult<MyResidencesResponse>> = _myResidencesState
|
||||
|
||||
private val _cancelTaskState = MutableStateFlow<ApiResult<com.mycrib.shared.models.TaskCancelResponse>>(ApiResult.Loading)
|
||||
val cancelTaskState: StateFlow<ApiResult<com.mycrib.shared.models.TaskCancelResponse>> = _cancelTaskState
|
||||
|
||||
private val _uncancelTaskState = MutableStateFlow<ApiResult<com.mycrib.shared.models.TaskCancelResponse>>(ApiResult.Loading)
|
||||
val uncancelTaskState: StateFlow<ApiResult<com.mycrib.shared.models.TaskCancelResponse>> = _uncancelTaskState
|
||||
|
||||
private val _updateTaskState = MutableStateFlow<ApiResult<com.mycrib.shared.models.CustomTask>>(ApiResult.Loading)
|
||||
val updateTaskState: StateFlow<ApiResult<com.mycrib.shared.models.CustomTask>> = _updateTaskState
|
||||
|
||||
fun loadResidences() {
|
||||
viewModelScope.launch {
|
||||
_residencesState.value = ApiResult.Loading
|
||||
@@ -132,4 +141,52 @@ class ResidenceViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelTask(taskId: Int) {
|
||||
viewModelScope.launch {
|
||||
_cancelTaskState.value = ApiResult.Loading
|
||||
val token = TokenStorage.getToken()
|
||||
if (token != null) {
|
||||
_cancelTaskState.value = taskApi.cancelTask(token, taskId)
|
||||
} else {
|
||||
_cancelTaskState.value = ApiResult.Error("Not authenticated", 401)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun uncancelTask(taskId: Int) {
|
||||
viewModelScope.launch {
|
||||
_uncancelTaskState.value = ApiResult.Loading
|
||||
val token = TokenStorage.getToken()
|
||||
if (token != null) {
|
||||
_uncancelTaskState.value = taskApi.uncancelTask(token, taskId)
|
||||
} else {
|
||||
_uncancelTaskState.value = ApiResult.Error("Not authenticated", 401)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTask(taskId: Int, request: com.mycrib.shared.models.TaskCreateRequest) {
|
||||
viewModelScope.launch {
|
||||
_updateTaskState.value = ApiResult.Loading
|
||||
val token = TokenStorage.getToken()
|
||||
if (token != null) {
|
||||
_updateTaskState.value = taskApi.updateTask(token, taskId, request)
|
||||
} else {
|
||||
_updateTaskState.value = ApiResult.Error("Not authenticated", 401)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetCancelTaskState() {
|
||||
_cancelTaskState.value = ApiResult.Loading
|
||||
}
|
||||
|
||||
fun resetUncancelTaskState() {
|
||||
_uncancelTaskState.value = ApiResult.Loading
|
||||
}
|
||||
|
||||
fun resetUpdateTaskState() {
|
||||
_updateTaskState.value = ApiResult.Loading
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user