Add contractors section to residence detail and fix search filtering

- Add GET /contractors/by-residence/:id endpoint integration
- Display contractors on residence detail screen (iOS & Android)
- Fix contractor search/filter to use client-side filtering
- Backend doesn't support search query params, so filter locally

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-01 20:38:57 -06:00
parent fe2e8275f5
commit e62e7d4371
9 changed files with 295 additions and 44 deletions

View File

@@ -66,12 +66,18 @@ fun ContractorsScreen(
}
}
LaunchedEffect(selectedFilter, showFavoritesOnly, searchQuery) {
viewModel.loadContractors(
specialty = selectedFilter,
isFavorite = if (showFavoritesOnly) true else null,
search = searchQuery.takeIf { it.isNotBlank() }
)
// Client-side filtering since backend doesn't support search/filter params
val filteredContractors = remember(contractorsState, searchQuery, selectedFilter, showFavoritesOnly) {
val contractors = (contractorsState as? ApiResult.Success)?.data ?: emptyList()
contractors.filter { contractor ->
val matchesSearch = searchQuery.isBlank() ||
contractor.name.contains(searchQuery, ignoreCase = true) ||
(contractor.company?.contains(searchQuery, ignoreCase = true) == true)
val matchesSpecialty = selectedFilter == null ||
contractor.specialties.any { it.name == selectedFilter }
val matchesFavorite = !showFavoritesOnly || contractor.isFavorite
matchesSearch && matchesSpecialty && matchesFavorite
}
}
// Handle errors for delete contractor
@@ -95,11 +101,7 @@ fun ContractorsScreen(
LaunchedEffect(toggleFavoriteState) {
if (toggleFavoriteState is ApiResult.Success) {
viewModel.loadContractors(
specialty = selectedFilter,
isFavorite = if (showFavoritesOnly) true else null,
search = searchQuery.takeIf { it.isNotBlank() }
)
viewModel.loadContractors()
viewModel.resetToggleFavoriteState()
}
}
@@ -266,11 +268,7 @@ fun ContractorsScreen(
ApiResultHandler(
state = contractorsState,
onRetry = {
viewModel.loadContractors(
specialty = selectedFilter,
isFavorite = if (showFavoritesOnly) true else null,
search = searchQuery.takeIf { it.isNotBlank() }
)
viewModel.loadContractors()
},
errorTitle = "Failed to Load Contractors",
loadingContent = {
@@ -278,8 +276,9 @@ fun ContractorsScreen(
CircularProgressIndicator(color = MaterialTheme.colorScheme.primary)
}
}
) { contractors ->
if (contractors.isEmpty()) {
) { _ ->
// Use filteredContractors for client-side filtering
if (filteredContractors.isEmpty()) {
// Empty state
Box(
modifier = Modifier.fillMaxSize(),
@@ -316,11 +315,7 @@ fun ContractorsScreen(
isRefreshing = isRefreshing,
onRefresh = {
isRefreshing = true
viewModel.loadContractors(
specialty = selectedFilter,
isFavorite = if (showFavoritesOnly) true else null,
search = searchQuery.takeIf { it.isNotBlank() }
)
viewModel.loadContractors()
},
modifier = Modifier.fillMaxSize()
) {
@@ -329,7 +324,7 @@ fun ContractorsScreen(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(contractors, key = { it.id }) { contractor ->
items(filteredContractors, key = { it.id }) { contractor ->
ContractorCard(
contractor = contractor,
onToggleFavorite = { viewModel.toggleFavorite(it) },

View File

@@ -29,6 +29,7 @@ import com.example.casera.viewmodel.TaskCompletionViewModel
import com.example.casera.viewmodel.TaskViewModel
import com.example.casera.models.Residence
import com.example.casera.models.TaskDetail
import com.example.casera.models.ContractorSummary
import com.example.casera.network.ApiResult
import com.example.casera.utils.SubscriptionHelper
import com.example.casera.ui.subscription.UpgradePromptDialog
@@ -43,12 +44,14 @@ fun ResidenceDetailScreen(
onNavigateBack: () -> Unit,
onNavigateToEditResidence: (Residence) -> Unit,
onNavigateToEditTask: (TaskDetail) -> Unit,
onNavigateToContractorDetail: (Int) -> Unit = {},
residenceViewModel: ResidenceViewModel = viewModel { ResidenceViewModel() },
taskCompletionViewModel: TaskCompletionViewModel = viewModel { TaskCompletionViewModel() },
taskViewModel: TaskViewModel = viewModel { TaskViewModel() }
) {
var residenceState by remember { mutableStateOf<ApiResult<Residence>>(ApiResult.Loading) }
val tasksState by residenceViewModel.residenceTasksState.collectAsState()
val contractorsState by residenceViewModel.residenceContractorsState.collectAsState()
val completionState by taskCompletionViewModel.createCompletionState.collectAsState()
val taskAddNewTaskState by taskViewModel.taskAddNewCustomTaskState.collectAsState()
val cancelTaskState by residenceViewModel.cancelTaskState.collectAsState()
@@ -90,6 +93,7 @@ fun ResidenceDetailScreen(
residenceState = result
}
residenceViewModel.loadResidenceTasks(residenceId)
residenceViewModel.loadResidenceContractors(residenceId)
}
// Handle completion success
@@ -745,6 +749,112 @@ fun ResidenceDetailScreen(
else -> {}
}
// Contractors Section Header
item {
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Default.People,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(28.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Contractors",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
}
}
when (contractorsState) {
is ApiResult.Idle, is ApiResult.Loading -> {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.height(80.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
}
is ApiResult.Error -> {
item {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
),
shape = RoundedCornerShape(12.dp)
) {
Text(
text = "Error loading contractors: ${com.example.casera.util.ErrorMessageParser.parse((contractorsState as ApiResult.Error).message)}",
color = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.padding(16.dp)
)
}
}
}
is ApiResult.Success -> {
val contractors = (contractorsState as ApiResult.Success<List<ContractorSummary>>).data
if (contractors.isEmpty()) {
item {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
Icons.Default.PersonAdd,
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
)
Spacer(modifier = Modifier.height(12.dp))
Text(
"No contractors yet",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Text(
"Add contractors from the Contractors tab",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
} else {
items(contractors, key = { it.id }) { contractor ->
ContractorCard(
contractor = contractor,
onToggleFavorite = { /* TODO: Implement in ResidenceViewModel if needed */ },
onClick = { onNavigateToContractorDetail(contractor.id) }
)
}
}
}
else -> {}
}
// Bottom spacing for FAB
item {
Spacer(modifier = Modifier.height(80.dp))
}
}
}
}