This commit is contained in:
Trey t
2025-11-05 15:15:59 -06:00
parent 5deac95818
commit 1d48a9bff1
13 changed files with 1360 additions and 871 deletions

View File

@@ -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))
}
}
}

View File

@@ -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")
}
}
}
}
}
}
}