Make contractor phone optional and add UI test accessibility identifiers

Updated contractor models and forms to make phone field optional. Added
accessibility identifiers for add buttons to enable UI testing.

Contractor changes:
- Kotlin: Made phone nullable in Contractor, ContractorCreateRequest,
  ContractorSummary models
- Android: Updated AddContractorDialog validation to only require name
- Android: Removed asterisk from phone field label
- Android: Updated ContractorDetailScreen to handle nullable phone
- iOS: Updated ContractorFormSheet validation to only check name field
- iOS: Updated form footer text to show only name as required
- iOS: Updated ContractorDetailView to use optional binding for phone display

Accessibility improvements:
- iOS: Added accessibility identifier to contractor add button in
  ContractorsListView
- iOS: Added accessibility identifier to task add button in
  ResidenceDetailView

These identifiers enable reliable UI testing by allowing tests to access
buttons by their accessibility identifiers instead of searching by label.

🤖 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-20 23:04:06 -06:00
parent dd5e050025
commit 1100bc423d
7 changed files with 33 additions and 20 deletions

View File

@@ -8,7 +8,7 @@ data class Contractor(
val id: Int, val id: Int,
val name: String, val name: String,
val company: String? = null, val company: String? = null,
val phone: String, val phone: String? = null,
val email: String? = null, val email: String? = null,
@SerialName("secondary_phone") val secondaryPhone: String? = null, @SerialName("secondary_phone") val secondaryPhone: String? = null,
val specialty: String? = null, val specialty: String? = null,
@@ -33,7 +33,7 @@ data class Contractor(
data class ContractorCreateRequest( data class ContractorCreateRequest(
val name: String, val name: String,
val company: String? = null, val company: String? = null,
val phone: String, val phone: String? = null,
val email: String? = null, val email: String? = null,
@SerialName("secondary_phone") val secondaryPhone: String? = null, @SerialName("secondary_phone") val secondaryPhone: String? = null,
val specialty: String? = null, val specialty: String? = null,
@@ -72,7 +72,7 @@ data class ContractorSummary(
val id: Int, val id: Int,
val name: String, val name: String,
val company: String? = null, val company: String? = null,
val phone: String, val phone: String? = null,
val specialty: String? = null, val specialty: String? = null,
@SerialName("average_rating") val averageRating: Double? = null, @SerialName("average_rating") val averageRating: Double? = null,
@SerialName("is_favorite") val isFavorite: Boolean = false, @SerialName("is_favorite") val isFavorite: Boolean = false,

View File

@@ -62,7 +62,7 @@ fun AddContractorDialog(
val contractor = (contractorDetailState as ApiResult.Success).data val contractor = (contractorDetailState as ApiResult.Success).data
name = contractor.name name = contractor.name
company = contractor.company ?: "" company = contractor.company ?: ""
phone = contractor.phone phone = contractor.phone ?: ""
email = contractor.email ?: "" email = contractor.email ?: ""
secondaryPhone = contractor.secondaryPhone ?: "" secondaryPhone = contractor.secondaryPhone ?: ""
specialty = contractor.specialty ?: "" specialty = contractor.specialty ?: ""
@@ -157,7 +157,7 @@ fun AddContractorDialog(
OutlinedTextField( OutlinedTextField(
value = phone, value = phone,
onValueChange = { phone = it }, onValueChange = { phone = it },
label = { Text("Phone *") }, label = { Text("Phone") },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
@@ -402,13 +402,13 @@ fun AddContractorDialog(
confirmButton = { confirmButton = {
Button( Button(
onClick = { onClick = {
if (name.isNotBlank() && phone.isNotBlank()) { if (name.isNotBlank()) {
if (contractorId == null) { if (contractorId == null) {
viewModel.createContractor( viewModel.createContractor(
ContractorCreateRequest( ContractorCreateRequest(
name = name, name = name,
company = company.takeIf { it.isNotBlank() }, company = company.takeIf { it.isNotBlank() },
phone = phone, phone = phone.takeIf { it.isNotBlank() },
email = email.takeIf { it.isNotBlank() }, email = email.takeIf { it.isNotBlank() },
secondaryPhone = secondaryPhone.takeIf { it.isNotBlank() }, secondaryPhone = secondaryPhone.takeIf { it.isNotBlank() },
specialty = specialty.takeIf { it.isNotBlank() }, specialty = specialty.takeIf { it.isNotBlank() },
@@ -445,7 +445,7 @@ fun AddContractorDialog(
} }
} }
}, },
enabled = name.isNotBlank() && phone.isNotBlank() && enabled = name.isNotBlank() &&
createState !is ApiResult.Loading && updateState !is ApiResult.Loading, createState !is ApiResult.Loading && updateState !is ApiResult.Loading,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF2563EB) containerColor = Color(0xFF2563EB)

View File

@@ -231,12 +231,14 @@ fun ContractorDetailScreen(
// Contact Information // Contact Information
item { item {
DetailSection(title = "Contact Information") { DetailSection(title = "Contact Information") {
if (contractor.phone != null) {
DetailRow( DetailRow(
icon = Icons.Default.Phone, icon = Icons.Default.Phone,
label = "Phone", label = "Phone",
value = contractor.phone, value = contractor.phone,
iconTint = Color(0xFF3B82F6) iconTint = Color(0xFF3B82F6)
) )
}
if (contractor.email != null) { if (contractor.email != null) {
DetailRow( DetailRow(

View File

@@ -92,7 +92,9 @@ struct ContractorDetailView: View {
// Contact Information // Contact Information
DetailSection(title: "Contact Information") { DetailSection(title: "Contact Information") {
DetailRow(icon: "phone", label: "Phone", value: contractor.phone, iconColor: .blue) if let phone = contractor.phone {
DetailRow(icon: "phone", label: "Phone", value: phone, iconColor: .blue)
}
if let email = contractor.email { if let email = contractor.email {
DetailRow(icon: "envelope", label: "Email", value: email, iconColor: .purple) DetailRow(icon: "envelope", label: "Email", value: email, iconColor: .purple)

View File

@@ -42,7 +42,7 @@ struct ContractorFormSheet: View {
} }
private var canSave: Bool { private var canSave: Bool {
!name.isEmpty && !phone.isEmpty !name.isEmpty
} }
var body: some View { var body: some View {
@@ -67,6 +67,10 @@ struct ContractorFormSheet: View {
} }
} header: { } header: {
Text("Basic Information") Text("Basic Information")
} footer: {
Text("Required: Name")
.font(.caption)
.foregroundColor(.red)
} }
// Contact Information // Contact Information
@@ -102,8 +106,7 @@ struct ContractorFormSheet: View {
} header: { } header: {
Text("Contact Information") Text("Contact Information")
} footer: { } footer: {
Text("Required: Name and Phone")
.font(.caption)
} }
// Business Details // Business Details
@@ -295,7 +298,7 @@ struct ContractorFormSheet: View {
name = contractor.name name = contractor.name
company = contractor.company ?? "" company = contractor.company ?? ""
phone = contractor.phone phone = contractor.phone ?? ""
email = contractor.email ?? "" email = contractor.email ?? ""
secondaryPhone = contractor.secondaryPhone ?? "" secondaryPhone = contractor.secondaryPhone ?? ""
specialty = contractor.specialty ?? "" specialty = contractor.specialty ?? ""
@@ -353,7 +356,7 @@ struct ContractorFormSheet: View {
let request = ContractorCreateRequest( let request = ContractorCreateRequest(
name: name, name: name,
company: company.isEmpty ? nil : company, company: company.isEmpty ? nil : company,
phone: phone, phone: phone.isEmpty ? nil : phone,
email: email.isEmpty ? nil : email, email: email.isEmpty ? nil : email,
secondaryPhone: secondaryPhone.isEmpty ? nil : secondaryPhone, secondaryPhone: secondaryPhone.isEmpty ? nil : secondaryPhone,
specialty: specialty.isEmpty ? nil : specialty, specialty: specialty.isEmpty ? nil : specialty,

View File

@@ -148,6 +148,7 @@ struct ContractorsListView: View {
.font(.title2) .font(.title2)
.foregroundColor(.blue) .foregroundColor(.blue)
} }
.accessibilityIdentifier(AccessibilityIdentifiers.Contractor.addButton)
} }
} }
} }

View File

@@ -56,9 +56,11 @@ struct ResidenceDetailView: View {
.alert("Delete Residence", isPresented: $showDeleteConfirmation) { .alert("Delete Residence", isPresented: $showDeleteConfirmation) {
Button("Cancel", role: .cancel) { } Button("Cancel", role: .cancel) { }
.accessibilityIdentifier(AccessibilityIdentifiers.Alert.cancelButton)
Button("Delete", role: .destructive) { Button("Delete", role: .destructive) {
deleteResidence() deleteResidence()
} }
.accessibilityIdentifier(AccessibilityIdentifiers.Alert.deleteButton)
} message: { } message: {
if let residence = viewModel.selectedResidence { if let residence = viewModel.selectedResidence {
Text("Are you sure you want to delete \(residence.name)? This action cannot be undone and will delete all associated tasks, documents, and data.") Text("Are you sure you want to delete \(residence.name)? This action cannot be undone and will delete all associated tasks, documents, and data.")
@@ -223,6 +225,7 @@ private extension ResidenceDetailView {
Button("Edit") { Button("Edit") {
showEditResidence = true showEditResidence = true
} }
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.editButton)
} }
} }
} }
@@ -256,6 +259,7 @@ private extension ResidenceDetailView {
} label: { } label: {
Image(systemName: "plus") Image(systemName: "plus")
} }
.accessibilityIdentifier(AccessibilityIdentifiers.Task.addButton)
if let residence = viewModel.selectedResidence, residence.isPrimaryOwner { if let residence = viewModel.selectedResidence, residence.isPrimaryOwner {
Button { Button {
@@ -264,6 +268,7 @@ private extension ResidenceDetailView {
Image(systemName: "trash") Image(systemName: "trash")
.foregroundStyle(.red) .foregroundStyle(.red)
} }
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.deleteButton)
} }
} }
} }