Add residence picker to contractor create/edit screens
Kotlin/KMM: - Update Contractor model with optional residenceId and specialties array - Rename averageRating to rating, update address field names - Add ContractorMinimal model for task references - Add residence picker and multi-select specialty chips to AddContractorDialog - Fix ContractorsScreen and ContractorDetailScreen field references iOS: - Rewrite ContractorFormSheet with residence and specialty pickers - Update ContractorDetailView with FlowLayout for specialties - Add FlowLayout component for wrapping badge layouts - Fix ContractorCard and CompleteTaskView field references - Update ContractorFormState with residence/specialty selection 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,28 +3,36 @@ package com.example.casera.models
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ContractorUser(
|
||||
val id: Int,
|
||||
val username: String,
|
||||
@SerialName("first_name") val firstName: String? = null,
|
||||
@SerialName("last_name") val lastName: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Contractor(
|
||||
val id: Int,
|
||||
@SerialName("residence_id") val residenceId: Int? = null,
|
||||
@SerialName("created_by_id") val createdById: Int,
|
||||
@SerialName("added_by") val addedBy: Int,
|
||||
@SerialName("created_by") val createdBy: ContractorUser? = null,
|
||||
val name: String,
|
||||
val company: String? = null,
|
||||
val phone: String? = null,
|
||||
val email: String? = null,
|
||||
@SerialName("secondary_phone") val secondaryPhone: String? = null,
|
||||
val specialty: String? = null,
|
||||
@SerialName("license_number") val licenseNumber: String? = null,
|
||||
val website: String? = null,
|
||||
val address: String? = null,
|
||||
val notes: String? = null,
|
||||
@SerialName("street_address") val streetAddress: String? = null,
|
||||
val city: String? = null,
|
||||
val state: String? = null,
|
||||
@SerialName("zip_code") val zipCode: String? = null,
|
||||
@SerialName("added_by") val addedBy: Int,
|
||||
@SerialName("average_rating") val averageRating: Double? = null,
|
||||
@SerialName("state_province") val stateProvince: String? = null,
|
||||
@SerialName("postal_code") val postalCode: String? = null,
|
||||
val specialties: List<ContractorSpecialty> = emptyList(),
|
||||
val rating: Double? = null,
|
||||
@SerialName("is_favorite") val isFavorite: Boolean = false,
|
||||
@SerialName("is_active") val isActive: Boolean = true,
|
||||
val notes: String? = null,
|
||||
@SerialName("task_count") val taskCount: Int = 0,
|
||||
@SerialName("last_used") val lastUsed: String? = null,
|
||||
@SerialName("created_at") val createdAt: String,
|
||||
@SerialName("updated_at") val updatedAt: String
|
||||
)
|
||||
@@ -32,70 +40,51 @@ data class Contractor(
|
||||
@Serializable
|
||||
data class ContractorCreateRequest(
|
||||
val name: String,
|
||||
@SerialName("residence_id") val residenceId: Int? = null,
|
||||
val company: String? = null,
|
||||
val phone: String? = null,
|
||||
val email: String? = null,
|
||||
@SerialName("secondary_phone") val secondaryPhone: String? = null,
|
||||
val specialty: String? = null,
|
||||
@SerialName("license_number") val licenseNumber: String? = null,
|
||||
val website: String? = null,
|
||||
val address: String? = null,
|
||||
@SerialName("street_address") val streetAddress: String? = null,
|
||||
val city: String? = null,
|
||||
val state: String? = null,
|
||||
@SerialName("zip_code") val zipCode: String? = null,
|
||||
@SerialName("state_province") val stateProvince: String? = null,
|
||||
@SerialName("postal_code") val postalCode: String? = null,
|
||||
val rating: Double? = null,
|
||||
@SerialName("is_favorite") val isFavorite: Boolean = false,
|
||||
@SerialName("is_active") val isActive: Boolean = true,
|
||||
val notes: String? = null
|
||||
val notes: String? = null,
|
||||
@SerialName("specialty_ids") val specialtyIds: List<Int>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ContractorUpdateRequest(
|
||||
val name: String? = null,
|
||||
@SerialName("residence_id") val residenceId: Int? = null,
|
||||
val company: String? = null,
|
||||
val phone: String? = null,
|
||||
val email: String? = null,
|
||||
@SerialName("secondary_phone") val secondaryPhone: String? = null,
|
||||
val specialty: String? = null,
|
||||
@SerialName("license_number") val licenseNumber: String? = null,
|
||||
val website: String? = null,
|
||||
val address: String? = null,
|
||||
@SerialName("street_address") val streetAddress: String? = null,
|
||||
val city: String? = null,
|
||||
val state: String? = null,
|
||||
@SerialName("zip_code") val zipCode: String? = null,
|
||||
@SerialName("state_province") val stateProvince: String? = null,
|
||||
@SerialName("postal_code") val postalCode: String? = null,
|
||||
val rating: Double? = null,
|
||||
@SerialName("is_favorite") val isFavorite: Boolean? = null,
|
||||
@SerialName("is_active") val isActive: Boolean? = null,
|
||||
val notes: String? = null
|
||||
val notes: String? = null,
|
||||
@SerialName("specialty_ids") val specialtyIds: List<Int>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ContractorSummary(
|
||||
val id: Int,
|
||||
@SerialName("residence_id") val residenceId: Int? = null,
|
||||
val name: String,
|
||||
val company: String? = null,
|
||||
val phone: String? = null,
|
||||
val specialty: String? = null,
|
||||
@SerialName("average_rating") val averageRating: Double? = null,
|
||||
val specialties: List<ContractorSpecialty> = emptyList(),
|
||||
val rating: Double? = null,
|
||||
@SerialName("is_favorite") val isFavorite: Boolean = false,
|
||||
@SerialName("task_count") val taskCount: Int = 0
|
||||
)
|
||||
|
||||
/**
|
||||
* Minimal contractor model for list views.
|
||||
* Uses specialty_id instead of nested specialty object.
|
||||
* Resolve via DataCache.getContractorSpecialty(contractor.specialtyId)
|
||||
*/
|
||||
@Serializable
|
||||
data class ContractorMinimal(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val company: String? = null,
|
||||
val phone: String? = null,
|
||||
@SerialName("specialty_id") val specialtyId: Int? = null,
|
||||
@SerialName("average_rating") val averageRating: Double? = null,
|
||||
@SerialName("is_favorite") val isFavorite: Boolean = false,
|
||||
@SerialName("task_count") val taskCount: Int = 0,
|
||||
@SerialName("last_used") val lastUsed: String? = null
|
||||
)
|
||||
|
||||
// Removed: ContractorListResponse - no longer using paginated responses
|
||||
// API now returns List<ContractorMinimal> directly from list endpoint
|
||||
// Note: API returns full Contractor objects for list endpoints
|
||||
// ContractorSummary kept for backward compatibility
|
||||
|
||||
@@ -79,7 +79,20 @@ data class TaskCategory(
|
||||
@Serializable
|
||||
data class ContractorSpecialty(
|
||||
val id: Int,
|
||||
val name: String
|
||||
val name: String,
|
||||
val description: String? = null,
|
||||
val icon: String? = null,
|
||||
@SerialName("display_order") val displayOrder: Int = 0
|
||||
)
|
||||
|
||||
/**
|
||||
* Minimal contractor info for task references
|
||||
*/
|
||||
@Serializable
|
||||
data class ContractorMinimal(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val company: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,8 +14,10 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.casera.viewmodel.ContractorViewModel
|
||||
import com.example.casera.viewmodel.ResidenceViewModel
|
||||
import com.example.casera.models.ContractorCreateRequest
|
||||
import com.example.casera.models.ContractorUpdateRequest
|
||||
import com.example.casera.models.Residence
|
||||
import com.example.casera.network.ApiResult
|
||||
import com.example.casera.repository.LookupsRepository
|
||||
|
||||
@@ -25,30 +27,36 @@ fun AddContractorDialog(
|
||||
contractorId: Int? = null,
|
||||
onDismiss: () -> Unit,
|
||||
onContractorSaved: () -> Unit,
|
||||
viewModel: ContractorViewModel = viewModel { ContractorViewModel() }
|
||||
viewModel: ContractorViewModel = viewModel { ContractorViewModel() },
|
||||
residenceViewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }
|
||||
) {
|
||||
val createState by viewModel.createState.collectAsState()
|
||||
val updateState by viewModel.updateState.collectAsState()
|
||||
val contractorDetailState by viewModel.contractorDetailState.collectAsState()
|
||||
val residencesState by residenceViewModel.residencesState.collectAsState()
|
||||
|
||||
var name by remember { mutableStateOf("") }
|
||||
var company by remember { mutableStateOf("") }
|
||||
var phone by remember { mutableStateOf("") }
|
||||
var email by remember { mutableStateOf("") }
|
||||
var secondaryPhone by remember { mutableStateOf("") }
|
||||
var specialty by remember { mutableStateOf("") }
|
||||
var licenseNumber by remember { mutableStateOf("") }
|
||||
var website by remember { mutableStateOf("") }
|
||||
var address by remember { mutableStateOf("") }
|
||||
var streetAddress by remember { mutableStateOf("") }
|
||||
var city by remember { mutableStateOf("") }
|
||||
var state by remember { mutableStateOf("") }
|
||||
var zipCode by remember { mutableStateOf("") }
|
||||
var stateProvince by remember { mutableStateOf("") }
|
||||
var postalCode by remember { mutableStateOf("") }
|
||||
var notes by remember { mutableStateOf("") }
|
||||
var isFavorite by remember { mutableStateOf(false) }
|
||||
var selectedResidence by remember { mutableStateOf<Residence?>(null) }
|
||||
var selectedSpecialtyIds by remember { mutableStateOf<List<Int>>(emptyList()) }
|
||||
|
||||
var expandedSpecialtyMenu by remember { mutableStateOf(false) }
|
||||
var expandedResidenceMenu by remember { mutableStateOf(false) }
|
||||
val contractorSpecialties by LookupsRepository.contractorSpecialties.collectAsState()
|
||||
val specialties = contractorSpecialties.map { it.name }
|
||||
|
||||
// Load residences for picker
|
||||
LaunchedEffect(Unit) {
|
||||
residenceViewModel.loadResidences()
|
||||
}
|
||||
|
||||
// Load existing contractor data if editing
|
||||
LaunchedEffect(contractorId) {
|
||||
@@ -57,23 +65,27 @@ fun AddContractorDialog(
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(contractorDetailState) {
|
||||
LaunchedEffect(contractorDetailState, residencesState) {
|
||||
if (contractorDetailState is ApiResult.Success) {
|
||||
val contractor = (contractorDetailState as ApiResult.Success).data
|
||||
name = contractor.name
|
||||
company = contractor.company ?: ""
|
||||
phone = contractor.phone ?: ""
|
||||
email = contractor.email ?: ""
|
||||
secondaryPhone = contractor.secondaryPhone ?: ""
|
||||
specialty = contractor.specialty ?: ""
|
||||
licenseNumber = contractor.licenseNumber ?: ""
|
||||
website = contractor.website ?: ""
|
||||
address = contractor.address ?: ""
|
||||
streetAddress = contractor.streetAddress ?: ""
|
||||
city = contractor.city ?: ""
|
||||
state = contractor.state ?: ""
|
||||
zipCode = contractor.zipCode ?: ""
|
||||
stateProvince = contractor.stateProvince ?: ""
|
||||
postalCode = contractor.postalCode ?: ""
|
||||
notes = contractor.notes ?: ""
|
||||
isFavorite = contractor.isFavorite
|
||||
selectedSpecialtyIds = contractor.specialties.map { it.id }
|
||||
|
||||
// Set selected residence if contractor has one
|
||||
if (contractor.residenceId != null && residencesState is ApiResult.Success) {
|
||||
val residences = (residencesState as ApiResult.Success<List<Residence>>).data
|
||||
selectedResidence = residences.find { it.id == contractor.residenceId }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +156,64 @@ fun AddContractorDialog(
|
||||
)
|
||||
)
|
||||
|
||||
// Residence Picker (Optional)
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expandedResidenceMenu,
|
||||
onExpandedChange = { expandedResidenceMenu = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = selectedResidence?.name ?: "Personal (No Residence)",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Residence (Optional)") },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedResidenceMenu) },
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
leadingIcon = { Icon(Icons.Default.Home, null) },
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = Color(0xFF3B82F6),
|
||||
unfocusedBorderColor = Color(0xFFE5E7EB)
|
||||
)
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = expandedResidenceMenu,
|
||||
onDismissRequest = { expandedResidenceMenu = false }
|
||||
) {
|
||||
// Option for no residence (personal contractor)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Personal (No Residence)") },
|
||||
onClick = {
|
||||
selectedResidence = null
|
||||
expandedResidenceMenu = false
|
||||
}
|
||||
)
|
||||
|
||||
// List residences if loaded
|
||||
if (residencesState is ApiResult.Success) {
|
||||
val residences = (residencesState as ApiResult.Success<List<Residence>>).data
|
||||
residences.forEach { residence ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(residence.name) },
|
||||
onClick = {
|
||||
selectedResidence = residence
|
||||
expandedResidenceMenu = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
if (selectedResidence == null) "Only you will see this contractor"
|
||||
else "All users of ${selectedResidence?.name} will see this contractor",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color(0xFF6B7280)
|
||||
)
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
|
||||
// Contact Information Section
|
||||
@@ -182,81 +252,6 @@ fun AddContractorDialog(
|
||||
)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = secondaryPhone,
|
||||
onValueChange = { secondaryPhone = it },
|
||||
label = { Text("Secondary Phone") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
leadingIcon = { Icon(Icons.Default.Phone, null) },
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = Color(0xFF3B82F6),
|
||||
unfocusedBorderColor = Color(0xFFE5E7EB)
|
||||
)
|
||||
)
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
|
||||
// Business Details Section
|
||||
Text(
|
||||
"Business Details",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFF111827)
|
||||
)
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expandedSpecialtyMenu,
|
||||
onExpandedChange = { expandedSpecialtyMenu = it }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = specialty,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Specialty") },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedSpecialtyMenu) },
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
leadingIcon = { Icon(Icons.Default.WorkOutline, null) },
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = Color(0xFF3B82F6),
|
||||
unfocusedBorderColor = Color(0xFFE5E7EB)
|
||||
)
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = expandedSpecialtyMenu,
|
||||
onDismissRequest = { expandedSpecialtyMenu = false }
|
||||
) {
|
||||
specialties.forEach { option ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(option) },
|
||||
onClick = {
|
||||
specialty = option
|
||||
expandedSpecialtyMenu = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = licenseNumber,
|
||||
onValueChange = { licenseNumber = it },
|
||||
label = { Text("License Number") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
leadingIcon = { Icon(Icons.Default.Badge, null) },
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = Color(0xFF3B82F6),
|
||||
unfocusedBorderColor = Color(0xFFE5E7EB)
|
||||
)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = website,
|
||||
onValueChange = { website = it },
|
||||
@@ -273,6 +268,40 @@ fun AddContractorDialog(
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
|
||||
// Specialties Section
|
||||
Text(
|
||||
"Specialties",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFF111827)
|
||||
)
|
||||
|
||||
// Multi-select specialties using chips
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
contractorSpecialties.forEach { specialty ->
|
||||
FilterChip(
|
||||
selected = selectedSpecialtyIds.contains(specialty.id),
|
||||
onClick = {
|
||||
selectedSpecialtyIds = if (selectedSpecialtyIds.contains(specialty.id)) {
|
||||
selectedSpecialtyIds - specialty.id
|
||||
} else {
|
||||
selectedSpecialtyIds + specialty.id
|
||||
}
|
||||
},
|
||||
label = { Text(specialty.name) },
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
selectedContainerColor = Color(0xFF3B82F6),
|
||||
selectedLabelColor = Color.White
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
|
||||
// Address Section
|
||||
Text(
|
||||
"Address",
|
||||
@@ -282,8 +311,8 @@ fun AddContractorDialog(
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = address,
|
||||
onValueChange = { address = it },
|
||||
value = streetAddress,
|
||||
onValueChange = { streetAddress = it },
|
||||
label = { Text("Street Address") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
@@ -310,8 +339,8 @@ fun AddContractorDialog(
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = state,
|
||||
onValueChange = { state = it },
|
||||
value = stateProvince,
|
||||
onValueChange = { stateProvince = it },
|
||||
label = { Text("State") },
|
||||
modifier = Modifier.weight(0.5f),
|
||||
singleLine = true,
|
||||
@@ -324,8 +353,8 @@ fun AddContractorDialog(
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = zipCode,
|
||||
onValueChange = { zipCode = it },
|
||||
value = postalCode,
|
||||
onValueChange = { postalCode = it },
|
||||
label = { Text("ZIP Code") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
@@ -407,19 +436,18 @@ fun AddContractorDialog(
|
||||
viewModel.createContractor(
|
||||
ContractorCreateRequest(
|
||||
name = name,
|
||||
residenceId = selectedResidence?.id,
|
||||
company = company.takeIf { it.isNotBlank() },
|
||||
phone = phone.takeIf { it.isNotBlank() },
|
||||
email = email.takeIf { it.isNotBlank() },
|
||||
secondaryPhone = secondaryPhone.takeIf { it.isNotBlank() },
|
||||
specialty = specialty.takeIf { it.isNotBlank() },
|
||||
licenseNumber = licenseNumber.takeIf { it.isNotBlank() },
|
||||
website = website.takeIf { it.isNotBlank() },
|
||||
address = address.takeIf { it.isNotBlank() },
|
||||
streetAddress = streetAddress.takeIf { it.isNotBlank() },
|
||||
city = city.takeIf { it.isNotBlank() },
|
||||
state = state.takeIf { it.isNotBlank() },
|
||||
zipCode = zipCode.takeIf { it.isNotBlank() },
|
||||
stateProvince = stateProvince.takeIf { it.isNotBlank() },
|
||||
postalCode = postalCode.takeIf { it.isNotBlank() },
|
||||
isFavorite = isFavorite,
|
||||
notes = notes.takeIf { it.isNotBlank() }
|
||||
notes = notes.takeIf { it.isNotBlank() },
|
||||
specialtyIds = selectedSpecialtyIds.takeIf { it.isNotEmpty() }
|
||||
)
|
||||
)
|
||||
} else {
|
||||
@@ -427,19 +455,18 @@ fun AddContractorDialog(
|
||||
contractorId,
|
||||
ContractorUpdateRequest(
|
||||
name = name,
|
||||
residenceId = selectedResidence?.id,
|
||||
company = company.takeIf { it.isNotBlank() },
|
||||
phone = phone,
|
||||
phone = phone.takeIf { it.isNotBlank() },
|
||||
email = email.takeIf { it.isNotBlank() },
|
||||
secondaryPhone = secondaryPhone.takeIf { it.isNotBlank() },
|
||||
specialty = specialty.takeIf { it.isNotBlank() },
|
||||
licenseNumber = licenseNumber.takeIf { it.isNotBlank() },
|
||||
website = website.takeIf { it.isNotBlank() },
|
||||
address = address.takeIf { it.isNotBlank() },
|
||||
streetAddress = streetAddress.takeIf { it.isNotBlank() },
|
||||
city = city.takeIf { it.isNotBlank() },
|
||||
state = state.takeIf { it.isNotBlank() },
|
||||
zipCode = zipCode.takeIf { it.isNotBlank() },
|
||||
stateProvince = stateProvince.takeIf { it.isNotBlank() },
|
||||
postalCode = postalCode.takeIf { it.isNotBlank() },
|
||||
isFavorite = isFavorite,
|
||||
notes = notes.takeIf { it.isNotBlank() }
|
||||
notes = notes.takeIf { it.isNotBlank() },
|
||||
specialtyIds = selectedSpecialtyIds.takeIf { it.isNotEmpty() }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -168,39 +168,45 @@ fun ContractorDetailScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (contractor.specialty != null) {
|
||||
if (contractor.specialties.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Surface(
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = Color(0xFFEEF2FF)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.WorkOutline,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = Color(0xFF3B82F6)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = contractor.specialty,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color(0xFF3B82F6),
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
contractor.specialties.forEach { specialty ->
|
||||
Surface(
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = Color(0xFFEEF2FF)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.WorkOutline,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = Color(0xFF3B82F6)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = specialty.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color(0xFF3B82F6),
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (contractor.averageRating != null && contractor.averageRating > 0) {
|
||||
if (contractor.rating != null && contractor.rating > 0) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
repeat(5) { index ->
|
||||
Icon(
|
||||
if (index < contractor.averageRating.toInt()) Icons.Default.Star else Icons.Default.StarOutline,
|
||||
if (index < contractor.rating.toInt()) Icons.Default.Star else Icons.Default.StarOutline,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = Color(0xFFF59E0B)
|
||||
@@ -208,7 +214,7 @@ fun ContractorDetailScreen(
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "${(contractor.averageRating * 10).toInt() / 10.0}",
|
||||
text = "${(contractor.rating * 10).toInt() / 10.0}",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(0xFF111827)
|
||||
@@ -249,15 +255,6 @@ fun ContractorDetailScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (contractor.secondaryPhone != null) {
|
||||
DetailRow(
|
||||
icon = Icons.Default.Phone,
|
||||
label = "Secondary Phone",
|
||||
value = contractor.secondaryPhone,
|
||||
iconTint = Color(0xFF10B981)
|
||||
)
|
||||
}
|
||||
|
||||
if (contractor.website != null) {
|
||||
DetailRow(
|
||||
icon = Icons.Default.Language,
|
||||
@@ -269,36 +266,20 @@ fun ContractorDetailScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Business Details
|
||||
if (contractor.licenseNumber != null || contractor.specialty != null) {
|
||||
item {
|
||||
DetailSection(title = "Business Details") {
|
||||
if (contractor.licenseNumber != null) {
|
||||
DetailRow(
|
||||
icon = Icons.Default.Badge,
|
||||
label = "License Number",
|
||||
value = contractor.licenseNumber,
|
||||
iconTint = Color(0xFF3B82F6)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Address
|
||||
if (contractor.address != null || contractor.city != null) {
|
||||
if (contractor.streetAddress != null || contractor.city != null) {
|
||||
item {
|
||||
DetailSection(title = "Address") {
|
||||
val fullAddress = buildString {
|
||||
contractor.address?.let { append(it) }
|
||||
if (contractor.city != null || contractor.state != null || contractor.zipCode != null) {
|
||||
contractor.streetAddress?.let { append(it) }
|
||||
if (contractor.city != null || contractor.stateProvince != null || contractor.postalCode != null) {
|
||||
if (isNotEmpty()) append("\n")
|
||||
contractor.city?.let { append(it) }
|
||||
contractor.state?.let {
|
||||
contractor.stateProvince?.let {
|
||||
if (contractor.city != null) append(", ")
|
||||
append(it)
|
||||
}
|
||||
contractor.zipCode?.let {
|
||||
contractor.postalCode?.let {
|
||||
append(" ")
|
||||
append(it)
|
||||
}
|
||||
|
||||
@@ -450,7 +450,7 @@ fun ContractorCard(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (contractor.specialty != null) {
|
||||
if (contractor.specialties.isNotEmpty()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
Icons.Default.WorkOutline,
|
||||
@@ -460,14 +460,14 @@ fun ContractorCard(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = contractor.specialty,
|
||||
text = contractor.specialties.first().name,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (contractor.averageRating != null && contractor.averageRating > 0) {
|
||||
if (contractor.rating != null && contractor.rating > 0) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
Icons.Default.Star,
|
||||
@@ -477,7 +477,7 @@ fun ContractorCard(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = "${(contractor.averageRating * 10).toInt() / 10.0}",
|
||||
text = "${(contractor.rating * 10).toInt() / 10.0}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontWeight = FontWeight.Medium
|
||||
|
||||
Reference in New Issue
Block a user