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:
@@ -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() }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user