Migrate Android app to Material3 system colors and improve UX
- Remove AppColors object, migrate to Material3 semantic colors throughout - Add collapsible dropdown menu for task card actions - Implement pull-to-refresh on Residences, Documents, and Contractors screens - Add refresh button to All Tasks screen with forceRefresh support - Fix nullable function reference error in TaskCard (make onEditClick nullable) - Fix layout padding issues on All Tasks and Tasks screens - Remove unused AppColors import from HomeScreen 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,13 +7,16 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.Description
|
import androidx.compose.material.icons.filled.Description
|
||||||
import androidx.compose.material.icons.filled.ReceiptLong
|
import androidx.compose.material.icons.filled.ReceiptLong
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.mycrib.shared.models.DocumentListResponse
|
import com.mycrib.shared.models.DocumentListResponse
|
||||||
import com.mycrib.shared.network.ApiResult
|
import com.mycrib.shared.network.ApiResult
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DocumentsTabContent(
|
fun DocumentsTabContent(
|
||||||
state: ApiResult<DocumentListResponse>,
|
state: ApiResult<DocumentListResponse>,
|
||||||
@@ -21,10 +24,21 @@ fun DocumentsTabContent(
|
|||||||
onDocumentClick: (Int) -> Unit,
|
onDocumentClick: (Int) -> Unit,
|
||||||
onRetry: () -> Unit
|
onRetry: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// Handle refresh state
|
||||||
|
LaunchedEffect(state) {
|
||||||
|
if (state !is ApiResult.Loading) {
|
||||||
|
isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
when (state) {
|
when (state) {
|
||||||
is ApiResult.Loading -> {
|
is ApiResult.Loading -> {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
if (!isRefreshing) {
|
||||||
CircularProgressIndicator()
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ApiResult.Success -> {
|
is ApiResult.Success -> {
|
||||||
@@ -35,17 +49,26 @@ fun DocumentsTabContent(
|
|||||||
message = if (isWarrantyTab) "No warranties found" else "No documents found"
|
message = if (isWarrantyTab) "No warranties found" else "No documents found"
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
PullToRefreshBox(
|
||||||
modifier = Modifier.fillMaxSize(),
|
isRefreshing = isRefreshing,
|
||||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 96.dp),
|
onRefresh = {
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
isRefreshing = true
|
||||||
|
onRetry()
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
items(documents) { document ->
|
LazyColumn(
|
||||||
DocumentCard(
|
modifier = Modifier.fillMaxSize(),
|
||||||
document = document,
|
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 96.dp),
|
||||||
isWarrantyCard = isWarrantyTab,
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
onClick = { document.id?.let { onDocumentClick(it) } }
|
) {
|
||||||
)
|
items(documents) { document ->
|
||||||
|
DocumentCard(
|
||||||
|
document = document,
|
||||||
|
isWarrantyCard = isWarrantyTab,
|
||||||
|
onClick = { document.id?.let { onDocumentClick(it) } }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
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.android.ui.theme.AppColors
|
|
||||||
import com.mycrib.shared.models.TaskDetail
|
import com.mycrib.shared.models.TaskDetail
|
||||||
import com.mycrib.shared.models.TaskCategory
|
import com.mycrib.shared.models.TaskCategory
|
||||||
import com.mycrib.shared.models.TaskPriority
|
import com.mycrib.shared.models.TaskPriority
|
||||||
@@ -28,7 +27,7 @@ fun TaskCard(
|
|||||||
task: TaskDetail,
|
task: TaskDetail,
|
||||||
buttonTypes: List<String> = emptyList(),
|
buttonTypes: List<String> = emptyList(),
|
||||||
onCompleteClick: (() -> Unit)?,
|
onCompleteClick: (() -> Unit)?,
|
||||||
onEditClick: () -> Unit,
|
onEditClick: (() -> Unit)?,
|
||||||
onCancelClick: (() -> Unit)?,
|
onCancelClick: (() -> Unit)?,
|
||||||
onUncancelClick: (() -> Unit)?,
|
onUncancelClick: (() -> Unit)?,
|
||||||
onMarkInProgressClick: (() -> Unit)? = null,
|
onMarkInProgressClick: (() -> Unit)? = null,
|
||||||
@@ -80,9 +79,9 @@ fun TaskCard(
|
|||||||
) {
|
) {
|
||||||
// Priority badge with semantic colors
|
// Priority badge with semantic colors
|
||||||
val priorityColor = when (task.priority.name.lowercase()) {
|
val priorityColor = when (task.priority.name.lowercase()) {
|
||||||
"urgent", "high" -> Color(0xFFEF4444) // Error/Red
|
"urgent", "high" -> MaterialTheme.colorScheme.error
|
||||||
"medium" -> Color(0xFFF59E0B) // Warning/Orange
|
"medium" -> MaterialTheme.colorScheme.tertiary
|
||||||
else -> Color(0xFF10B981) // Success/Green
|
else -> MaterialTheme.colorScheme.secondary
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -110,11 +109,11 @@ fun TaskCard(
|
|||||||
// Status badge with semantic colors
|
// Status badge with semantic colors
|
||||||
if (task.status != null) {
|
if (task.status != null) {
|
||||||
val statusColor = when (task.status.name.lowercase()) {
|
val statusColor = when (task.status.name.lowercase()) {
|
||||||
"completed" -> AppColors.taskCompleted
|
"completed" -> MaterialTheme.colorScheme.secondary
|
||||||
"in_progress" -> AppColors.taskInProgress
|
"in_progress" -> MaterialTheme.colorScheme.tertiary
|
||||||
"pending" -> Color(0xFFF59E0B)
|
"pending" -> MaterialTheme.colorScheme.tertiary
|
||||||
"cancelled" -> AppColors.taskCanceled
|
"cancelled" -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
else -> Color(0xFF9CA3AF)
|
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
}
|
}
|
||||||
Surface(
|
Surface(
|
||||||
color = statusColor.copy(alpha = 0.15f),
|
color = statusColor.copy(alpha = 0.15f),
|
||||||
@@ -228,85 +227,78 @@ fun TaskCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render buttons based on buttonTypes array
|
// Actions dropdown menu based on buttonTypes array
|
||||||
if (buttonTypes.isNotEmpty()) {
|
if (buttonTypes.isNotEmpty()) {
|
||||||
|
var showActionsMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
buttonTypes.forEach { buttonType ->
|
Box {
|
||||||
when (buttonType) {
|
Button(
|
||||||
"mark_in_progress" -> {
|
onClick = { showActionsMenu = true },
|
||||||
onMarkInProgressClick?.let {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
MarkInProgressButton(
|
colors = ButtonDefaults.buttonColors(
|
||||||
taskId = task.id,
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
onCompletion = it,
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
onError = { error -> println("Error: $error") },
|
),
|
||||||
modifier = Modifier.fillMaxWidth()
|
shape = RoundedCornerShape(8.dp)
|
||||||
)
|
) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Icon(
|
||||||
}
|
Icons.Default.MoreVert,
|
||||||
}
|
contentDescription = null,
|
||||||
"complete" -> {
|
modifier = Modifier.size(18.dp)
|
||||||
onCompleteClick?.let {
|
)
|
||||||
CompleteTaskButton(
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
taskId = task.id,
|
Text(
|
||||||
onCompletion = it,
|
text = "Actions",
|
||||||
onError = { error -> println("Error: $error") },
|
style = MaterialTheme.typography.labelLarge,
|
||||||
modifier = Modifier.fillMaxWidth()
|
fontWeight = FontWeight.SemiBold
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
}
|
||||||
}
|
|
||||||
}
|
DropdownMenu(
|
||||||
"edit" -> {
|
expanded = showActionsMenu,
|
||||||
EditTaskButton(
|
onDismissRequest = { showActionsMenu = false }
|
||||||
taskId = task.id,
|
) {
|
||||||
onCompletion = onEditClick,
|
// Primary actions
|
||||||
onError = { error -> println("Error: $error") },
|
buttonTypes.filter { isPrimaryAction(it) }.forEach { buttonType ->
|
||||||
modifier = Modifier.fillMaxWidth()
|
getActionMenuItem(
|
||||||
|
buttonType = buttonType,
|
||||||
|
task = task,
|
||||||
|
onMarkInProgressClick = onMarkInProgressClick,
|
||||||
|
onCompleteClick = onCompleteClick,
|
||||||
|
onEditClick = onEditClick,
|
||||||
|
onUncancelClick = onUncancelClick,
|
||||||
|
onUnarchiveClick = onUnarchiveClick,
|
||||||
|
onDismiss = { showActionsMenu = false }
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
}
|
||||||
"cancel" -> {
|
|
||||||
onCancelClick?.let {
|
// Secondary actions
|
||||||
CancelTaskButton(
|
if (buttonTypes.any { isSecondaryAction(it) }) {
|
||||||
taskId = task.id,
|
HorizontalDivider()
|
||||||
onCompletion = it,
|
buttonTypes.filter { isSecondaryAction(it) }.forEach { buttonType ->
|
||||||
onError = { error -> println("Error: $error") },
|
getActionMenuItem(
|
||||||
modifier = Modifier.fillMaxWidth()
|
buttonType = buttonType,
|
||||||
|
task = task,
|
||||||
|
onArchiveClick = onArchiveClick,
|
||||||
|
onDismiss = { showActionsMenu = false }
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"uncancel" -> {
|
|
||||||
onUncancelClick?.let {
|
// Destructive actions
|
||||||
UncancelTaskButton(
|
if (buttonTypes.any { isDestructiveAction(it) }) {
|
||||||
taskId = task.id,
|
HorizontalDivider()
|
||||||
onCompletion = it,
|
buttonTypes.filter { isDestructiveAction(it) }.forEach { buttonType ->
|
||||||
onError = { error -> println("Error: $error") },
|
getActionMenuItem(
|
||||||
modifier = Modifier.fillMaxWidth()
|
buttonType = buttonType,
|
||||||
|
task = task,
|
||||||
|
onCancelClick = onCancelClick,
|
||||||
|
onDismiss = { showActionsMenu = false }
|
||||||
)
|
)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,6 +308,138 @@ fun TaskCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper functions for action classification
|
||||||
|
private fun isPrimaryAction(buttonType: String): Boolean {
|
||||||
|
return buttonType in listOf("mark_in_progress", "complete", "edit", "uncancel", "unarchive")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isSecondaryAction(buttonType: String): Boolean {
|
||||||
|
return buttonType == "archive"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isDestructiveAction(buttonType: String): Boolean {
|
||||||
|
return buttonType == "cancel"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun getActionMenuItem(
|
||||||
|
buttonType: String,
|
||||||
|
task: TaskDetail,
|
||||||
|
onMarkInProgressClick: (() -> Unit)? = null,
|
||||||
|
onCompleteClick: (() -> Unit)? = null,
|
||||||
|
onEditClick: (() -> Unit)? = null,
|
||||||
|
onCancelClick: (() -> Unit)? = null,
|
||||||
|
onUncancelClick: (() -> Unit)? = null,
|
||||||
|
onArchiveClick: (() -> Unit)? = null,
|
||||||
|
onUnarchiveClick: (() -> Unit)? = null,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
when (buttonType) {
|
||||||
|
"mark_in_progress" -> {
|
||||||
|
onMarkInProgressClick?.let {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Mark In Progress") },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(Icons.Default.PlayArrow, contentDescription = null)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
it()
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"complete" -> {
|
||||||
|
onCompleteClick?.let {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Complete Task") },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(Icons.Default.CheckCircle, contentDescription = null)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
it()
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"edit" -> {
|
||||||
|
onEditClick?.let {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Edit Task") },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(Icons.Default.Edit, contentDescription = null)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
it()
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"cancel" -> {
|
||||||
|
onCancelClick?.let {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Cancel Task") },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Cancel,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
it()
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"uncancel" -> {
|
||||||
|
onUncancelClick?.let {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Restore Task") },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(Icons.Default.Undo, contentDescription = null)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
it()
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"archive" -> {
|
||||||
|
onArchiveClick?.let {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Archive Task") },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(Icons.Default.Archive, contentDescription = null)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
it()
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"unarchive" -> {
|
||||||
|
onUnarchiveClick?.let {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Unarchive Task") },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(Icons.Default.Unarchive, contentDescription = null)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
it()
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CompletionCard(completion: TaskCompletion) {
|
fun CompletionCard(completion: TaskCompletion) {
|
||||||
var showPhotoDialog by remember { mutableStateOf(false) }
|
var showPhotoDialog by remember { mutableStateOf(false) }
|
||||||
|
|||||||
@@ -241,29 +241,28 @@ fun DynamicTaskKanbanView(
|
|||||||
onUncancelTask: ((TaskDetail) -> Unit)?,
|
onUncancelTask: ((TaskDetail) -> Unit)?,
|
||||||
onMarkInProgress: ((TaskDetail) -> Unit)?,
|
onMarkInProgress: ((TaskDetail) -> Unit)?,
|
||||||
onArchiveTask: ((TaskDetail) -> Unit)?,
|
onArchiveTask: ((TaskDetail) -> Unit)?,
|
||||||
onUnarchiveTask: ((TaskDetail) -> Unit)?
|
onUnarchiveTask: ((TaskDetail) -> Unit)?,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val pagerState = rememberPagerState(pageCount = { columns.size })
|
val pagerState = rememberPagerState(pageCount = { columns.size })
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
HorizontalPager(
|
||||||
HorizontalPager(
|
state = pagerState,
|
||||||
state = pagerState,
|
modifier = modifier.fillMaxSize(),
|
||||||
modifier = Modifier.fillMaxSize(),
|
pageSpacing = 16.dp,
|
||||||
pageSpacing = 16.dp,
|
contentPadding = PaddingValues(start = 16.dp, end = 48.dp)
|
||||||
contentPadding = PaddingValues(start = 16.dp, end = 48.dp)
|
) { page ->
|
||||||
) { page ->
|
val column = columns[page]
|
||||||
val column = columns[page]
|
DynamicTaskColumn(
|
||||||
DynamicTaskColumn(
|
column = column,
|
||||||
column = column,
|
onCompleteTask = onCompleteTask,
|
||||||
onCompleteTask = onCompleteTask,
|
onEditTask = onEditTask,
|
||||||
onEditTask = onEditTask,
|
onCancelTask = onCancelTask,
|
||||||
onCancelTask = onCancelTask,
|
onUncancelTask = onUncancelTask,
|
||||||
onUncancelTask = onUncancelTask,
|
onMarkInProgress = onMarkInProgress,
|
||||||
onMarkInProgress = onMarkInProgress,
|
onArchiveTask = onArchiveTask,
|
||||||
onArchiveTask = onArchiveTask,
|
onUnarchiveTask = onUnarchiveTask
|
||||||
onUnarchiveTask = onUnarchiveTask
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,14 @@ fun AllTasksScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
|
IconButton(
|
||||||
|
onClick = { viewModel.loadTasks(forceRefresh = true) }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Refresh,
|
||||||
|
contentDescription = "Refresh"
|
||||||
|
)
|
||||||
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { showNewTaskDialog = true },
|
onClick = { showNewTaskDialog = true },
|
||||||
enabled = myResidencesState is ApiResult.Success &&
|
enabled = myResidencesState is ApiResult.Success &&
|
||||||
@@ -141,7 +149,7 @@ fun AllTasksScreen(
|
|||||||
style = MaterialTheme.typography.bodyLarge
|
style = MaterialTheme.typography.bodyLarge
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Button(onClick = { viewModel.loadTasks() }) {
|
Button(onClick = { viewModel.loadTasks(forceRefresh = true) }) {
|
||||||
Text("Retry")
|
Text("Retry")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,53 +219,48 @@ fun AllTasksScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Box(
|
DynamicTaskKanbanView(
|
||||||
modifier = Modifier
|
columns = taskData.columns,
|
||||||
.fillMaxSize()
|
onCompleteTask = { task ->
|
||||||
.padding(paddingValues)
|
selectedTask = task
|
||||||
) {
|
showCompleteDialog = true
|
||||||
DynamicTaskKanbanView(
|
},
|
||||||
columns = taskData.columns,
|
onEditTask = { task ->
|
||||||
onCompleteTask = { task ->
|
onNavigateToEditTask(task)
|
||||||
selectedTask = task
|
},
|
||||||
showCompleteDialog = true
|
onCancelTask = { task ->
|
||||||
},
|
|
||||||
onEditTask = { task ->
|
|
||||||
onNavigateToEditTask(task)
|
|
||||||
},
|
|
||||||
onCancelTask = { task ->
|
|
||||||
// viewModel.cancelTask(task.id) { _ ->
|
// viewModel.cancelTask(task.id) { _ ->
|
||||||
// viewModel.loadTasks()
|
// viewModel.loadTasks()
|
||||||
// }
|
// }
|
||||||
},
|
},
|
||||||
onUncancelTask = { task ->
|
onUncancelTask = { task ->
|
||||||
// viewModel.uncancelTask(task.id) { _ ->
|
// viewModel.uncancelTask(task.id) { _ ->
|
||||||
// viewModel.loadTasks()
|
// viewModel.loadTasks()
|
||||||
// }
|
// }
|
||||||
},
|
},
|
||||||
onMarkInProgress = { task ->
|
onMarkInProgress = { task ->
|
||||||
viewModel.markInProgress(task.id) { success ->
|
viewModel.markInProgress(task.id) { success ->
|
||||||
if (success) {
|
if (success) {
|
||||||
viewModel.loadTasks()
|
viewModel.loadTasks()
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onArchiveTask = { task ->
|
|
||||||
viewModel.archiveTask(task.id) { success ->
|
|
||||||
if (success) {
|
|
||||||
viewModel.loadTasks()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onUnarchiveTask = { task ->
|
|
||||||
viewModel.unarchiveTask(task.id) { success ->
|
|
||||||
if (success) {
|
|
||||||
viewModel.loadTasks()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
}
|
onArchiveTask = { task ->
|
||||||
|
viewModel.archiveTask(task.id) { success ->
|
||||||
|
if (success) {
|
||||||
|
viewModel.loadTasks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onUnarchiveTask = { task ->
|
||||||
|
viewModel.unarchiveTask(task.id) { success ->
|
||||||
|
if (success) {
|
||||||
|
viewModel.loadTasks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(paddingValues)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -42,11 +43,19 @@ fun ContractorsScreen(
|
|||||||
var showFavoritesOnly by remember { mutableStateOf(false) }
|
var showFavoritesOnly by remember { mutableStateOf(false) }
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
var showFiltersMenu by remember { mutableStateOf(false) }
|
var showFiltersMenu by remember { mutableStateOf(false) }
|
||||||
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.loadContractors()
|
viewModel.loadContractors()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle refresh state
|
||||||
|
LaunchedEffect(contractorsState) {
|
||||||
|
if (contractorsState !is ApiResult.Loading) {
|
||||||
|
isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(selectedFilter, showFavoritesOnly, searchQuery) {
|
LaunchedEffect(selectedFilter, showFavoritesOnly, searchQuery) {
|
||||||
viewModel.loadContractors(
|
viewModel.loadContractors(
|
||||||
specialty = selectedFilter,
|
specialty = selectedFilter,
|
||||||
@@ -83,7 +92,7 @@ fun ContractorsScreen(
|
|||||||
Icon(
|
Icon(
|
||||||
if (showFavoritesOnly) Icons.Default.Star else Icons.Default.StarOutline,
|
if (showFavoritesOnly) Icons.Default.Star else Icons.Default.StarOutline,
|
||||||
"Filter favorites",
|
"Filter favorites",
|
||||||
tint = if (showFavoritesOnly) Color(0xFFF59E0B) else LocalContentColor.current
|
tint = if (showFavoritesOnly) MaterialTheme.colorScheme.tertiary else LocalContentColor.current
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +102,7 @@ fun ContractorsScreen(
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.Default.FilterList,
|
Icons.Default.FilterList,
|
||||||
"Filter by specialty",
|
"Filter by specialty",
|
||||||
tint = if (selectedFilter != null) Color(0xFF3B82F6) else LocalContentColor.current
|
tint = if (selectedFilter != null) MaterialTheme.colorScheme.primary else LocalContentColor.current
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +118,7 @@ fun ContractorsScreen(
|
|||||||
},
|
},
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
if (selectedFilter == null) {
|
if (selectedFilter == null) {
|
||||||
Icon(Icons.Default.Check, null, tint = Color(0xFF10B981))
|
Icon(Icons.Default.Check, null, tint = MaterialTheme.colorScheme.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -123,7 +132,7 @@ fun ContractorsScreen(
|
|||||||
},
|
},
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
if (selectedFilter == specialty.name) {
|
if (selectedFilter == specialty.name) {
|
||||||
Icon(Icons.Default.Check, null, tint = Color(0xFF10B981))
|
Icon(Icons.Default.Check, null, tint = MaterialTheme.colorScheme.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -132,7 +141,7 @@ fun ContractorsScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = Color(0xFFF9FAFB)
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -140,8 +149,8 @@ fun ContractorsScreen(
|
|||||||
Box(modifier = Modifier.padding(bottom = 80.dp)) {
|
Box(modifier = Modifier.padding(bottom = 80.dp)) {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = { showAddDialog = true },
|
onClick = { showAddDialog = true },
|
||||||
containerColor = Color(0xFF2563EB),
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
contentColor = Color.White
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Add, "Add contractor")
|
Icon(Icons.Default.Add, "Add contractor")
|
||||||
}
|
}
|
||||||
@@ -152,7 +161,7 @@ fun ContractorsScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.background(Color(0xFFF9FAFB))
|
.background(MaterialTheme.colorScheme.background)
|
||||||
) {
|
) {
|
||||||
// Search bar
|
// Search bar
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
@@ -173,10 +182,10 @@ fun ContractorsScreen(
|
|||||||
singleLine = true,
|
singleLine = true,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
colors = OutlinedTextFieldDefaults.colors(
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
focusedContainerColor = Color.White,
|
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
unfocusedContainerColor = Color.White,
|
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
focusedBorderColor = Color(0xFF3B82F6),
|
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||||
unfocusedBorderColor = Color(0xFFE5E7EB)
|
unfocusedBorderColor = MaterialTheme.colorScheme.outline
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -209,11 +218,13 @@ fun ContractorsScreen(
|
|||||||
|
|
||||||
when (val state = contractorsState) {
|
when (val state = contractorsState) {
|
||||||
is ApiResult.Loading -> {
|
is ApiResult.Loading -> {
|
||||||
Box(
|
if (!isRefreshing) {
|
||||||
modifier = Modifier.fillMaxSize(),
|
Box(
|
||||||
contentAlignment = Alignment.Center
|
modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
contentAlignment = Alignment.Center
|
||||||
CircularProgressIndicator(color = Color(0xFF2563EB))
|
) {
|
||||||
|
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ApiResult.Success -> {
|
is ApiResult.Success -> {
|
||||||
@@ -232,36 +243,49 @@ fun ContractorsScreen(
|
|||||||
Icons.Default.PersonAdd,
|
Icons.Default.PersonAdd,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(64.dp),
|
modifier = Modifier.size(64.dp),
|
||||||
tint = Color(0xFF9CA3AF)
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
if (searchQuery.isNotEmpty() || selectedFilter != null || showFavoritesOnly)
|
if (searchQuery.isNotEmpty() || selectedFilter != null || showFavoritesOnly)
|
||||||
"No contractors found"
|
"No contractors found"
|
||||||
else
|
else
|
||||||
"No contractors yet",
|
"No contractors yet",
|
||||||
color = Color(0xFF6B7280)
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
if (searchQuery.isEmpty() && selectedFilter == null && !showFavoritesOnly) {
|
if (searchQuery.isEmpty() && selectedFilter == null && !showFavoritesOnly) {
|
||||||
Text(
|
Text(
|
||||||
"Add your first contractor to get started",
|
"Add your first contractor to get started",
|
||||||
color = Color(0xFF9CA3AF),
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
PullToRefreshBox(
|
||||||
modifier = Modifier.fillMaxSize(),
|
isRefreshing = isRefreshing,
|
||||||
contentPadding = PaddingValues(16.dp),
|
onRefresh = {
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
isRefreshing = true
|
||||||
) {
|
viewModel.loadContractors(
|
||||||
items(contractors, key = { it.id }) { contractor ->
|
specialty = selectedFilter,
|
||||||
ContractorCard(
|
isFavorite = if (showFavoritesOnly) true else null,
|
||||||
contractor = contractor,
|
search = searchQuery.takeIf { it.isNotBlank() }
|
||||||
onToggleFavorite = { viewModel.toggleFavorite(it) },
|
|
||||||
onClick = { onNavigateToContractorDetail(it) }
|
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
items(contractors, key = { it.id }) { contractor ->
|
||||||
|
ContractorCard(
|
||||||
|
contractor = contractor,
|
||||||
|
onToggleFavorite = { viewModel.toggleFavorite(it) },
|
||||||
|
onClick = { onNavigateToContractorDetail(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,13 +303,13 @@ fun ContractorsScreen(
|
|||||||
Icons.Default.ErrorOutline,
|
Icons.Default.ErrorOutline,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(64.dp),
|
modifier = Modifier.size(64.dp),
|
||||||
tint = Color(0xFFEF4444)
|
tint = MaterialTheme.colorScheme.error
|
||||||
)
|
)
|
||||||
Text(state.message, color = Color(0xFFEF4444))
|
Text(state.message, color = MaterialTheme.colorScheme.error)
|
||||||
Button(
|
Button(
|
||||||
onClick = { viewModel.loadContractors() },
|
onClick = { viewModel.loadContractors() },
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = Color(0xFF2563EB)
|
containerColor = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text("Retry")
|
Text("Retry")
|
||||||
@@ -321,7 +345,7 @@ fun ContractorCard(
|
|||||||
.clickable { onClick(contractor.id) },
|
.clickable { onClick(contractor.id) },
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = Color.White
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
),
|
),
|
||||||
elevation = CardDefaults.cardElevation(
|
elevation = CardDefaults.cardElevation(
|
||||||
defaultElevation = 1.dp
|
defaultElevation = 1.dp
|
||||||
@@ -338,14 +362,14 @@ fun ContractorCard(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(56.dp)
|
.size(56.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(Color(0xFFEEF2FF)),
|
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Person,
|
Icons.Default.Person,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(32.dp),
|
modifier = Modifier.size(32.dp),
|
||||||
tint = Color(0xFF3B82F6)
|
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +381,7 @@ fun ContractorCard(
|
|||||||
text = contractor.name,
|
text = contractor.name,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = Color(0xFF111827),
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
@@ -367,7 +391,7 @@ fun ContractorCard(
|
|||||||
Icons.Default.Star,
|
Icons.Default.Star,
|
||||||
contentDescription = "Favorite",
|
contentDescription = "Favorite",
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
tint = Color(0xFFF59E0B)
|
tint = MaterialTheme.colorScheme.tertiary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,7 +400,7 @@ fun ContractorCard(
|
|||||||
Text(
|
Text(
|
||||||
text = contractor.company,
|
text = contractor.company,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = Color(0xFF6B7280),
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
@@ -394,13 +418,13 @@ fun ContractorCard(
|
|||||||
Icons.Default.WorkOutline,
|
Icons.Default.WorkOutline,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(14.dp),
|
modifier = Modifier.size(14.dp),
|
||||||
tint = Color(0xFF6B7280)
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = contractor.specialty,
|
text = contractor.specialty,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = Color(0xFF6B7280)
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -411,13 +435,13 @@ fun ContractorCard(
|
|||||||
Icons.Default.Star,
|
Icons.Default.Star,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(14.dp),
|
modifier = Modifier.size(14.dp),
|
||||||
tint = Color(0xFFF59E0B)
|
tint = MaterialTheme.colorScheme.tertiary
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "${(contractor.averageRating * 10).toInt() / 10.0}",
|
text = "${(contractor.averageRating * 10).toInt() / 10.0}",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = Color(0xFF6B7280),
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
fontWeight = FontWeight.Medium
|
fontWeight = FontWeight.Medium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -429,13 +453,13 @@ fun ContractorCard(
|
|||||||
Icons.Default.CheckCircle,
|
Icons.Default.CheckCircle,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(14.dp),
|
modifier = Modifier.size(14.dp),
|
||||||
tint = Color(0xFF10B981)
|
tint = MaterialTheme.colorScheme.secondary
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "${contractor.taskCount} tasks",
|
text = "${contractor.taskCount} tasks",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = Color(0xFF6B7280)
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -449,7 +473,7 @@ fun ContractorCard(
|
|||||||
Icon(
|
Icon(
|
||||||
if (contractor.isFavorite) Icons.Default.Star else Icons.Default.StarOutline,
|
if (contractor.isFavorite) Icons.Default.Star else Icons.Default.StarOutline,
|
||||||
contentDescription = if (contractor.isFavorite) "Remove from favorites" else "Add to favorites",
|
contentDescription = if (contractor.isFavorite) "Remove from favorites" else "Add to favorites",
|
||||||
tint = if (contractor.isFavorite) Color(0xFFF59E0B) else Color(0xFF9CA3AF)
|
tint = if (contractor.isFavorite) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,7 +481,7 @@ fun ContractorCard(
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.Default.ChevronRight,
|
Icons.Default.ChevronRight,
|
||||||
contentDescription = "View details",
|
contentDescription = "View details",
|
||||||
tint = Color(0xFF9CA3AF)
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import androidx.compose.ui.graphics.Brush
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.mycrib.android.ui.theme.AppColors
|
|
||||||
import com.mycrib.android.ui.theme.AppRadius
|
import com.mycrib.android.ui.theme.AppRadius
|
||||||
import com.mycrib.android.viewmodel.ResidenceViewModel
|
import com.mycrib.android.viewmodel.ResidenceViewModel
|
||||||
import com.mycrib.shared.network.ApiResult
|
import com.mycrib.shared.network.ApiResult
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -36,11 +37,19 @@ fun ResidencesScreen(
|
|||||||
) {
|
) {
|
||||||
val myResidencesState by viewModel.myResidencesState.collectAsState()
|
val myResidencesState by viewModel.myResidencesState.collectAsState()
|
||||||
var showJoinDialog by remember { mutableStateOf(false) }
|
var showJoinDialog by remember { mutableStateOf(false) }
|
||||||
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.loadMyResidences()
|
viewModel.loadMyResidences()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle refresh state
|
||||||
|
LaunchedEffect(myResidencesState) {
|
||||||
|
if (myResidencesState !is ApiResult.Loading) {
|
||||||
|
isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (showJoinDialog) {
|
if (showJoinDialog) {
|
||||||
JoinResidenceDialog(
|
JoinResidenceDialog(
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
@@ -219,18 +228,26 @@ fun ResidencesScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
PullToRefreshBox(
|
||||||
|
isRefreshing = isRefreshing,
|
||||||
|
onRefresh = {
|
||||||
|
isRefreshing = true
|
||||||
|
viewModel.loadMyResidences()
|
||||||
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues),
|
.padding(paddingValues)
|
||||||
contentPadding = PaddingValues(
|
|
||||||
start = 16.dp,
|
|
||||||
end = 16.dp,
|
|
||||||
top = 16.dp,
|
|
||||||
bottom = 96.dp
|
|
||||||
),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
start = 16.dp,
|
||||||
|
end = 16.dp,
|
||||||
|
top = 16.dp,
|
||||||
|
bottom = 96.dp
|
||||||
|
),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
// Summary Card
|
// Summary Card
|
||||||
item {
|
item {
|
||||||
Card(
|
Card(
|
||||||
@@ -341,20 +358,13 @@ fun ResidencesScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(56.dp)
|
.size(56.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(
|
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||||
Brush.linearGradient(
|
|
||||||
listOf(
|
|
||||||
Color(0xFF2563EB),
|
|
||||||
Color(0xFF8B5CF6)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Home,
|
Icons.Default.Home,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Color.White,
|
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
modifier = Modifier.size(28.dp)
|
modifier = Modifier.size(28.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -403,24 +413,25 @@ fun ResidencesScreen(
|
|||||||
icon = Icons.Default.Assignment,
|
icon = Icons.Default.Assignment,
|
||||||
value = "${residence.taskSummary.total}",
|
value = "${residence.taskSummary.total}",
|
||||||
label = "Tasks",
|
label = "Tasks",
|
||||||
color = Color(0xFF3B82F6)
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
TaskStatChip(
|
TaskStatChip(
|
||||||
icon = Icons.Default.CheckCircle,
|
icon = Icons.Default.CheckCircle,
|
||||||
value = "${residence.taskSummary.completed}",
|
value = "${residence.taskSummary.completed}",
|
||||||
label = "Done",
|
label = "Done",
|
||||||
color = Color(0xFF10B981)
|
color = MaterialTheme.colorScheme.secondary
|
||||||
)
|
)
|
||||||
TaskStatChip(
|
TaskStatChip(
|
||||||
icon = Icons.Default.Schedule,
|
icon = Icons.Default.Schedule,
|
||||||
value = "${residence.taskSummary.pending}",
|
value = "${residence.taskSummary.pending}",
|
||||||
label = "Pending",
|
label = "Pending",
|
||||||
color = Color(0xFFF59E0B)
|
color = MaterialTheme.colorScheme.tertiary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,9 +135,13 @@ fun TasksScreen(
|
|||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize(),
|
||||||
.padding(paddingValues),
|
contentPadding = PaddingValues(
|
||||||
contentPadding = PaddingValues(16.dp),
|
top = paddingValues.calculateTopPadding() + 16.dp,
|
||||||
|
bottom = paddingValues.calculateBottomPadding() + 16.dp,
|
||||||
|
start = 16.dp,
|
||||||
|
end = 16.dp
|
||||||
|
),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
// Task summary pills - dynamically generated from all columns
|
// Task summary pills - dynamically generated from all columns
|
||||||
|
|||||||
@@ -125,15 +125,3 @@ fun MyCribTheme(
|
|||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extended colors for semantic states (not part of Material3 ColorScheme)
|
|
||||||
object AppColors {
|
|
||||||
val accent = Accent
|
|
||||||
val accentLight = AccentLight
|
|
||||||
val info = Info
|
|
||||||
val taskUpcoming = TaskUpcoming
|
|
||||||
val taskInProgress = TaskInProgress
|
|
||||||
val taskCompleted = TaskCompleted
|
|
||||||
val taskCanceled = TaskCanceled
|
|
||||||
val taskArchived = TaskArchived
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "app-icon-1024.png",
|
"filename" : "icon.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"platform" : "ios",
|
"platform" : "ios",
|
||||||
"size" : "1024x1024"
|
"size" : "1024x1024"
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB |
BIN
iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/icon.png
Normal file
BIN
iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Reference in New Issue
Block a user