diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentsTabContent.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentsTabContent.kt index 1be0cd9..50e6d0f 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentsTabContent.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/documents/DocumentsTabContent.kt @@ -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, @@ -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) } } + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskCard.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskCard.kt index 8cca8d5..bc46f63 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskCard.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskCard.kt @@ -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 = 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) } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskKanbanView.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskKanbanView.kt index 021fea2..45048c2 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskKanbanView.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/components/task/TaskKanbanView.kt @@ -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 + ) } } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/AllTasksScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/AllTasksScreen.kt index 4638ca5..765f27d 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/AllTasksScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/AllTasksScreen.kt @@ -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) + ) } } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ContractorsScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ContractorsScreen.kt index 584b7fe..4b3bd0f 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ContractorsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ContractorsScreen.kt @@ -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 ) } } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/HomeScreen.kt index 00bdfdf..9f9eacd 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/HomeScreen.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidencesScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidencesScreen.kt index e21a0a8..0ec255c 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidencesScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidencesScreen.kt @@ -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 ) } } } } + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/TasksScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/TasksScreen.kt index d3a3948..2609e44 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/TasksScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/TasksScreen.kt @@ -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 diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/theme/Theme.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/theme/Theme.kt index dd23c44..79e3f8b 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/theme/Theme.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/theme/Theme.kt @@ -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 -} diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json index 4e8d485..c68da6c 100644 --- a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "app-icon-1024.png", + "filename" : "icon.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png deleted file mode 100644 index 53fc536..0000000 Binary files a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png and /dev/null differ diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/icon.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/icon.png new file mode 100644 index 0000000..b1e6f70 Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/icon.png differ