wip
This commit is contained in:
@@ -44,6 +44,7 @@ data class TaskCreateRequest(
|
|||||||
val frequency: Int,
|
val frequency: Int,
|
||||||
@SerialName("interval_days") val intervalDays: Int? = null,
|
@SerialName("interval_days") val intervalDays: Int? = null,
|
||||||
val priority: Int,
|
val priority: Int,
|
||||||
|
val status: Int? = null,
|
||||||
@SerialName("due_date") val dueDate: String,
|
@SerialName("due_date") val dueDate: String,
|
||||||
@SerialName("estimated_cost") val estimatedCost: String? = null
|
@SerialName("estimated_cost") val estimatedCost: String? = null
|
||||||
)
|
)
|
||||||
@@ -106,3 +107,21 @@ data class TaskCancelResponse(
|
|||||||
val message: String,
|
val message: String,
|
||||||
val task: TaskDetail
|
val task: TaskDetail
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TaskColumn(
|
||||||
|
val name: String,
|
||||||
|
@SerialName("display_name") val displayName: String,
|
||||||
|
@SerialName("button_types") val buttonTypes: List<String>,
|
||||||
|
val icons: Map<String, String>,
|
||||||
|
val color: String,
|
||||||
|
val tasks: List<TaskDetail>,
|
||||||
|
val count: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TaskColumnsResponse(
|
||||||
|
val columns: List<TaskColumn>,
|
||||||
|
@SerialName("days_threshold") val daysThreshold: Int? = null,
|
||||||
|
@SerialName("residence_id") val residenceId: String? = null
|
||||||
|
)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
|||||||
suspend fun getTasks(
|
suspend fun getTasks(
|
||||||
token: String,
|
token: String,
|
||||||
days: Int = 30
|
days: Int = 30
|
||||||
): ApiResult<AllTasksResponse> {
|
): ApiResult<TaskColumnsResponse> {
|
||||||
return try {
|
return try {
|
||||||
val response = client.get("$baseUrl/tasks/") {
|
val response = client.get("$baseUrl/tasks/") {
|
||||||
header("Authorization", "Token $token")
|
header("Authorization", "Token $token")
|
||||||
@@ -101,7 +101,7 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
|||||||
token: String,
|
token: String,
|
||||||
residenceId: Int,
|
residenceId: Int,
|
||||||
days: Int = 30
|
days: Int = 30
|
||||||
): ApiResult<TasksByResidenceResponse> {
|
): ApiResult<TaskColumnsResponse> {
|
||||||
return try {
|
return try {
|
||||||
val response = client.get("$baseUrl/tasks/by-residence/$residenceId/") {
|
val response = client.get("$baseUrl/tasks/by-residence/$residenceId/") {
|
||||||
header("Authorization", "Token $token")
|
header("Authorization", "Token $token")
|
||||||
|
|||||||
@@ -273,6 +273,7 @@ fun AddNewTaskDialog(
|
|||||||
frequency = frequency.id,
|
frequency = frequency.id,
|
||||||
intervalDays = intervalDays.toIntOrNull(),
|
intervalDays = intervalDays.toIntOrNull(),
|
||||||
priority = priority.id,
|
priority = priority.id,
|
||||||
|
status = null,
|
||||||
dueDate = dueDate,
|
dueDate = dueDate,
|
||||||
estimatedCost = estimatedCost.ifBlank { null }
|
estimatedCost = estimatedCost.ifBlank { null }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ fun AddNewTaskWithResidenceDialog(
|
|||||||
frequency = frequency.id,
|
frequency = frequency.id,
|
||||||
intervalDays = intervalDays.toIntOrNull(),
|
intervalDays = intervalDays.toIntOrNull(),
|
||||||
priority = priority.id,
|
priority = priority.id,
|
||||||
|
status = null,
|
||||||
dueDate = dueDate,
|
dueDate = dueDate,
|
||||||
estimatedCost = estimatedCost.ifBlank { null }
|
estimatedCost = estimatedCost.ifBlank { null }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
package com.mycrib.android.ui.components.task
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.mycrib.android.viewmodel.TaskViewModel
|
||||||
|
|
||||||
|
// MARK: - Edit Task Button
|
||||||
|
@Composable
|
||||||
|
fun EditTaskButton(
|
||||||
|
taskId: Int,
|
||||||
|
onCompletion: () -> Unit,
|
||||||
|
onError: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
// Edit navigates to edit screen - handled by parent
|
||||||
|
onCompletion()
|
||||||
|
},
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Edit,
|
||||||
|
contentDescription = "Edit",
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("Edit", style = MaterialTheme.typography.labelLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Cancel Task Button
|
||||||
|
@Composable
|
||||||
|
fun CancelTaskButton(
|
||||||
|
taskId: Int,
|
||||||
|
onCompletion: () -> Unit,
|
||||||
|
onError: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: TaskViewModel = viewModel { TaskViewModel() }
|
||||||
|
) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.cancelTask(taskId) { success ->
|
||||||
|
if (success) {
|
||||||
|
onCompletion()
|
||||||
|
} else {
|
||||||
|
onError("Failed to cancel task")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Cancel,
|
||||||
|
contentDescription = "Cancel",
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("Cancel", style = MaterialTheme.typography.labelLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Uncancel (Restore) Task Button
|
||||||
|
@Composable
|
||||||
|
fun UncancelTaskButton(
|
||||||
|
taskId: Int,
|
||||||
|
onCompletion: () -> Unit,
|
||||||
|
onError: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: TaskViewModel = viewModel { TaskViewModel() }
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
viewModel.uncancelTask(taskId) { success ->
|
||||||
|
if (success) {
|
||||||
|
onCompletion()
|
||||||
|
} else {
|
||||||
|
onError("Failed to restore task")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Undo,
|
||||||
|
contentDescription = "Restore",
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("Restore", style = MaterialTheme.typography.labelLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Mark In Progress Button
|
||||||
|
@Composable
|
||||||
|
fun MarkInProgressButton(
|
||||||
|
taskId: Int,
|
||||||
|
onCompletion: () -> Unit,
|
||||||
|
onError: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: TaskViewModel = viewModel { TaskViewModel() }
|
||||||
|
) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.markInProgress(taskId) { success ->
|
||||||
|
if (success) {
|
||||||
|
onCompletion()
|
||||||
|
} else {
|
||||||
|
onError("Failed to mark task in progress")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = MaterialTheme.colorScheme.tertiary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PlayCircle,
|
||||||
|
contentDescription = "Mark In Progress",
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("In Progress", style = MaterialTheme.typography.labelLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Complete Task Button
|
||||||
|
@Composable
|
||||||
|
fun CompleteTaskButton(
|
||||||
|
taskId: Int,
|
||||||
|
onCompletion: () -> Unit,
|
||||||
|
onError: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
// Complete shows dialog - handled by parent
|
||||||
|
onCompletion()
|
||||||
|
},
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.CheckCircle,
|
||||||
|
contentDescription = "Complete",
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("Complete", style = MaterialTheme.typography.labelLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Archive Task Button
|
||||||
|
@Composable
|
||||||
|
fun ArchiveTaskButton(
|
||||||
|
taskId: Int,
|
||||||
|
onCompletion: () -> Unit,
|
||||||
|
onError: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: TaskViewModel = viewModel { TaskViewModel() }
|
||||||
|
) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.archiveTask(taskId) { success ->
|
||||||
|
if (success) {
|
||||||
|
onCompletion()
|
||||||
|
} else {
|
||||||
|
onError("Failed to archive task")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Archive,
|
||||||
|
contentDescription = "Archive",
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("Archive", style = MaterialTheme.typography.labelLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Unarchive Task Button
|
||||||
|
@Composable
|
||||||
|
fun UnarchiveTaskButton(
|
||||||
|
taskId: Int,
|
||||||
|
onCompletion: () -> Unit,
|
||||||
|
onError: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: TaskViewModel = viewModel { TaskViewModel() }
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
viewModel.unarchiveTask(taskId) { success ->
|
||||||
|
if (success) {
|
||||||
|
onCompletion()
|
||||||
|
} else {
|
||||||
|
onError("Failed to unarchive task")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Unarchive,
|
||||||
|
contentDescription = "Unarchive",
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("Unarchive", style = MaterialTheme.typography.labelLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import org.jetbrains.compose.ui.tooling.preview.Preview
|
|||||||
@Composable
|
@Composable
|
||||||
fun TaskCard(
|
fun TaskCard(
|
||||||
task: TaskDetail,
|
task: TaskDetail,
|
||||||
|
buttonTypes: List<String> = emptyList(),
|
||||||
onCompleteClick: (() -> Unit)?,
|
onCompleteClick: (() -> Unit)?,
|
||||||
onEditClick: () -> Unit,
|
onEditClick: () -> Unit,
|
||||||
onCancelClick: (() -> Unit)?,
|
onCancelClick: (() -> Unit)?,
|
||||||
@@ -243,161 +244,87 @@ fun TaskCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show complete task button and mark in progress button
|
// Render buttons based on buttonTypes array
|
||||||
if ((task.showCompletedButton && onCompleteClick != null) || (onMarkInProgressClick != null && task.status?.name != "in_progress")) {
|
if (buttonTypes.isNotEmpty()) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
// Mark In Progress button
|
|
||||||
if (onMarkInProgressClick != null && task.status?.name != "in_progress") {
|
|
||||||
Button(
|
|
||||||
onClick = onMarkInProgressClick,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.tertiary
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.PlayArrow,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(18.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
"In Progress",
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
fontWeight = FontWeight.SemiBold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complete Task button
|
buttonTypes.forEach { buttonType ->
|
||||||
if (task.showCompletedButton && onCompleteClick != null) {
|
when (buttonType) {
|
||||||
Button(
|
"mark_in_progress" -> {
|
||||||
onClick = onCompleteClick,
|
onMarkInProgressClick?.let {
|
||||||
modifier = Modifier.weight(1f),
|
MarkInProgressButton(
|
||||||
shape = RoundedCornerShape(12.dp)
|
taskId = task.id,
|
||||||
) {
|
onCompletion = it,
|
||||||
Icon(
|
onError = { error -> println("Error: $error") },
|
||||||
Icons.Default.CheckCircle,
|
modifier = Modifier.fillMaxWidth()
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(18.dp)
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text(
|
|
||||||
"Complete",
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
fontWeight = FontWeight.SemiBold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Archive/Unarchive button row
|
|
||||||
if (task.archived) {
|
|
||||||
if (onUnarchiveClick != null) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
OutlinedButton(
|
|
||||||
onClick = onUnarchiveClick,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
colors = ButtonDefaults.outlinedButtonColors(
|
|
||||||
contentColor = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Unarchive,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(18.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
Text("Unarchive")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
"complete" -> {
|
||||||
if (onArchiveClick != null) {
|
onCompleteClick?.let {
|
||||||
|
CompleteTaskButton(
|
||||||
|
taskId = task.id,
|
||||||
|
onCompletion = it,
|
||||||
|
onError = { error -> println("Error: $error") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
OutlinedButton(
|
}
|
||||||
onClick = onArchiveClick,
|
}
|
||||||
modifier = Modifier.fillMaxWidth(),
|
"edit" -> {
|
||||||
shape = RoundedCornerShape(12.dp),
|
EditTaskButton(
|
||||||
colors = ButtonDefaults.outlinedButtonColors(
|
taskId = task.id,
|
||||||
contentColor = MaterialTheme.colorScheme.outline
|
onCompletion = onEditClick,
|
||||||
|
onError = { error -> println("Error: $error") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
) {
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Icon(
|
}
|
||||||
Icons.Default.Archive,
|
"cancel" -> {
|
||||||
contentDescription = null,
|
onCancelClick?.let {
|
||||||
modifier = Modifier.size(18.dp)
|
CancelTaskButton(
|
||||||
|
taskId = task.id,
|
||||||
|
onCompletion = it,
|
||||||
|
onError = { error -> println("Error: $error") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text("Archive")
|
}
|
||||||
|
}
|
||||||
|
"uncancel" -> {
|
||||||
|
onUncancelClick?.let {
|
||||||
|
UncancelTaskButton(
|
||||||
|
taskId = task.id,
|
||||||
|
onCompletion = it,
|
||||||
|
onError = { error -> println("Error: $error") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"archive" -> {
|
||||||
|
onArchiveClick?.let {
|
||||||
|
ArchiveTaskButton(
|
||||||
|
taskId = task.id,
|
||||||
|
onCompletion = it,
|
||||||
|
onError = { error -> println("Error: $error") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"unarchive" -> {
|
||||||
|
onUnarchiveClick?.let {
|
||||||
|
UnarchiveTaskButton(
|
||||||
|
taskId = task.id,
|
||||||
|
onCompletion = it,
|
||||||
|
onError = { error -> println("Error: $error") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.mycrib.shared.models.AllTasksResponse
|
import com.mycrib.shared.models.TaskColumn
|
||||||
import com.mycrib.shared.models.TaskDetail
|
import com.mycrib.shared.models.TaskDetail
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@@ -227,3 +227,210 @@ private fun TaskColumn(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic Task Kanban View that creates columns based on API response
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun DynamicTaskKanbanView(
|
||||||
|
columns: List<TaskColumn>,
|
||||||
|
onCompleteTask: (TaskDetail) -> Unit,
|
||||||
|
onEditTask: (TaskDetail) -> Unit,
|
||||||
|
onCancelTask: ((TaskDetail) -> Unit)?,
|
||||||
|
onUncancelTask: ((TaskDetail) -> Unit)?,
|
||||||
|
onMarkInProgress: ((TaskDetail) -> Unit)?,
|
||||||
|
onArchiveTask: ((TaskDetail) -> Unit)?,
|
||||||
|
onUnarchiveTask: ((TaskDetail) -> Unit)?
|
||||||
|
) {
|
||||||
|
val pagerState = rememberPagerState(pageCount = { columns.size })
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
pageSpacing = 16.dp,
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp)
|
||||||
|
) { page ->
|
||||||
|
val column = columns[page]
|
||||||
|
DynamicTaskColumn(
|
||||||
|
column = column,
|
||||||
|
onCompleteTask = onCompleteTask,
|
||||||
|
onEditTask = onEditTask,
|
||||||
|
onCancelTask = onCancelTask,
|
||||||
|
onUncancelTask = onUncancelTask,
|
||||||
|
onMarkInProgress = onMarkInProgress,
|
||||||
|
onArchiveTask = onArchiveTask,
|
||||||
|
onUnarchiveTask = onUnarchiveTask
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic Task Column that adapts based on column configuration
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun DynamicTaskColumn(
|
||||||
|
column: TaskColumn,
|
||||||
|
onCompleteTask: (TaskDetail) -> Unit,
|
||||||
|
onEditTask: (TaskDetail) -> Unit,
|
||||||
|
onCancelTask: ((TaskDetail) -> Unit)?,
|
||||||
|
onUncancelTask: ((TaskDetail) -> Unit)?,
|
||||||
|
onMarkInProgress: ((TaskDetail) -> Unit)?,
|
||||||
|
onArchiveTask: ((TaskDetail) -> Unit)?,
|
||||||
|
onUnarchiveTask: ((TaskDetail) -> Unit)?
|
||||||
|
) {
|
||||||
|
// Get icon from API response, with fallback
|
||||||
|
val columnIcon = getIconFromName(column.icons["android"] ?: "List")
|
||||||
|
|
||||||
|
val columnColor = hexToColor(column.color)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.surface,
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = columnIcon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = columnColor,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = column.displayName,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = columnColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
color = columnColor,
|
||||||
|
shape = CircleShape
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = column.count.toString(),
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.surface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tasks List
|
||||||
|
if (column.tasks.isEmpty()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(32.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = columnIcon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = columnColor.copy(alpha = 0.3f),
|
||||||
|
modifier = Modifier.size(48.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "No tasks",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
items(column.tasks, key = { it.id }) { task ->
|
||||||
|
// Use existing TaskCard component with buttonTypes array
|
||||||
|
TaskCard(
|
||||||
|
task = task,
|
||||||
|
buttonTypes = column.buttonTypes,
|
||||||
|
onCompleteClick = { onCompleteTask(task) },
|
||||||
|
onEditClick = { onEditTask(task) },
|
||||||
|
onCancelClick = onCancelTask?.let { { it(task) } },
|
||||||
|
onUncancelClick = onUncancelTask?.let { { it(task) } },
|
||||||
|
onMarkInProgressClick = onMarkInProgress?.let { { it(task) } },
|
||||||
|
onArchiveClick = onArchiveTask?.let { { it(task) } },
|
||||||
|
onUnarchiveClick = onUnarchiveTask?.let { { it(task) } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to convert icon name string to ImageVector
|
||||||
|
*/
|
||||||
|
private fun getIconFromName(iconName: String): ImageVector {
|
||||||
|
return when (iconName) {
|
||||||
|
"CalendarToday" -> Icons.Default.CalendarToday
|
||||||
|
"PlayCircle" -> Icons.Default.PlayCircle
|
||||||
|
"CheckCircle" -> Icons.Default.CheckCircle
|
||||||
|
"Archive" -> Icons.Default.Archive
|
||||||
|
"List" -> Icons.Default.List
|
||||||
|
"PlayArrow" -> Icons.Default.PlayArrow
|
||||||
|
"Unarchive" -> Icons.Default.Unarchive
|
||||||
|
else -> Icons.Default.List // Default fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to convert hex color string to Color
|
||||||
|
* Supports formats: #RGB, #RRGGBB, #AARRGGBB
|
||||||
|
* Platform-independent implementation
|
||||||
|
*/
|
||||||
|
private fun hexToColor(hex: String): Color {
|
||||||
|
val cleanHex = hex.removePrefix("#")
|
||||||
|
return try {
|
||||||
|
when (cleanHex.length) {
|
||||||
|
3 -> {
|
||||||
|
// RGB format - expand to RRGGBB
|
||||||
|
val r = cleanHex[0].toString().repeat(2).toInt(16)
|
||||||
|
val g = cleanHex[1].toString().repeat(2).toInt(16)
|
||||||
|
val b = cleanHex[2].toString().repeat(2).toInt(16)
|
||||||
|
Color(red = r / 255f, green = g / 255f, blue = b / 255f)
|
||||||
|
}
|
||||||
|
6 -> {
|
||||||
|
// RRGGBB format
|
||||||
|
val r = cleanHex.substring(0, 2).toInt(16)
|
||||||
|
val g = cleanHex.substring(2, 4).toInt(16)
|
||||||
|
val b = cleanHex.substring(4, 6).toInt(16)
|
||||||
|
Color(red = r / 255f, green = g / 255f, blue = b / 255f)
|
||||||
|
}
|
||||||
|
8 -> {
|
||||||
|
// AARRGGBB format
|
||||||
|
val a = cleanHex.substring(0, 2).toInt(16)
|
||||||
|
val r = cleanHex.substring(2, 4).toInt(16)
|
||||||
|
val g = cleanHex.substring(4, 6).toInt(16)
|
||||||
|
val b = cleanHex.substring(6, 8).toInt(16)
|
||||||
|
Color(red = r / 255f, green = g / 255f, blue = b / 255f, alpha = a / 255f)
|
||||||
|
}
|
||||||
|
else -> Color.Gray // Default fallback
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Color.Gray // Fallback on parse error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import com.mycrib.android.ui.components.AddNewTaskWithResidenceDialog
|
import com.mycrib.android.ui.components.AddNewTaskWithResidenceDialog
|
||||||
import com.mycrib.android.ui.components.CompleteTaskDialog
|
import com.mycrib.android.ui.components.CompleteTaskDialog
|
||||||
import com.mycrib.android.ui.components.task.TaskCard
|
import com.mycrib.android.ui.components.task.TaskCard
|
||||||
import com.mycrib.android.ui.components.task.TaskKanbanView
|
import com.mycrib.android.ui.components.task.DynamicTaskKanbanView
|
||||||
import com.mycrib.android.viewmodel.ResidenceViewModel
|
import com.mycrib.android.viewmodel.ResidenceViewModel
|
||||||
import com.mycrib.android.viewmodel.TaskCompletionViewModel
|
import com.mycrib.android.viewmodel.TaskCompletionViewModel
|
||||||
import com.mycrib.android.viewmodel.TaskViewModel
|
import com.mycrib.android.viewmodel.TaskViewModel
|
||||||
@@ -149,10 +149,7 @@ fun AllTasksScreen(
|
|||||||
}
|
}
|
||||||
is ApiResult.Success -> {
|
is ApiResult.Success -> {
|
||||||
val taskData = (tasksState as ApiResult.Success).data
|
val taskData = (tasksState as ApiResult.Success).data
|
||||||
val hasNoTasks = taskData.upcomingTasks.isEmpty() &&
|
val hasNoTasks = taskData.columns.all { it.tasks.isEmpty() }
|
||||||
taskData.inProgressTasks.isEmpty() &&
|
|
||||||
taskData.doneTasks.isEmpty() &&
|
|
||||||
taskData.archivedTasks.isEmpty()
|
|
||||||
|
|
||||||
if (hasNoTasks) {
|
if (hasNoTasks) {
|
||||||
Box(
|
Box(
|
||||||
@@ -219,11 +216,8 @@ fun AllTasksScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
TaskKanbanView(
|
DynamicTaskKanbanView(
|
||||||
upcomingTasks = taskData.upcomingTasks,
|
columns = taskData.columns,
|
||||||
inProgressTasks = taskData.inProgressTasks,
|
|
||||||
doneTasks = taskData.doneTasks,
|
|
||||||
archivedTasks = taskData.archivedTasks,
|
|
||||||
onCompleteTask = { task ->
|
onCompleteTask = { task ->
|
||||||
selectedTask = task
|
selectedTask = task
|
||||||
showCompleteDialog = true
|
showCompleteDialog = true
|
||||||
|
|||||||
@@ -300,6 +300,7 @@ fun EditTaskScreen(
|
|||||||
category = selectedCategory!!.id,
|
category = selectedCategory!!.id,
|
||||||
frequency = selectedFrequency!!.id,
|
frequency = selectedFrequency!!.id,
|
||||||
priority = selectedPriority!!.id,
|
priority = selectedPriority!!.id,
|
||||||
|
status = selectedStatus!!.id,
|
||||||
dueDate = dueDate,
|
dueDate = dueDate,
|
||||||
estimatedCost = estimatedCost.ifBlank { null }
|
estimatedCost = estimatedCost.ifBlank { null }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import com.mycrib.android.ui.components.common.InfoCard
|
|||||||
import com.mycrib.android.ui.components.residence.PropertyDetailItem
|
import com.mycrib.android.ui.components.residence.PropertyDetailItem
|
||||||
import com.mycrib.android.ui.components.residence.DetailRow
|
import com.mycrib.android.ui.components.residence.DetailRow
|
||||||
import com.mycrib.android.ui.components.task.TaskCard
|
import com.mycrib.android.ui.components.task.TaskCard
|
||||||
import com.mycrib.android.ui.components.task.TaskKanbanView
|
import com.mycrib.android.ui.components.task.DynamicTaskKanbanView
|
||||||
import com.mycrib.android.viewmodel.ResidenceViewModel
|
import com.mycrib.android.viewmodel.ResidenceViewModel
|
||||||
import com.mycrib.android.viewmodel.TaskCompletionViewModel
|
import com.mycrib.android.viewmodel.TaskCompletionViewModel
|
||||||
import com.mycrib.android.viewmodel.TaskViewModel
|
import com.mycrib.android.viewmodel.TaskViewModel
|
||||||
@@ -393,7 +393,8 @@ fun ResidenceDetailScreen(
|
|||||||
}
|
}
|
||||||
is ApiResult.Success -> {
|
is ApiResult.Success -> {
|
||||||
val taskData = (tasksState as ApiResult.Success).data
|
val taskData = (tasksState as ApiResult.Success).data
|
||||||
if (taskData.upcomingTasks.isEmpty() && taskData.inProgressTasks.isEmpty() && taskData.doneTasks.isEmpty() && taskData.archivedTasks.isEmpty()) {
|
val allTasksEmpty = taskData.columns.all { it.tasks.isEmpty() }
|
||||||
|
if (allTasksEmpty) {
|
||||||
item {
|
item {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -432,11 +433,8 @@ fun ResidenceDetailScreen(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(500.dp)
|
.height(500.dp)
|
||||||
) {
|
) {
|
||||||
TaskKanbanView(
|
DynamicTaskKanbanView(
|
||||||
upcomingTasks = taskData.upcomingTasks,
|
columns = taskData.columns,
|
||||||
inProgressTasks = taskData.inProgressTasks,
|
|
||||||
doneTasks = taskData.doneTasks,
|
|
||||||
archivedTasks = taskData.archivedTasks,
|
|
||||||
onCompleteTask = { task ->
|
onCompleteTask = { task ->
|
||||||
selectedTask = task
|
selectedTask = task
|
||||||
showCompleteDialog = true
|
showCompleteDialog = true
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ fun TasksScreen(
|
|||||||
) {
|
) {
|
||||||
val tasksState by viewModel.tasksState.collectAsState()
|
val tasksState by viewModel.tasksState.collectAsState()
|
||||||
val completionState by taskCompletionViewModel.createCompletionState.collectAsState()
|
val completionState by taskCompletionViewModel.createCompletionState.collectAsState()
|
||||||
var showInProgressTasks by remember { mutableStateOf(false) }
|
var expandedColumns by remember { mutableStateOf(setOf<String>()) }
|
||||||
var showDoneTasks by remember { mutableStateOf(false) }
|
|
||||||
var showCompleteDialog by remember { mutableStateOf(false) }
|
var showCompleteDialog by remember { mutableStateOf(false) }
|
||||||
var selectedTask by remember { mutableStateOf<com.mycrib.shared.models.TaskDetail?>(null) }
|
var selectedTask by remember { mutableStateOf<com.mycrib.shared.models.TaskDetail?>(null) }
|
||||||
|
|
||||||
@@ -93,9 +92,7 @@ fun TasksScreen(
|
|||||||
}
|
}
|
||||||
is ApiResult.Success -> {
|
is ApiResult.Success -> {
|
||||||
val taskData = (tasksState as ApiResult.Success).data
|
val taskData = (tasksState as ApiResult.Success).data
|
||||||
val hasNoTasks = taskData.upcomingTasks.isEmpty() &&
|
val hasNoTasks = taskData.columns.all { it.tasks.isEmpty() }
|
||||||
taskData.inProgressTasks.isEmpty() &&
|
|
||||||
taskData.doneTasks.isEmpty()
|
|
||||||
|
|
||||||
if (hasNoTasks) {
|
if (hasNoTasks) {
|
||||||
Box(
|
Box(
|
||||||
@@ -140,43 +137,37 @@ fun TasksScreen(
|
|||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
// Task summary pills
|
// Task summary pills - dynamically generated from all columns
|
||||||
item {
|
item {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
|
taskData.columns.forEach { column ->
|
||||||
TaskPill(
|
TaskPill(
|
||||||
count = taskData.summary.upcoming,
|
count = column.count,
|
||||||
label = "Upcoming",
|
label = column.displayName,
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = hexToColor(column.color)
|
||||||
)
|
|
||||||
TaskPill(
|
|
||||||
count = taskData.summary.inProgress,
|
|
||||||
label = "In Progress",
|
|
||||||
color = MaterialTheme.colorScheme.tertiary
|
|
||||||
)
|
|
||||||
TaskPill(
|
|
||||||
count = taskData.summary.done,
|
|
||||||
label = "Done",
|
|
||||||
color = MaterialTheme.colorScheme.secondary
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Upcoming tasks header
|
// Dynamically render all columns
|
||||||
if (taskData.upcomingTasks.isNotEmpty()) {
|
taskData.columns.forEachIndexed { index, column ->
|
||||||
|
if (column.tasks.isNotEmpty()) {
|
||||||
|
// First column (index 0) expanded by default, others collapsible
|
||||||
|
if (index == 0) {
|
||||||
|
// First column - always expanded, show tasks directly
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
text = "Upcoming (${taskData.upcomingTasks.size})",
|
text = "${column.displayName} (${column.tasks.size})",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
modifier = Modifier.padding(top = 8.dp)
|
modifier = Modifier.padding(top = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Upcoming tasks
|
items(column.tasks) { task ->
|
||||||
items(taskData.upcomingTasks) { task ->
|
|
||||||
TaskCard(
|
TaskCard(
|
||||||
task = task,
|
task = task,
|
||||||
onCompleteClick = {
|
onCompleteClick = {
|
||||||
@@ -188,13 +179,20 @@ fun TasksScreen(
|
|||||||
onUncancelClick = { }
|
onUncancelClick = { }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Other columns - collapsible
|
||||||
|
val isExpanded = expandedColumns.contains(column.name)
|
||||||
|
|
||||||
// In Progress section (collapsible)
|
|
||||||
if (taskData.inProgressTasks.isNotEmpty()) {
|
|
||||||
item {
|
item {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onClick = { showInProgressTasks = !showInProgressTasks }
|
onClick = {
|
||||||
|
expandedColumns = if (isExpanded) {
|
||||||
|
expandedColumns - column.name
|
||||||
|
} else {
|
||||||
|
expandedColumns + column.name
|
||||||
|
}
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -208,85 +206,38 @@ fun TasksScreen(
|
|||||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.PlayArrow,
|
getIconFromName(column.icons["android"] ?: "List"),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.tertiary
|
tint = hexToColor(column.color)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "In Progress (${taskData.inProgressTasks.size})",
|
text = "${column.displayName} (${column.tasks.size})",
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Icon(
|
Icon(
|
||||||
if (showInProgressTasks) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
|
if (isExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
|
||||||
contentDescription = if (showInProgressTasks) "Collapse" else "Expand"
|
contentDescription = if (isExpanded) "Collapse" else "Expand"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showInProgressTasks) {
|
if (isExpanded) {
|
||||||
items(taskData.inProgressTasks) { task ->
|
items(column.tasks) { task ->
|
||||||
TaskCard(
|
TaskCard(
|
||||||
task = task,
|
task = task,
|
||||||
onCompleteClick = {
|
onCompleteClick = {
|
||||||
selectedTask = task
|
selectedTask = task
|
||||||
showCompleteDialog = true
|
showCompleteDialog = true
|
||||||
},
|
},
|
||||||
onEditClick = { /* TODO */ },
|
onEditClick = { },
|
||||||
onCancelClick = { },
|
onCancelClick = { },
|
||||||
onUncancelClick = { }
|
onUncancelClick = { }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Done section (collapsible)
|
|
||||||
if (taskData.doneTasks.isNotEmpty()) {
|
|
||||||
item {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
onClick = { showDoneTasks = !showDoneTasks }
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.CheckCircle,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.secondary
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Done (${taskData.doneTasks.size})",
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Icon(
|
|
||||||
if (showDoneTasks) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
|
|
||||||
contentDescription = if (showDoneTasks) "Collapse" else "Expand"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showDoneTasks) {
|
|
||||||
items(taskData.doneTasks) { task ->
|
|
||||||
TaskCard(
|
|
||||||
task = task,
|
|
||||||
onCompleteClick = { /* TODO */ },
|
|
||||||
onEditClick = { /* TODO */ },
|
|
||||||
onUncancelClick = {},
|
|
||||||
onCancelClick = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,3 +300,79 @@ private fun TaskPill(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun getColumnColor(columnName: String): androidx.compose.ui.graphics.Color {
|
||||||
|
return when (columnName) {
|
||||||
|
"upcoming_tasks" -> MaterialTheme.colorScheme.primary
|
||||||
|
"in_progress_tasks" -> MaterialTheme.colorScheme.tertiary
|
||||||
|
"done_tasks" -> MaterialTheme.colorScheme.secondary
|
||||||
|
"archived_tasks" -> MaterialTheme.colorScheme.outline
|
||||||
|
else -> MaterialTheme.colorScheme.primary // Default color for unknown columns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun getColumnIcon(columnName: String): androidx.compose.ui.graphics.vector.ImageVector {
|
||||||
|
return when (columnName) {
|
||||||
|
"upcoming_tasks" -> Icons.Default.CalendarToday
|
||||||
|
"in_progress_tasks" -> Icons.Default.PlayArrow
|
||||||
|
"done_tasks" -> Icons.Default.CheckCircle
|
||||||
|
"archived_tasks" -> Icons.Default.Archive
|
||||||
|
else -> Icons.Default.List // Default icon for unknown columns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to convert icon name string to ImageVector
|
||||||
|
*/
|
||||||
|
private fun getIconFromName(iconName: String): androidx.compose.ui.graphics.vector.ImageVector {
|
||||||
|
return when (iconName) {
|
||||||
|
"CalendarToday" -> Icons.Default.CalendarToday
|
||||||
|
"PlayCircle" -> Icons.Default.PlayCircle
|
||||||
|
"PlayArrow" -> Icons.Default.PlayArrow
|
||||||
|
"CheckCircle" -> Icons.Default.CheckCircle
|
||||||
|
"Archive" -> Icons.Default.Archive
|
||||||
|
"List" -> Icons.Default.List
|
||||||
|
"Unarchive" -> Icons.Default.Unarchive
|
||||||
|
else -> Icons.Default.List // Default fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to convert hex color string to Color
|
||||||
|
* Supports formats: #RGB, #RRGGBB, #AARRGGBB
|
||||||
|
* Platform-independent implementation
|
||||||
|
*/
|
||||||
|
private fun hexToColor(hex: String): androidx.compose.ui.graphics.Color {
|
||||||
|
val cleanHex = hex.removePrefix("#")
|
||||||
|
return try {
|
||||||
|
when (cleanHex.length) {
|
||||||
|
3 -> {
|
||||||
|
// RGB format - expand to RRGGBB
|
||||||
|
val r = cleanHex[0].toString().repeat(2).toInt(16)
|
||||||
|
val g = cleanHex[1].toString().repeat(2).toInt(16)
|
||||||
|
val b = cleanHex[2].toString().repeat(2).toInt(16)
|
||||||
|
androidx.compose.ui.graphics.Color(red = r / 255f, green = g / 255f, blue = b / 255f)
|
||||||
|
}
|
||||||
|
6 -> {
|
||||||
|
// RRGGBB format
|
||||||
|
val r = cleanHex.substring(0, 2).toInt(16)
|
||||||
|
val g = cleanHex.substring(2, 4).toInt(16)
|
||||||
|
val b = cleanHex.substring(4, 6).toInt(16)
|
||||||
|
androidx.compose.ui.graphics.Color(red = r / 255f, green = g / 255f, blue = b / 255f)
|
||||||
|
}
|
||||||
|
8 -> {
|
||||||
|
// AARRGGBB format
|
||||||
|
val a = cleanHex.substring(0, 2).toInt(16)
|
||||||
|
val r = cleanHex.substring(2, 4).toInt(16)
|
||||||
|
val g = cleanHex.substring(4, 6).toInt(16)
|
||||||
|
val b = cleanHex.substring(6, 8).toInt(16)
|
||||||
|
androidx.compose.ui.graphics.Color(red = r / 255f, green = g / 255f, blue = b / 255f, alpha = a / 255f)
|
||||||
|
}
|
||||||
|
else -> androidx.compose.ui.graphics.Color.Gray // Default fallback
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
androidx.compose.ui.graphics.Color.Gray // Fallback on parse error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import com.mycrib.shared.models.Residence
|
|||||||
import com.mycrib.shared.models.ResidenceCreateRequest
|
import com.mycrib.shared.models.ResidenceCreateRequest
|
||||||
import com.mycrib.shared.models.ResidenceSummaryResponse
|
import com.mycrib.shared.models.ResidenceSummaryResponse
|
||||||
import com.mycrib.shared.models.MyResidencesResponse
|
import com.mycrib.shared.models.MyResidencesResponse
|
||||||
import com.mycrib.shared.models.TasksByResidenceResponse
|
import com.mycrib.shared.models.TaskColumnsResponse
|
||||||
import com.mycrib.shared.network.ApiResult
|
import com.mycrib.shared.network.ApiResult
|
||||||
import com.mycrib.shared.network.ResidenceApi
|
import com.mycrib.shared.network.ResidenceApi
|
||||||
import com.mycrib.shared.network.TaskApi
|
import com.mycrib.shared.network.TaskApi
|
||||||
@@ -31,8 +31,8 @@ class ResidenceViewModel : ViewModel() {
|
|||||||
private val _updateResidenceState = MutableStateFlow<ApiResult<Residence>>(ApiResult.Idle)
|
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.Idle)
|
private val _residenceTasksState = MutableStateFlow<ApiResult<TaskColumnsResponse>>(ApiResult.Idle)
|
||||||
val residenceTasksState: StateFlow<ApiResult<TasksByResidenceResponse>> = _residenceTasksState
|
val residenceTasksState: StateFlow<ApiResult<TaskColumnsResponse>> = _residenceTasksState
|
||||||
|
|
||||||
private val _myResidencesState = MutableStateFlow<ApiResult<MyResidencesResponse>>(ApiResult.Idle)
|
private val _myResidencesState = MutableStateFlow<ApiResult<MyResidencesResponse>>(ApiResult.Idle)
|
||||||
val myResidencesState: StateFlow<ApiResult<MyResidencesResponse>> = _myResidencesState
|
val myResidencesState: StateFlow<ApiResult<MyResidencesResponse>> = _myResidencesState
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ package com.mycrib.android.viewmodel
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.mycrib.shared.models.AllTasksResponse
|
import com.mycrib.shared.models.TaskColumnsResponse
|
||||||
import com.mycrib.shared.models.CustomTask
|
import com.mycrib.shared.models.CustomTask
|
||||||
import com.mycrib.shared.models.TaskCreateRequest
|
import com.mycrib.shared.models.TaskCreateRequest
|
||||||
import com.mycrib.shared.models.TasksByResidenceResponse
|
|
||||||
import com.mycrib.shared.network.ApiResult
|
import com.mycrib.shared.network.ApiResult
|
||||||
import com.mycrib.shared.network.TaskApi
|
import com.mycrib.shared.network.TaskApi
|
||||||
import com.mycrib.storage.TokenStorage
|
import com.mycrib.storage.TokenStorage
|
||||||
@@ -16,11 +15,11 @@ 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.Idle)
|
private val _tasksState = MutableStateFlow<ApiResult<TaskColumnsResponse>>(ApiResult.Idle)
|
||||||
val tasksState: StateFlow<ApiResult<AllTasksResponse>> = _tasksState
|
val tasksState: StateFlow<ApiResult<TaskColumnsResponse>> = _tasksState
|
||||||
|
|
||||||
private val _tasksByResidenceState = MutableStateFlow<ApiResult<TasksByResidenceResponse>>(ApiResult.Idle)
|
private val _tasksByResidenceState = MutableStateFlow<ApiResult<TaskColumnsResponse>>(ApiResult.Idle)
|
||||||
val tasksByResidenceState: StateFlow<ApiResult<TasksByResidenceResponse>> = _tasksByResidenceState
|
val tasksByResidenceState: StateFlow<ApiResult<TaskColumnsResponse>> = _tasksByResidenceState
|
||||||
|
|
||||||
private val _taskAddNewCustomTaskState = MutableStateFlow<ApiResult<CustomTask>>(ApiResult.Idle)
|
private val _taskAddNewCustomTaskState = MutableStateFlow<ApiResult<CustomTask>>(ApiResult.Idle)
|
||||||
val taskAddNewCustomTaskState: StateFlow<ApiResult<CustomTask>> = _taskAddNewCustomTaskState
|
val taskAddNewCustomTaskState: StateFlow<ApiResult<CustomTask>> = _taskAddNewCustomTaskState
|
||||||
@@ -74,6 +73,48 @@ class TaskViewModel : ViewModel() {
|
|||||||
_taskAddNewCustomTaskState.value = ApiResult.Idle
|
_taskAddNewCustomTaskState.value = ApiResult.Idle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cancelTask(taskId: Int, onComplete: (Boolean) -> Unit) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val token = TokenStorage.getToken()
|
||||||
|
if (token != null) {
|
||||||
|
when (val result = taskApi.cancelTask(token, taskId)) {
|
||||||
|
is ApiResult.Success -> {
|
||||||
|
onComplete(true)
|
||||||
|
}
|
||||||
|
is ApiResult.Error -> {
|
||||||
|
onComplete(false)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
onComplete(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onComplete(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uncancelTask(taskId: Int, onComplete: (Boolean) -> Unit) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val token = TokenStorage.getToken()
|
||||||
|
if (token != null) {
|
||||||
|
when (val result = taskApi.uncancelTask(token, taskId)) {
|
||||||
|
is ApiResult.Success -> {
|
||||||
|
onComplete(true)
|
||||||
|
}
|
||||||
|
is ApiResult.Error -> {
|
||||||
|
onComplete(false)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
onComplete(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onComplete(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun markInProgress(taskId: Int, onComplete: (Boolean) -> Unit) {
|
fun markInProgress(taskId: Int, onComplete: (Boolean) -> Unit) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val token = TokenStorage.getToken()
|
val token = TokenStorage.getToken()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ struct ResidenceDetailView: View {
|
|||||||
let residenceId: Int32
|
let residenceId: Int32
|
||||||
@StateObject private var viewModel = ResidenceViewModel()
|
@StateObject private var viewModel = ResidenceViewModel()
|
||||||
@StateObject private var taskViewModel = TaskViewModel()
|
@StateObject private var taskViewModel = TaskViewModel()
|
||||||
@State private var tasksResponse: TasksByResidenceResponse?
|
@State private var tasksResponse: TaskColumnsResponse?
|
||||||
@State private var isLoadingTasks = false
|
@State private var isLoadingTasks = false
|
||||||
@State private var tasksError: String?
|
@State private var tasksError: String?
|
||||||
@State private var showAddTask = false
|
@State private var showAddTask = false
|
||||||
@@ -41,18 +41,18 @@ struct ResidenceDetailView: View {
|
|||||||
selectedTaskForEdit = task
|
selectedTaskForEdit = task
|
||||||
showEditTask = true
|
showEditTask = true
|
||||||
},
|
},
|
||||||
onCancelTask: { task in
|
onCancelTask: { taskId in
|
||||||
taskViewModel.cancelTask(id: task.id) { _ in
|
taskViewModel.cancelTask(id: taskId) { _ in
|
||||||
loadResidenceTasks()
|
loadResidenceTasks()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onUncancelTask: { task in
|
onUncancelTask: { taskId in
|
||||||
taskViewModel.uncancelTask(id: task.id) { _ in
|
taskViewModel.uncancelTask(id: taskId) { _ in
|
||||||
loadResidenceTasks()
|
loadResidenceTasks()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onMarkInProgress: { task in
|
onMarkInProgress: { taskId in
|
||||||
taskViewModel.markInProgress(id: task.id) { success in
|
taskViewModel.markInProgress(id: taskId) { success in
|
||||||
if success {
|
if success {
|
||||||
loadResidenceTasks()
|
loadResidenceTasks()
|
||||||
}
|
}
|
||||||
@@ -61,13 +61,13 @@ struct ResidenceDetailView: View {
|
|||||||
onCompleteTask: { task in
|
onCompleteTask: { task in
|
||||||
selectedTaskForComplete = task
|
selectedTaskForComplete = task
|
||||||
},
|
},
|
||||||
onArchiveTask: { task in
|
onArchiveTask: { taskId in
|
||||||
taskViewModel.archiveTask(id: task.id) { _ in
|
taskViewModel.archiveTask(id: taskId) { _ in
|
||||||
loadResidenceTasks()
|
loadResidenceTasks()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onUnarchiveTask: { task in
|
onUnarchiveTask: { taskId in
|
||||||
taskViewModel.unarchiveTask(id: task.id) { _ in
|
taskViewModel.unarchiveTask(id: taskId) { _ in
|
||||||
loadResidenceTasks()
|
loadResidenceTasks()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,7 +158,7 @@ struct ResidenceDetailView: View {
|
|||||||
|
|
||||||
let taskApi = TaskApi(client: ApiClient_iosKt.createHttpClient())
|
let taskApi = TaskApi(client: ApiClient_iosKt.createHttpClient())
|
||||||
taskApi.getTasksByResidence(token: token, residenceId: residenceId, days: 30) { result, error in
|
taskApi.getTasksByResidence(token: token, residenceId: residenceId, days: 30) { result, error in
|
||||||
if let successResult = result as? ApiResultSuccess<TasksByResidenceResponse> {
|
if let successResult = result as? ApiResultSuccess<TaskColumnsResponse> {
|
||||||
self.tasksResponse = successResult.data
|
self.tasksResponse = successResult.data
|
||||||
self.isLoadingTasks = false
|
self.isLoadingTasks = false
|
||||||
} else if let errorResult = result as? ApiResultError {
|
} else if let errorResult = result as? ApiResultError {
|
||||||
|
|||||||
185
iosApp/iosApp/Subviews/Task/TaskActionButtons.swift
Normal file
185
iosApp/iosApp/Subviews/Task/TaskActionButtons.swift
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import ComposeApp
|
||||||
|
|
||||||
|
// MARK: - Edit Task Button
|
||||||
|
struct EditTaskButton: View {
|
||||||
|
let taskId: Int32
|
||||||
|
let onCompletion: () -> Void
|
||||||
|
let onError: (String) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: {
|
||||||
|
// Edit navigates to edit screen - handled by parent
|
||||||
|
onCompletion()
|
||||||
|
}) {
|
||||||
|
Label("Edit", systemImage: "pencil")
|
||||||
|
.font(.subheadline)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Cancel Task Button
|
||||||
|
struct CancelTaskButton: View {
|
||||||
|
let taskId: Int32
|
||||||
|
let onCompletion: () -> Void
|
||||||
|
let onError: (String) -> Void
|
||||||
|
|
||||||
|
@StateObject private var viewModel = TaskViewModel()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: {
|
||||||
|
viewModel.cancelTask(id: taskId) { success in
|
||||||
|
if success {
|
||||||
|
onCompletion()
|
||||||
|
} else {
|
||||||
|
onError("Failed to cancel task")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Label("Cancel", systemImage: "xmark.circle")
|
||||||
|
.font(.subheadline)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.tint(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Uncancel (Restore) Task Button
|
||||||
|
struct UncancelTaskButton: View {
|
||||||
|
let taskId: Int32
|
||||||
|
let onCompletion: () -> Void
|
||||||
|
let onError: (String) -> Void
|
||||||
|
|
||||||
|
@StateObject private var viewModel = TaskViewModel()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: {
|
||||||
|
viewModel.uncancelTask(id: taskId) { success in
|
||||||
|
if success {
|
||||||
|
onCompletion()
|
||||||
|
} else {
|
||||||
|
onError("Failed to restore task")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Label("Restore", systemImage: "arrow.uturn.backward")
|
||||||
|
.font(.subheadline)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.tint(.blue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Mark In Progress Button
|
||||||
|
struct MarkInProgressButton: View {
|
||||||
|
let taskId: Int32
|
||||||
|
let onCompletion: () -> Void
|
||||||
|
let onError: (String) -> Void
|
||||||
|
|
||||||
|
@StateObject private var viewModel = TaskViewModel()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: {
|
||||||
|
viewModel.markInProgress(id: taskId) { success in
|
||||||
|
if success {
|
||||||
|
onCompletion()
|
||||||
|
} else {
|
||||||
|
onError("Failed to mark task in progress")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "play.circle.fill")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 18, height: 18)
|
||||||
|
Text("In Progress")
|
||||||
|
.font(.subheadline.weight(.semibold))
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.tint(.orange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Complete Task Button
|
||||||
|
struct CompleteTaskButton: View {
|
||||||
|
let taskId: Int32
|
||||||
|
let onCompletion: () -> Void
|
||||||
|
let onError: (String) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: {
|
||||||
|
// Complete shows dialog - handled by parent
|
||||||
|
onCompletion()
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 18, height: 18)
|
||||||
|
Text("Complete")
|
||||||
|
.font(.subheadline.weight(.semibold))
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Archive Task Button
|
||||||
|
struct ArchiveTaskButton: View {
|
||||||
|
let taskId: Int32
|
||||||
|
let onCompletion: () -> Void
|
||||||
|
let onError: (String) -> Void
|
||||||
|
|
||||||
|
@StateObject private var viewModel = TaskViewModel()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: {
|
||||||
|
viewModel.archiveTask(id: taskId) { success in
|
||||||
|
if success {
|
||||||
|
onCompletion()
|
||||||
|
} else {
|
||||||
|
onError("Failed to archive task")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Label("Archive", systemImage: "archivebox")
|
||||||
|
.font(.subheadline)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.tint(.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Unarchive Task Button
|
||||||
|
struct UnarchiveTaskButton: View {
|
||||||
|
let taskId: Int32
|
||||||
|
let onCompletion: () -> Void
|
||||||
|
let onError: (String) -> Void
|
||||||
|
|
||||||
|
@StateObject private var viewModel = TaskViewModel()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: {
|
||||||
|
viewModel.unarchiveTask(id: taskId) { success in
|
||||||
|
if success {
|
||||||
|
onCompletion()
|
||||||
|
} else {
|
||||||
|
onError("Failed to unarchive task")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Label("Unarchive", systemImage: "tray.and.arrow.up")
|
||||||
|
.font(.subheadline)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.tint(.blue)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,14 +2,18 @@ import SwiftUI
|
|||||||
import ComposeApp
|
import ComposeApp
|
||||||
|
|
||||||
struct TasksSection: View {
|
struct TasksSection: View {
|
||||||
let tasksResponse: TasksByResidenceResponse
|
let tasksResponse: TaskColumnsResponse
|
||||||
let onEditTask: (TaskDetail) -> Void
|
let onEditTask: (TaskDetail) -> Void
|
||||||
let onCancelTask: (TaskDetail) -> Void
|
let onCancelTask: (Int32) -> Void
|
||||||
let onUncancelTask: (TaskDetail) -> Void
|
let onUncancelTask: (Int32) -> Void
|
||||||
let onMarkInProgress: (TaskDetail) -> Void
|
let onMarkInProgress: (Int32) -> Void
|
||||||
let onCompleteTask: (TaskDetail) -> Void
|
let onCompleteTask: (TaskDetail) -> Void
|
||||||
let onArchiveTask: (TaskDetail) -> Void
|
let onArchiveTask: (Int32) -> Void
|
||||||
let onUnarchiveTask: (TaskDetail) -> Void
|
let onUnarchiveTask: (Int32) -> Void
|
||||||
|
|
||||||
|
private var hasNoTasks: Bool {
|
||||||
|
tasksResponse.columns.allSatisfy { $0.tasks.isEmpty }
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
@@ -17,80 +21,41 @@ struct TasksSection: View {
|
|||||||
.font(.title2)
|
.font(.title2)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
|
|
||||||
if tasksResponse.upcomingTasks.isEmpty && tasksResponse.inProgressTasks.isEmpty && tasksResponse.doneTasks.isEmpty && tasksResponse.archivedTasks.isEmpty {
|
if hasNoTasks {
|
||||||
EmptyTasksView()
|
EmptyTasksView()
|
||||||
} else {
|
} else {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
LazyHStack(spacing: 16) {
|
LazyHStack(spacing: 16) {
|
||||||
// Upcoming Column
|
// Dynamically create columns from response
|
||||||
TaskColumnView(
|
ForEach(Array(tasksResponse.columns.enumerated()), id: \.element.name) { index, column in
|
||||||
title: "Upcoming",
|
DynamicTaskColumnView(
|
||||||
icon: "calendar",
|
column: column,
|
||||||
color: .blue,
|
onEditTask: { task in
|
||||||
count: tasksResponse.upcomingTasks.count,
|
onEditTask(task)
|
||||||
tasks: tasksResponse.upcomingTasks,
|
},
|
||||||
onEditTask: onEditTask,
|
onCancelTask: { taskId in
|
||||||
onCancelTask: onCancelTask,
|
onCancelTask(taskId)
|
||||||
onUncancelTask: onUncancelTask,
|
},
|
||||||
onMarkInProgress: onMarkInProgress,
|
onUncancelTask: { taskId in
|
||||||
onCompleteTask: onCompleteTask,
|
onUncancelTask(taskId)
|
||||||
onArchiveTask: onArchiveTask,
|
},
|
||||||
onUnarchiveTask: onUnarchiveTask
|
onMarkInProgress: { taskId in
|
||||||
)
|
onMarkInProgress(taskId)
|
||||||
.frame(width: geometry.size.width - 48)
|
},
|
||||||
|
onCompleteTask: { task in
|
||||||
// In Progress Column
|
onCompleteTask(task)
|
||||||
TaskColumnView(
|
},
|
||||||
title: "In Progress",
|
onArchiveTask: { taskId in
|
||||||
icon: "play.circle",
|
onArchiveTask(taskId)
|
||||||
color: .orange,
|
},
|
||||||
count: tasksResponse.inProgressTasks.count,
|
onUnarchiveTask: { taskId in
|
||||||
tasks: tasksResponse.inProgressTasks,
|
onUnarchiveTask(taskId)
|
||||||
onEditTask: onEditTask,
|
}
|
||||||
onCancelTask: onCancelTask,
|
|
||||||
onUncancelTask: onUncancelTask,
|
|
||||||
onMarkInProgress: nil,
|
|
||||||
onCompleteTask: onCompleteTask,
|
|
||||||
onArchiveTask: onArchiveTask,
|
|
||||||
onUnarchiveTask: onUnarchiveTask
|
|
||||||
)
|
|
||||||
.frame(width: geometry.size.width - 48)
|
|
||||||
|
|
||||||
// Done Column
|
|
||||||
TaskColumnView(
|
|
||||||
title: "Done",
|
|
||||||
icon: "checkmark.circle",
|
|
||||||
color: .green,
|
|
||||||
count: tasksResponse.doneTasks.count,
|
|
||||||
tasks: tasksResponse.doneTasks,
|
|
||||||
onEditTask: onEditTask,
|
|
||||||
onCancelTask: nil,
|
|
||||||
onUncancelTask: nil,
|
|
||||||
onMarkInProgress: nil,
|
|
||||||
onCompleteTask: nil,
|
|
||||||
onArchiveTask: onArchiveTask,
|
|
||||||
onUnarchiveTask: onUnarchiveTask
|
|
||||||
)
|
|
||||||
.frame(width: geometry.size.width - 48)
|
|
||||||
|
|
||||||
// Archived Column
|
|
||||||
TaskColumnView(
|
|
||||||
title: "Archived",
|
|
||||||
icon: "archivebox",
|
|
||||||
color: .gray,
|
|
||||||
count: tasksResponse.archivedTasks.count,
|
|
||||||
tasks: tasksResponse.archivedTasks,
|
|
||||||
onEditTask: onEditTask,
|
|
||||||
onCancelTask: nil,
|
|
||||||
onUncancelTask: nil,
|
|
||||||
onMarkInProgress: nil,
|
|
||||||
onCompleteTask: nil,
|
|
||||||
onArchiveTask: nil,
|
|
||||||
onUnarchiveTask: onUnarchiveTask
|
|
||||||
)
|
)
|
||||||
.frame(width: geometry.size.width - 48)
|
.frame(width: geometry.size.width - 48)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.scrollTargetLayout()
|
.scrollTargetLayout()
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
}
|
}
|
||||||
@@ -104,25 +69,24 @@ struct TasksSection: View {
|
|||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
TasksSection(
|
TasksSection(
|
||||||
tasksResponse: TasksByResidenceResponse(
|
tasksResponse: TaskColumnsResponse(
|
||||||
residenceId: "1",
|
columns: [
|
||||||
daysThreshold: 30,
|
TaskColumn(
|
||||||
summary: CategorizedTaskSummary(
|
name: "upcoming_tasks",
|
||||||
upcoming: 3,
|
displayName: "Upcoming",
|
||||||
inProgress: 1,
|
buttonTypes: ["edit", "cancel", "uncancel", "mark_in_progress", "complete", "archive"],
|
||||||
done: 2,
|
icons: ["ios": "calendar", "android": "CalendarToday", "web": "calendar"],
|
||||||
archived: 0
|
color: "#007AFF",
|
||||||
),
|
tasks: [
|
||||||
upcomingTasks: [
|
|
||||||
TaskDetail(
|
TaskDetail(
|
||||||
id: 1,
|
id: 1,
|
||||||
residence: 1,
|
residence: 1,
|
||||||
title: "Clean Gutters",
|
title: "Clean Gutters",
|
||||||
description: "Remove all debris",
|
description: "Remove all debris",
|
||||||
category: TaskCategory(id: 1, name: "maintenance", description: "General upkeep tasks"),
|
category: TaskCategory(id: 1, name: "maintenance", description: ""),
|
||||||
priority: TaskPriority(id: 2, name: "medium", displayName: "Medium", description: "Standard priority"),
|
priority: TaskPriority(id: 2, name: "medium", displayName: "Medium", description: ""),
|
||||||
frequency: TaskFrequency(id: 1, name: "monthly", displayName: "Monthly", daySpan: 0, notifyDays: 0),
|
frequency: TaskFrequency(id: 1, name: "monthly", displayName: "Monthly", daySpan: 0, notifyDays: 0),
|
||||||
status: TaskStatus(id: 1, name: "pending", displayName: "Pending", description: "Awaiting completion"),
|
status: TaskStatus(id: 1, name: "pending", displayName: "Pending", description: ""),
|
||||||
dueDate: "2024-12-15",
|
dueDate: "2024-12-15",
|
||||||
estimatedCost: "150.00",
|
estimatedCost: "150.00",
|
||||||
actualCost: nil,
|
actualCost: nil,
|
||||||
@@ -135,17 +99,24 @@ struct TasksSection: View {
|
|||||||
completions: []
|
completions: []
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
inProgressTasks: [],
|
count: 1
|
||||||
doneTasks: [
|
),
|
||||||
|
TaskColumn(
|
||||||
|
name: "done_tasks",
|
||||||
|
displayName: "Done",
|
||||||
|
buttonTypes: ["edit", "archive"],
|
||||||
|
icons: ["ios": "checkmark.circle", "android": "CheckCircle", "web": "check-circle"],
|
||||||
|
color: "#34C759",
|
||||||
|
tasks: [
|
||||||
TaskDetail(
|
TaskDetail(
|
||||||
id: 2,
|
id: 2,
|
||||||
residence: 1,
|
residence: 1,
|
||||||
title: "Fix Leaky Faucet",
|
title: "Fix Leaky Faucet",
|
||||||
description: "Kitchen sink fixed",
|
description: "Kitchen sink fixed",
|
||||||
category: TaskCategory(id: 2, name: "plumbing", description: "Plumbing tasks"),
|
category: TaskCategory(id: 2, name: "plumbing", description: ""),
|
||||||
priority: TaskPriority(id: 3, name: "high", displayName: "High", description: "High priority"),
|
priority: TaskPriority(id: 3, name: "high", displayName: "High", description: ""),
|
||||||
frequency: TaskFrequency(id: 6, name: "once", displayName: "One Time", daySpan: 0, notifyDays: 0),
|
frequency: TaskFrequency(id: 6, name: "once", displayName: "One Time", daySpan: 0, notifyDays: 0),
|
||||||
status: TaskStatus(id: 3, name: "completed", displayName: "Completed", description: "Task completed"),
|
status: TaskStatus(id: 3, name: "completed", displayName: "Completed", description: ""),
|
||||||
dueDate: "2024-11-01",
|
dueDate: "2024-11-01",
|
||||||
estimatedCost: "200.00",
|
estimatedCost: "200.00",
|
||||||
actualCost: nil,
|
actualCost: nil,
|
||||||
@@ -158,15 +129,19 @@ struct TasksSection: View {
|
|||||||
completions: []
|
completions: []
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
archivedTasks: []
|
count: 1
|
||||||
|
)
|
||||||
|
],
|
||||||
|
daysThreshold: 30,
|
||||||
|
residenceId: "1"
|
||||||
),
|
),
|
||||||
onEditTask: { _ in },
|
onEditTask: { _ in },
|
||||||
onCancelTask: { _ in },
|
onCancelTask: { _ in },
|
||||||
onUncancelTask: { _ in },
|
onUncancelTask: { _ in },
|
||||||
onMarkInProgress: { _ in },
|
onMarkInProgress: { _ in },
|
||||||
onCompleteTask: { _ in }
|
onCompleteTask: { _ in },
|
||||||
, onArchiveTask: { _ in }
|
onArchiveTask: { _ in },
|
||||||
, onUnarchiveTask: { _ in }
|
onUnarchiveTask: { _ in }
|
||||||
)
|
)
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ struct AddTaskView: View {
|
|||||||
frequency: Int32(frequency.id),
|
frequency: Int32(frequency.id),
|
||||||
intervalDays: intervalDays.isEmpty ? nil : Int32(intervalDays) as? KotlinInt,
|
intervalDays: intervalDays.isEmpty ? nil : Int32(intervalDays) as? KotlinInt,
|
||||||
priority: Int32(priority.id),
|
priority: Int32(priority.id),
|
||||||
|
status: selectedStatus.map { KotlinInt(value: $0.id) },
|
||||||
dueDate: dueDateString,
|
dueDate: dueDateString,
|
||||||
estimatedCost: estimatedCost.isEmpty ? nil : estimatedCost
|
estimatedCost: estimatedCost.isEmpty ? nil : estimatedCost
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -237,6 +237,7 @@ struct AddTaskWithResidenceView: View {
|
|||||||
frequency: Int32(frequency.id),
|
frequency: Int32(frequency.id),
|
||||||
intervalDays: intervalDays.isEmpty ? nil : Int32(intervalDays) as? KotlinInt,
|
intervalDays: intervalDays.isEmpty ? nil : Int32(intervalDays) as? KotlinInt,
|
||||||
priority: Int32(priority.id),
|
priority: Int32(priority.id),
|
||||||
|
status: selectedStatus.map { KotlinInt(value: $0.id) },
|
||||||
dueDate: dueDateString,
|
dueDate: dueDateString,
|
||||||
estimatedCost: estimatedCost.isEmpty ? nil : estimatedCost
|
estimatedCost: estimatedCost.isEmpty ? nil : estimatedCost
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import ComposeApp
|
|||||||
struct AllTasksView: View {
|
struct AllTasksView: View {
|
||||||
@StateObject private var taskViewModel = TaskViewModel()
|
@StateObject private var taskViewModel = TaskViewModel()
|
||||||
@StateObject private var residenceViewModel = ResidenceViewModel()
|
@StateObject private var residenceViewModel = ResidenceViewModel()
|
||||||
@State private var tasksResponse: AllTasksResponse?
|
@State private var tasksResponse: TaskColumnsResponse?
|
||||||
@State private var isLoadingTasks = false
|
@State private var isLoadingTasks = false
|
||||||
@State private var tasksError: String?
|
@State private var tasksError: String?
|
||||||
@State private var showAddTask = false
|
@State private var showAddTask = false
|
||||||
@@ -14,10 +14,7 @@ struct AllTasksView: View {
|
|||||||
|
|
||||||
private var hasNoTasks: Bool {
|
private var hasNoTasks: Bool {
|
||||||
guard let response = tasksResponse else { return true }
|
guard let response = tasksResponse else { return true }
|
||||||
return response.upcomingTasks.isEmpty &&
|
return response.columns.allSatisfy { $0.tasks.isEmpty }
|
||||||
response.inProgressTasks.isEmpty &&
|
|
||||||
response.doneTasks.isEmpty &&
|
|
||||||
response.archivedTasks.isEmpty
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var hasTasks: Bool {
|
private var hasTasks: Bool {
|
||||||
@@ -83,29 +80,26 @@ struct AllTasksView: View {
|
|||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
LazyHStack(spacing: 16) {
|
LazyHStack(spacing: 16) {
|
||||||
// Upcoming Column
|
// Dynamically create columns from response
|
||||||
TaskColumnView(
|
ForEach(Array(tasksResponse.columns.enumerated()), id: \.element.name) { index, column in
|
||||||
title: "Upcoming",
|
DynamicTaskColumnView(
|
||||||
icon: "calendar",
|
column: column,
|
||||||
color: .blue,
|
|
||||||
count: tasksResponse.upcomingTasks.count,
|
|
||||||
tasks: tasksResponse.upcomingTasks,
|
|
||||||
onEditTask: { task in
|
onEditTask: { task in
|
||||||
selectedTaskForEdit = task
|
selectedTaskForEdit = task
|
||||||
showEditTask = true
|
showEditTask = true
|
||||||
},
|
},
|
||||||
onCancelTask: { task in
|
onCancelTask: { taskId in
|
||||||
taskViewModel.cancelTask(id: task.id) { _ in
|
taskViewModel.cancelTask(id: taskId) { _ in
|
||||||
loadAllTasks()
|
loadAllTasks()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onUncancelTask: { task in
|
onUncancelTask: { taskId in
|
||||||
taskViewModel.uncancelTask(id: task.id) { _ in
|
taskViewModel.uncancelTask(id: taskId) { _ in
|
||||||
loadAllTasks()
|
loadAllTasks()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onMarkInProgress: { task in
|
onMarkInProgress: { taskId in
|
||||||
taskViewModel.markInProgress(id: task.id) { success in
|
taskViewModel.markInProgress(id: taskId) { success in
|
||||||
if success {
|
if success {
|
||||||
loadAllTasks()
|
loadAllTasks()
|
||||||
}
|
}
|
||||||
@@ -114,98 +108,20 @@ struct AllTasksView: View {
|
|||||||
onCompleteTask: { task in
|
onCompleteTask: { task in
|
||||||
selectedTaskForComplete = task
|
selectedTaskForComplete = task
|
||||||
},
|
},
|
||||||
onArchiveTask: { task in
|
onArchiveTask: { taskId in
|
||||||
taskViewModel.archiveTask(id: task.id) { _ in
|
taskViewModel.archiveTask(id: taskId) { _ in
|
||||||
loadAllTasks()
|
loadAllTasks()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onUnarchiveTask: nil
|
onUnarchiveTask: { taskId in
|
||||||
)
|
taskViewModel.unarchiveTask(id: taskId) { _ in
|
||||||
.frame(width: geometry.size.width - 48)
|
|
||||||
|
|
||||||
|
|
||||||
// In Progress Column
|
|
||||||
TaskColumnView(
|
|
||||||
title: "In Progress",
|
|
||||||
icon: "play.circle",
|
|
||||||
color: .orange,
|
|
||||||
count: tasksResponse.inProgressTasks.count,
|
|
||||||
tasks: tasksResponse.inProgressTasks,
|
|
||||||
onEditTask: { task in
|
|
||||||
selectedTaskForEdit = task
|
|
||||||
showEditTask = true
|
|
||||||
},
|
|
||||||
onCancelTask: { task in
|
|
||||||
taskViewModel.cancelTask(id: task.id) { _ in
|
|
||||||
loadAllTasks()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onUncancelTask: { task in
|
|
||||||
taskViewModel.uncancelTask(id: task.id) { _ in
|
|
||||||
loadAllTasks()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onMarkInProgress: nil,
|
|
||||||
onCompleteTask: { task in
|
|
||||||
selectedTaskForComplete = task
|
|
||||||
},
|
|
||||||
onArchiveTask: { task in
|
|
||||||
taskViewModel.archiveTask(id: task.id) { _ in
|
|
||||||
loadAllTasks()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onUnarchiveTask: nil
|
|
||||||
)
|
|
||||||
.frame(width: geometry.size.width - 48)
|
|
||||||
|
|
||||||
// Done Column
|
|
||||||
TaskColumnView(
|
|
||||||
title: "Done",
|
|
||||||
icon: "checkmark.circle",
|
|
||||||
color: .green,
|
|
||||||
count: tasksResponse.doneTasks.count,
|
|
||||||
tasks: tasksResponse.doneTasks,
|
|
||||||
onEditTask: { task in
|
|
||||||
selectedTaskForEdit = task
|
|
||||||
showEditTask = true
|
|
||||||
},
|
|
||||||
onCancelTask: nil,
|
|
||||||
onUncancelTask: nil,
|
|
||||||
onMarkInProgress: nil,
|
|
||||||
onCompleteTask: nil,
|
|
||||||
onArchiveTask: { task in
|
|
||||||
taskViewModel.archiveTask(id: task.id) { _ in
|
|
||||||
loadAllTasks()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onUnarchiveTask: nil
|
|
||||||
)
|
|
||||||
.frame(width: geometry.size.width - 48)
|
|
||||||
|
|
||||||
// Archived Column
|
|
||||||
TaskColumnView(
|
|
||||||
title: "Archived",
|
|
||||||
icon: "archivebox",
|
|
||||||
color: .gray,
|
|
||||||
count: tasksResponse.archivedTasks.count,
|
|
||||||
tasks: tasksResponse.archivedTasks,
|
|
||||||
onEditTask: { task in
|
|
||||||
selectedTaskForEdit = task
|
|
||||||
showEditTask = true
|
|
||||||
},
|
|
||||||
onCancelTask: nil,
|
|
||||||
onUncancelTask: nil,
|
|
||||||
onMarkInProgress: nil,
|
|
||||||
onCompleteTask: nil,
|
|
||||||
onArchiveTask: nil,
|
|
||||||
onUnarchiveTask: { task in
|
|
||||||
taskViewModel.unarchiveTask(id: task.id) { _ in
|
|
||||||
loadAllTasks()
|
loadAllTasks()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.frame(width: geometry.size.width - 48)
|
.frame(width: geometry.size.width - 48)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.scrollTargetLayout()
|
.scrollTargetLayout()
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
}
|
}
|
||||||
@@ -268,7 +184,7 @@ private func loadAllTasks() {
|
|||||||
|
|
||||||
let taskApi = TaskApi(client: ApiClient_iosKt.createHttpClient())
|
let taskApi = TaskApi(client: ApiClient_iosKt.createHttpClient())
|
||||||
taskApi.getTasks(token: token, days: 30) { result, error in
|
taskApi.getTasks(token: token, days: 30) { result, error in
|
||||||
if let successResult = result as? ApiResultSuccess<AllTasksResponse> {
|
if let successResult = result as? ApiResultSuccess<TaskColumnsResponse> {
|
||||||
self.tasksResponse = successResult.data
|
self.tasksResponse = successResult.data
|
||||||
self.isLoadingTasks = false
|
self.isLoadingTasks = false
|
||||||
} else if let errorResult = result as? ApiResultError {
|
} else if let errorResult = result as? ApiResultError {
|
||||||
@@ -282,69 +198,76 @@ private func loadAllTasks() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TaskColumnView: View {
|
/// Dynamic task column view that adapts based on the column configuration
|
||||||
let title: String
|
struct DynamicTaskColumnView: View {
|
||||||
let icon: String
|
let column: TaskColumn
|
||||||
let color: Color
|
|
||||||
let count: Int
|
|
||||||
let tasks: [TaskDetail]
|
|
||||||
let onEditTask: (TaskDetail) -> Void
|
let onEditTask: (TaskDetail) -> Void
|
||||||
let onCancelTask: ((TaskDetail) -> Void)?
|
let onCancelTask: (Int32) -> Void
|
||||||
let onUncancelTask: ((TaskDetail) -> Void)?
|
let onUncancelTask: (Int32) -> Void
|
||||||
let onMarkInProgress: ((TaskDetail) -> Void)?
|
let onMarkInProgress: (Int32) -> Void
|
||||||
let onCompleteTask: ((TaskDetail) -> Void)?
|
let onCompleteTask: (TaskDetail) -> Void
|
||||||
let onArchiveTask: ((TaskDetail) -> Void)?
|
let onArchiveTask: (Int32) -> Void
|
||||||
let onUnarchiveTask: ((TaskDetail) -> Void)?
|
let onUnarchiveTask: (Int32) -> Void
|
||||||
|
|
||||||
|
// Get icon from API response, with fallback
|
||||||
|
private var columnIcon: String {
|
||||||
|
column.icons["ios"] ?? "list.bullet"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var columnColor: Color {
|
||||||
|
Color(hex: column.color) ?? .primary
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// Tasks List
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
// Header
|
// Header
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Image(systemName: icon)
|
Image(systemName: columnIcon)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(color)
|
.foregroundColor(columnColor)
|
||||||
|
|
||||||
Text(title)
|
Text(column.displayName)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(color)
|
.foregroundColor(columnColor)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Text("\(count)")
|
Text("\(column.count)")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
.background(color)
|
.background(columnColor)
|
||||||
.cornerRadius(12)
|
.cornerRadius(12)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tasks.isEmpty {
|
if column.tasks.isEmpty {
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
Image(systemName: icon)
|
Image(systemName: columnIcon)
|
||||||
.font(.system(size: 40))
|
.font(.system(size: 40))
|
||||||
.foregroundColor(color.opacity(0.3))
|
.foregroundColor(columnColor.opacity(0.3))
|
||||||
|
|
||||||
Text("No tasks")
|
Text("No tasks")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.top, 40)
|
||||||
} else {
|
} else {
|
||||||
ForEach(tasks, id: \.id) { task in
|
ForEach(column.tasks, id: \.id) { task in
|
||||||
TaskCard(
|
DynamicTaskCard(
|
||||||
task: task,
|
task: task,
|
||||||
|
buttonTypes: column.buttonTypes,
|
||||||
onEdit: { onEditTask(task) },
|
onEdit: { onEditTask(task) },
|
||||||
onCancel: onCancelTask != nil ? { onCancelTask?(task) } : nil,
|
onCancel: { onCancelTask(task.id) },
|
||||||
onUncancel: onUncancelTask != nil ? { onUncancelTask?(task) } : nil,
|
onUncancel: { onUncancelTask(task.id) },
|
||||||
onMarkInProgress: onMarkInProgress != nil ? { onMarkInProgress?(task) } : nil,
|
onMarkInProgress: { onMarkInProgress(task.id) },
|
||||||
onComplete: onCompleteTask != nil ? { onCompleteTask?(task) } : nil,
|
onComplete: { onCompleteTask(task) },
|
||||||
onArchive: onArchiveTask != nil ? { onArchiveTask?(task) } : nil,
|
onArchive: { onArchiveTask(task.id) },
|
||||||
onUnarchive: onUnarchiveTask != nil ? { onUnarchiveTask?(task) } : nil
|
onUnarchive: { onUnarchiveTask(task.id) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,6 +277,155 @@ struct TaskColumnView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Task card that dynamically renders buttons based on the column's button types
|
||||||
|
struct DynamicTaskCard: View {
|
||||||
|
let task: TaskDetail
|
||||||
|
let buttonTypes: [String]
|
||||||
|
let onEdit: () -> Void
|
||||||
|
let onCancel: () -> Void
|
||||||
|
let onUncancel: () -> Void
|
||||||
|
let onMarkInProgress: () -> Void
|
||||||
|
let onComplete: () -> Void
|
||||||
|
let onArchive: () -> Void
|
||||||
|
let onUnarchive: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(task.title)
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
if let status = task.status {
|
||||||
|
StatusBadge(status: status.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
PriorityBadge(priority: task.priority.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let description = task.description_, !description.isEmpty {
|
||||||
|
Text(description)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Label(task.frequency.displayName, systemImage: "repeat")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Label(formatDate(task.dueDate), systemImage: "calendar")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if task.completions.count > 0 {
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "checkmark.circle")
|
||||||
|
.foregroundColor(.green)
|
||||||
|
Text("Completed \(task.completions.count) time\(task.completions.count == 1 ? "" : "s")")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render buttons based on buttonTypes array
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
ForEach(Array(buttonTypes.enumerated()), id: \.offset) { index, buttonType in
|
||||||
|
renderButton(for: buttonType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(16)
|
||||||
|
.background(Color(.systemBackground))
|
||||||
|
.cornerRadius(12)
|
||||||
|
.shadow(color: Color.black.opacity(0.05), radius: 3, x: 0, y: 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formatDate(_ dateString: String) -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "yyyy-MM-dd"
|
||||||
|
if let date = formatter.date(from: dateString) {
|
||||||
|
formatter.dateStyle = .medium
|
||||||
|
return formatter.string(from: date)
|
||||||
|
}
|
||||||
|
return dateString
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func renderButton(for buttonType: String) -> some View {
|
||||||
|
switch buttonType {
|
||||||
|
case "mark_in_progress":
|
||||||
|
MarkInProgressButton(
|
||||||
|
taskId: task.id,
|
||||||
|
onCompletion: onMarkInProgress,
|
||||||
|
onError: { error in
|
||||||
|
print("Error marking in progress: \(error)")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
case "complete":
|
||||||
|
CompleteTaskButton(
|
||||||
|
taskId: task.id,
|
||||||
|
onCompletion: onComplete,
|
||||||
|
onError: { error in
|
||||||
|
print("Error completing task: \(error)")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
case "edit":
|
||||||
|
EditTaskButton(
|
||||||
|
taskId: task.id,
|
||||||
|
onCompletion: onEdit,
|
||||||
|
onError: { error in
|
||||||
|
print("Error editing task: \(error)")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
case "cancel":
|
||||||
|
CancelTaskButton(
|
||||||
|
taskId: task.id,
|
||||||
|
onCompletion: onCancel,
|
||||||
|
onError: { error in
|
||||||
|
print("Error cancelling task: \(error)")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
case "uncancel":
|
||||||
|
UncancelTaskButton(
|
||||||
|
taskId: task.id,
|
||||||
|
onCompletion: onUncancel,
|
||||||
|
onError: { error in
|
||||||
|
print("Error restoring task: \(error)")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
case "archive":
|
||||||
|
ArchiveTaskButton(
|
||||||
|
taskId: task.id,
|
||||||
|
onCompletion: onArchive,
|
||||||
|
onError: { error in
|
||||||
|
print("Error archiving task: \(error)")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
case "unarchive":
|
||||||
|
UnarchiveTaskButton(
|
||||||
|
taskId: task.id,
|
||||||
|
onCompletion: onUnarchive,
|
||||||
|
onError: { error in
|
||||||
|
print("Error unarchiving task: \(error)")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Extension to apply corner radius to specific corners
|
// Extension to apply corner radius to specific corners
|
||||||
extension View {
|
extension View {
|
||||||
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
|
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
|
||||||
@@ -381,7 +453,6 @@ struct RoundedCorner: Shape {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extension Array where Element == ResidenceWithTasks {
|
extension Array where Element == ResidenceWithTasks {
|
||||||
/// Converts an array of ResidenceWithTasks into an array of Residence.
|
/// Converts an array of ResidenceWithTasks into an array of Residence.
|
||||||
/// Adjust the mapping inside as needed to match your model initializers.
|
/// Adjust the mapping inside as needed to match your model initializers.
|
||||||
@@ -414,3 +485,31 @@ extension Array where Element == ResidenceWithTasks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Color {
|
||||||
|
/// Initialize Color from hex string (e.g., "#007AFF" or "007AFF")
|
||||||
|
init?(hex: String) {
|
||||||
|
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||||
|
var int: UInt64 = 0
|
||||||
|
Scanner(string: hex).scanHexInt64(&int)
|
||||||
|
let a, r, g, b: UInt64
|
||||||
|
switch hex.count {
|
||||||
|
case 3: // RGB (12-bit)
|
||||||
|
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
|
||||||
|
case 6: // RGB (24-bit)
|
||||||
|
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
|
||||||
|
case 8: // ARGB (32-bit)
|
||||||
|
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(
|
||||||
|
.sRGB,
|
||||||
|
red: Double(r) / 255,
|
||||||
|
green: Double(g) / 255,
|
||||||
|
blue: Double(b) / 255,
|
||||||
|
opacity: Double(a) / 255
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ struct EditTaskView: View {
|
|||||||
frequency: frequency.id,
|
frequency: frequency.id,
|
||||||
intervalDays: nil,
|
intervalDays: nil,
|
||||||
priority: priority.id,
|
priority: priority.id,
|
||||||
|
status: KotlinInt(value: status.id),
|
||||||
dueDate: dueDate,
|
dueDate: dueDate,
|
||||||
estimatedCost: estimatedCost.isEmpty ? nil : estimatedCost
|
estimatedCost: estimatedCost.isEmpty ? nil : estimatedCost
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user