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:
Trey t
2025-11-13 22:21:54 -06:00
parent ff390a85c2
commit 29c136d612
12 changed files with 413 additions and 238 deletions

View File

@@ -7,13 +7,16 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Description
import androidx.compose.material.icons.filled.ReceiptLong
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.Modifier
import androidx.compose.ui.unit.dp
import com.mycrib.shared.models.DocumentListResponse
import com.mycrib.shared.network.ApiResult
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DocumentsTabContent(
state: ApiResult<DocumentListResponse>,
@@ -21,10 +24,21 @@ fun DocumentsTabContent(
onDocumentClick: (Int) -> Unit,
onRetry: () -> Unit
) {
var isRefreshing by remember { mutableStateOf(false) }
// Handle refresh state
LaunchedEffect(state) {
if (state !is ApiResult.Loading) {
isRefreshing = false
}
}
when (state) {
is ApiResult.Loading -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
if (!isRefreshing) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
}
is ApiResult.Success -> {
@@ -35,17 +49,26 @@ fun DocumentsTabContent(
message = if (isWarrantyTab) "No warranties found" else "No documents found"
)
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 96.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = {
isRefreshing = true
onRetry()
},
modifier = Modifier.fillMaxSize()
) {
items(documents) { document ->
DocumentCard(
document = document,
isWarrantyCard = isWarrantyTab,
onClick = { document.id?.let { onDocumentClick(it) } }
)
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 96.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(documents) { document ->
DocumentCard(
document = document,
isWarrantyCard = isWarrantyTab,
onClick = { document.id?.let { onDocumentClick(it) } }
)
}
}
}
}

View File

@@ -14,7 +14,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.mycrib.android.ui.theme.AppColors
import com.mycrib.shared.models.TaskDetail
import com.mycrib.shared.models.TaskCategory
import com.mycrib.shared.models.TaskPriority
@@ -28,7 +27,7 @@ fun TaskCard(
task: TaskDetail,
buttonTypes: List<String> = emptyList(),
onCompleteClick: (() -> Unit)?,
onEditClick: () -> Unit,
onEditClick: (() -> Unit)?,
onCancelClick: (() -> Unit)?,
onUncancelClick: (() -> Unit)?,
onMarkInProgressClick: (() -> Unit)? = null,
@@ -80,9 +79,9 @@ fun TaskCard(
) {
// Priority badge with semantic colors
val priorityColor = when (task.priority.name.lowercase()) {
"urgent", "high" -> Color(0xFFEF4444) // Error/Red
"medium" -> Color(0xFFF59E0B) // Warning/Orange
else -> Color(0xFF10B981) // Success/Green
"urgent", "high" -> MaterialTheme.colorScheme.error
"medium" -> MaterialTheme.colorScheme.tertiary
else -> MaterialTheme.colorScheme.secondary
}
Row(
modifier = Modifier
@@ -110,11 +109,11 @@ fun TaskCard(
// Status badge with semantic colors
if (task.status != null) {
val statusColor = when (task.status.name.lowercase()) {
"completed" -> AppColors.taskCompleted
"in_progress" -> AppColors.taskInProgress
"pending" -> Color(0xFFF59E0B)
"cancelled" -> AppColors.taskCanceled
else -> Color(0xFF9CA3AF)
"completed" -> MaterialTheme.colorScheme.secondary
"in_progress" -> MaterialTheme.colorScheme.tertiary
"pending" -> MaterialTheme.colorScheme.tertiary
"cancelled" -> MaterialTheme.colorScheme.onSurfaceVariant
else -> MaterialTheme.colorScheme.onSurfaceVariant
}
Surface(
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()) {
var showActionsMenu by remember { mutableStateOf(false) }
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
Spacer(modifier = Modifier.height(16.dp))
buttonTypes.forEach { buttonType ->
when (buttonType) {
"mark_in_progress" -> {
onMarkInProgressClick?.let {
MarkInProgressButton(
taskId = task.id,
onCompletion = it,
onError = { error -> println("Error: $error") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
}
}
"complete" -> {
onCompleteClick?.let {
CompleteTaskButton(
taskId = task.id,
onCompletion = it,
onError = { error -> println("Error: $error") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
}
}
"edit" -> {
EditTaskButton(
taskId = task.id,
onCompletion = onEditClick,
onError = { error -> println("Error: $error") },
modifier = Modifier.fillMaxWidth()
Box {
Button(
onClick = { showActionsMenu = true },
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
),
shape = RoundedCornerShape(8.dp)
) {
Icon(
Icons.Default.MoreVert,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Actions",
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold
)
}
DropdownMenu(
expanded = showActionsMenu,
onDismissRequest = { showActionsMenu = false }
) {
// Primary actions
buttonTypes.filter { isPrimaryAction(it) }.forEach { buttonType ->
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 {
CancelTaskButton(
taskId = task.id,
onCompletion = it,
onError = { error -> println("Error: $error") },
modifier = Modifier.fillMaxWidth()
// Secondary actions
if (buttonTypes.any { isSecondaryAction(it) }) {
HorizontalDivider()
buttonTypes.filter { isSecondaryAction(it) }.forEach { buttonType ->
getActionMenuItem(
buttonType = buttonType,
task = task,
onArchiveClick = onArchiveClick,
onDismiss = { showActionsMenu = false }
)
Spacer(modifier = Modifier.height(8.dp))
}
}
"uncancel" -> {
onUncancelClick?.let {
UncancelTaskButton(
taskId = task.id,
onCompletion = it,
onError = { error -> println("Error: $error") },
modifier = Modifier.fillMaxWidth()
// Destructive actions
if (buttonTypes.any { isDestructiveAction(it) }) {
HorizontalDivider()
buttonTypes.filter { isDestructiveAction(it) }.forEach { buttonType ->
getActionMenuItem(
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
fun CompletionCard(completion: TaskCompletion) {
var showPhotoDialog by remember { mutableStateOf(false) }

View File

@@ -241,29 +241,28 @@ fun DynamicTaskKanbanView(
onUncancelTask: ((TaskDetail) -> Unit)?,
onMarkInProgress: ((TaskDetail) -> Unit)?,
onArchiveTask: ((TaskDetail) -> Unit)?,
onUnarchiveTask: ((TaskDetail) -> Unit)?
onUnarchiveTask: ((TaskDetail) -> Unit)?,
modifier: Modifier = Modifier
) {
val pagerState = rememberPagerState(pageCount = { columns.size })
Column(modifier = Modifier.fillMaxSize()) {
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize(),
pageSpacing = 16.dp,
contentPadding = PaddingValues(start = 16.dp, end = 48.dp)
) { page ->
val column = columns[page]
DynamicTaskColumn(
column = column,
onCompleteTask = onCompleteTask,
onEditTask = onEditTask,
onCancelTask = onCancelTask,
onUncancelTask = onUncancelTask,
onMarkInProgress = onMarkInProgress,
onArchiveTask = onArchiveTask,
onUnarchiveTask = onUnarchiveTask
)
}
HorizontalPager(
state = pagerState,
modifier = modifier.fillMaxSize(),
pageSpacing = 16.dp,
contentPadding = PaddingValues(start = 16.dp, end = 48.dp)
) { page ->
val column = columns[page]
DynamicTaskColumn(
column = column,
onCompleteTask = onCompleteTask,
onEditTask = onEditTask,
onCancelTask = onCancelTask,
onUncancelTask = onUncancelTask,
onMarkInProgress = onMarkInProgress,
onArchiveTask = onArchiveTask,
onUnarchiveTask = onUnarchiveTask
)
}
}

View File

@@ -90,6 +90,14 @@ fun AllTasksScreen(
)
},
actions = {
IconButton(
onClick = { viewModel.loadTasks(forceRefresh = true) }
) {
Icon(
Icons.Default.Refresh,
contentDescription = "Refresh"
)
}
IconButton(
onClick = { showNewTaskDialog = true },
enabled = myResidencesState is ApiResult.Success &&
@@ -141,7 +149,7 @@ fun AllTasksScreen(
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { viewModel.loadTasks() }) {
Button(onClick = { viewModel.loadTasks(forceRefresh = true) }) {
Text("Retry")
}
}
@@ -211,53 +219,48 @@ fun AllTasksScreen(
}
}
} else {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
DynamicTaskKanbanView(
columns = taskData.columns,
onCompleteTask = { task ->
selectedTask = task
showCompleteDialog = true
},
onEditTask = { task ->
onNavigateToEditTask(task)
},
onCancelTask = { task ->
DynamicTaskKanbanView(
columns = taskData.columns,
onCompleteTask = { task ->
selectedTask = task
showCompleteDialog = true
},
onEditTask = { task ->
onNavigateToEditTask(task)
},
onCancelTask = { task ->
// viewModel.cancelTask(task.id) { _ ->
// viewModel.loadTasks()
// }
},
onUncancelTask = { task ->
},
onUncancelTask = { task ->
// viewModel.uncancelTask(task.id) { _ ->
// viewModel.loadTasks()
// }
},
onMarkInProgress = { task ->
viewModel.markInProgress(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()
}
},
onMarkInProgress = { task ->
viewModel.markInProgress(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)
)
}
}

View File

@@ -10,6 +10,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -42,11 +43,19 @@ fun ContractorsScreen(
var showFavoritesOnly by remember { mutableStateOf(false) }
var searchQuery by remember { mutableStateOf("") }
var showFiltersMenu by remember { mutableStateOf(false) }
var isRefreshing by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
viewModel.loadContractors()
}
// Handle refresh state
LaunchedEffect(contractorsState) {
if (contractorsState !is ApiResult.Loading) {
isRefreshing = false
}
}
LaunchedEffect(selectedFilter, showFavoritesOnly, searchQuery) {
viewModel.loadContractors(
specialty = selectedFilter,
@@ -83,7 +92,7 @@ fun ContractorsScreen(
Icon(
if (showFavoritesOnly) Icons.Default.Star else Icons.Default.StarOutline,
"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(
Icons.Default.FilterList,
"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 = {
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 = {
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(
containerColor = Color(0xFFF9FAFB)
containerColor = MaterialTheme.colorScheme.surface
)
)
},
@@ -140,8 +149,8 @@ fun ContractorsScreen(
Box(modifier = Modifier.padding(bottom = 80.dp)) {
FloatingActionButton(
onClick = { showAddDialog = true },
containerColor = Color(0xFF2563EB),
contentColor = Color.White
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
) {
Icon(Icons.Default.Add, "Add contractor")
}
@@ -152,7 +161,7 @@ fun ContractorsScreen(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.background(Color(0xFFF9FAFB))
.background(MaterialTheme.colorScheme.background)
) {
// Search bar
OutlinedTextField(
@@ -173,10 +182,10 @@ fun ContractorsScreen(
singleLine = true,
shape = RoundedCornerShape(12.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = Color.White,
unfocusedContainerColor = Color.White,
focusedBorderColor = Color(0xFF3B82F6),
unfocusedBorderColor = Color(0xFFE5E7EB)
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline
)
)
@@ -209,11 +218,13 @@ fun ContractorsScreen(
when (val state = contractorsState) {
is ApiResult.Loading -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(color = Color(0xFF2563EB))
if (!isRefreshing) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
}
}
}
is ApiResult.Success -> {
@@ -232,36 +243,49 @@ fun ContractorsScreen(
Icons.Default.PersonAdd,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = Color(0xFF9CA3AF)
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
if (searchQuery.isNotEmpty() || selectedFilter != null || showFavoritesOnly)
"No contractors found"
else
"No contractors yet",
color = Color(0xFF6B7280)
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (searchQuery.isEmpty() && selectedFilter == null && !showFavoritesOnly) {
Text(
"Add your first contractor to get started",
color = Color(0xFF9CA3AF),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall
)
}
}
}
} else {
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) }
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = {
isRefreshing = true
viewModel.loadContractors(
specialty = selectedFilter,
isFavorite = if (showFavoritesOnly) true else null,
search = searchQuery.takeIf { it.isNotBlank() }
)
},
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,
contentDescription = null,
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(
onClick = { viewModel.loadContractors() },
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF2563EB)
containerColor = MaterialTheme.colorScheme.primary
)
) {
Text("Retry")
@@ -321,7 +345,7 @@ fun ContractorCard(
.clickable { onClick(contractor.id) },
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(
defaultElevation = 1.dp
@@ -338,14 +362,14 @@ fun ContractorCard(
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
.background(Color(0xFFEEF2FF)),
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Person,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = Color(0xFF3B82F6)
tint = MaterialTheme.colorScheme.onPrimaryContainer
)
}
@@ -357,7 +381,7 @@ fun ContractorCard(
text = contractor.name,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = Color(0xFF111827),
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@@ -367,7 +391,7 @@ fun ContractorCard(
Icons.Default.Star,
contentDescription = "Favorite",
modifier = Modifier.size(16.dp),
tint = Color(0xFFF59E0B)
tint = MaterialTheme.colorScheme.tertiary
)
}
}
@@ -376,7 +400,7 @@ fun ContractorCard(
Text(
text = contractor.company,
style = MaterialTheme.typography.bodyMedium,
color = Color(0xFF6B7280),
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@@ -394,13 +418,13 @@ fun ContractorCard(
Icons.Default.WorkOutline,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = Color(0xFF6B7280)
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = contractor.specialty,
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF6B7280)
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@@ -411,13 +435,13 @@ fun ContractorCard(
Icons.Default.Star,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = Color(0xFFF59E0B)
tint = MaterialTheme.colorScheme.tertiary
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "${(contractor.averageRating * 10).toInt() / 10.0}",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF6B7280),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.Medium
)
}
@@ -429,13 +453,13 @@ fun ContractorCard(
Icons.Default.CheckCircle,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = Color(0xFF10B981)
tint = MaterialTheme.colorScheme.secondary
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "${contractor.taskCount} tasks",
style = MaterialTheme.typography.bodySmall,
color = Color(0xFF6B7280)
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@@ -449,7 +473,7 @@ fun ContractorCard(
Icon(
if (contractor.isFavorite) Icons.Default.Star else Icons.Default.StarOutline,
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(
Icons.Default.ChevronRight,
contentDescription = "View details",
tint = Color(0xFF9CA3AF)
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}

View File

@@ -15,7 +15,6 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.mycrib.android.ui.theme.AppColors
import com.mycrib.android.ui.theme.AppRadius
import com.mycrib.android.viewmodel.ResidenceViewModel
import com.mycrib.shared.network.ApiResult

View File

@@ -10,6 +10,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -36,11 +37,19 @@ fun ResidencesScreen(
) {
val myResidencesState by viewModel.myResidencesState.collectAsState()
var showJoinDialog by remember { mutableStateOf(false) }
var isRefreshing by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
viewModel.loadMyResidences()
}
// Handle refresh state
LaunchedEffect(myResidencesState) {
if (myResidencesState !is ApiResult.Loading) {
isRefreshing = false
}
}
if (showJoinDialog) {
JoinResidenceDialog(
onDismiss = {
@@ -219,18 +228,26 @@ fun ResidencesScreen(
}
}
} else {
LazyColumn(
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = {
isRefreshing = true
viewModel.loadMyResidences()
},
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentPadding = PaddingValues(
start = 16.dp,
end = 16.dp,
top = 16.dp,
bottom = 96.dp
),
verticalArrangement = Arrangement.spacedBy(16.dp)
.padding(paddingValues)
) {
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
item {
Card(
@@ -341,20 +358,13 @@ fun ResidencesScreen(
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
.background(
Brush.linearGradient(
listOf(
Color(0xFF2563EB),
Color(0xFF8B5CF6)
)
)
),
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Home,
contentDescription = null,
tint = Color.White,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.size(28.dp)
)
}
@@ -403,24 +413,25 @@ fun ResidencesScreen(
icon = Icons.Default.Assignment,
value = "${residence.taskSummary.total}",
label = "Tasks",
color = Color(0xFF3B82F6)
color = MaterialTheme.colorScheme.primary
)
TaskStatChip(
icon = Icons.Default.CheckCircle,
value = "${residence.taskSummary.completed}",
label = "Done",
color = Color(0xFF10B981)
color = MaterialTheme.colorScheme.secondary
)
TaskStatChip(
icon = Icons.Default.Schedule,
value = "${residence.taskSummary.pending}",
label = "Pending",
color = Color(0xFFF59E0B)
color = MaterialTheme.colorScheme.tertiary
)
}
}
}
}
}
}
}
}

View File

@@ -135,9 +135,13 @@ fun TasksScreen(
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentPadding = PaddingValues(16.dp),
.fillMaxSize(),
contentPadding = PaddingValues(
top = paddingValues.calculateTopPadding() + 16.dp,
bottom = paddingValues.calculateBottomPadding() + 16.dp,
start = 16.dp,
end = 16.dp
),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// Task summary pills - dynamically generated from all columns

View File

@@ -125,15 +125,3 @@ fun MyCribTheme(
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
}