Make residence address fields optional (only name required)
Updated Kotlin models, Android UI, and iOS UI to make all address fields optional for residences. Only the residence name is now required. Changes: - Kotlin: Made propertyType, streetAddress, city, stateProvince, postalCode, country nullable in Residence, ResidenceSummary, ResidenceWithTasks models - Kotlin: Updated navigation routes to handle nullable address fields - Android: Updated ResidenceFormScreen and ResidenceDetailScreen to handle nulls - iOS: Updated ResidenceFormView validation to only check name field - iOS: Updated PropertyHeaderCard and ResidenceCard to use optional binding for address field displays 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -286,7 +286,7 @@ fun App(
|
|||||||
EditResidenceRoute(
|
EditResidenceRoute(
|
||||||
residenceId = residence.id,
|
residenceId = residence.id,
|
||||||
name = residence.name,
|
name = residence.name,
|
||||||
propertyType = residence.propertyType.toInt(),
|
propertyType = residence.propertyType?.toInt(),
|
||||||
streetAddress = residence.streetAddress,
|
streetAddress = residence.streetAddress,
|
||||||
apartmentUnit = residence.apartmentUnit,
|
apartmentUnit = residence.apartmentUnit,
|
||||||
city = residence.city,
|
city = residence.city,
|
||||||
@@ -452,7 +452,7 @@ fun App(
|
|||||||
EditResidenceRoute(
|
EditResidenceRoute(
|
||||||
residenceId = residence.id,
|
residenceId = residence.id,
|
||||||
name = residence.name,
|
name = residence.name,
|
||||||
propertyType = residence.propertyType.toInt(),
|
propertyType = residence.propertyType?.toInt(),
|
||||||
streetAddress = residence.streetAddress,
|
streetAddress = residence.streetAddress,
|
||||||
apartmentUnit = residence.apartmentUnit,
|
apartmentUnit = residence.apartmentUnit,
|
||||||
city = residence.city,
|
city = residence.city,
|
||||||
|
|||||||
@@ -11,21 +11,21 @@ data class Residence(
|
|||||||
@SerialName("is_primary_owner") val isPrimaryOwner: Boolean = false,
|
@SerialName("is_primary_owner") val isPrimaryOwner: Boolean = false,
|
||||||
@SerialName("user_count") val userCount: Int = 1,
|
@SerialName("user_count") val userCount: Int = 1,
|
||||||
val name: String,
|
val name: String,
|
||||||
@SerialName("property_type") val propertyType: String,
|
@SerialName("property_type") val propertyType: String? = null,
|
||||||
@SerialName("street_address") val streetAddress: String,
|
@SerialName("street_address") val streetAddress: String? = null,
|
||||||
@SerialName("apartment_unit") val apartmentUnit: String?,
|
@SerialName("apartment_unit") val apartmentUnit: String? = null,
|
||||||
val city: String,
|
val city: String? = null,
|
||||||
@SerialName("state_province") val stateProvince: String,
|
@SerialName("state_province") val stateProvince: String? = null,
|
||||||
@SerialName("postal_code") val postalCode: String,
|
@SerialName("postal_code") val postalCode: String? = null,
|
||||||
val country: String,
|
val country: String? = null,
|
||||||
val bedrooms: Int?,
|
val bedrooms: Int? = null,
|
||||||
val bathrooms: Float?,
|
val bathrooms: Float? = null,
|
||||||
@SerialName("square_footage") val squareFootage: Int?,
|
@SerialName("square_footage") val squareFootage: Int? = null,
|
||||||
@SerialName("lot_size") val lotSize: Float?,
|
@SerialName("lot_size") val lotSize: Float? = null,
|
||||||
@SerialName("year_built") val yearBuilt: Int?,
|
@SerialName("year_built") val yearBuilt: Int? = null,
|
||||||
val description: String?,
|
val description: String? = null,
|
||||||
@SerialName("purchase_date") val purchaseDate: String?,
|
@SerialName("purchase_date") val purchaseDate: String? = null,
|
||||||
@SerialName("purchase_price") val purchasePrice: Double?,
|
@SerialName("purchase_price") val purchasePrice: Double? = null,
|
||||||
@SerialName("is_primary") val isPrimary: Boolean = false,
|
@SerialName("is_primary") val isPrimary: Boolean = false,
|
||||||
@SerialName("created_at") val createdAt: String,
|
@SerialName("created_at") val createdAt: String,
|
||||||
@SerialName("updated_at") val updatedAt: String
|
@SerialName("updated_at") val updatedAt: String
|
||||||
@@ -34,13 +34,13 @@ data class Residence(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class ResidenceCreateRequest(
|
data class ResidenceCreateRequest(
|
||||||
val name: String,
|
val name: String,
|
||||||
@SerialName("property_type") val propertyType: Int,
|
@SerialName("property_type") val propertyType: Int? = null,
|
||||||
@SerialName("street_address") val streetAddress: String,
|
@SerialName("street_address") val streetAddress: String? = null,
|
||||||
@SerialName("apartment_unit") val apartmentUnit: String? = null,
|
@SerialName("apartment_unit") val apartmentUnit: String? = null,
|
||||||
val city: String,
|
val city: String? = null,
|
||||||
@SerialName("state_province") val stateProvince: String,
|
@SerialName("state_province") val stateProvince: String? = null,
|
||||||
@SerialName("postal_code") val postalCode: String,
|
@SerialName("postal_code") val postalCode: String? = null,
|
||||||
val country: String,
|
val country: String? = null,
|
||||||
val bedrooms: Int? = null,
|
val bedrooms: Int? = null,
|
||||||
val bathrooms: Float? = null,
|
val bathrooms: Float? = null,
|
||||||
@SerialName("square_footage") val squareFootage: Int? = null,
|
@SerialName("square_footage") val squareFootage: Int? = null,
|
||||||
@@ -80,13 +80,13 @@ data class ResidenceSummary(
|
|||||||
val owner: Int,
|
val owner: Int,
|
||||||
@SerialName("owner_username") val ownerUsername: String,
|
@SerialName("owner_username") val ownerUsername: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
@SerialName("property_type") val propertyType: String,
|
@SerialName("property_type") val propertyType: String? = null,
|
||||||
@SerialName("street_address") val streetAddress: String,
|
@SerialName("street_address") val streetAddress: String? = null,
|
||||||
@SerialName("apartment_unit") val apartmentUnit: String?,
|
@SerialName("apartment_unit") val apartmentUnit: String? = null,
|
||||||
val city: String,
|
val city: String? = null,
|
||||||
@SerialName("state_province") val stateProvince: String,
|
@SerialName("state_province") val stateProvince: String? = null,
|
||||||
@SerialName("postal_code") val postalCode: String,
|
@SerialName("postal_code") val postalCode: String? = null,
|
||||||
val country: String,
|
val country: String? = null,
|
||||||
@SerialName("is_primary") val isPrimary: Boolean,
|
@SerialName("is_primary") val isPrimary: Boolean,
|
||||||
@SerialName("task_summary") val taskSummary: TaskSummary,
|
@SerialName("task_summary") val taskSummary: TaskSummary,
|
||||||
@SerialName("last_completed_task") val lastCompletedCustomTask: CustomTask?,
|
@SerialName("last_completed_task") val lastCompletedCustomTask: CustomTask?,
|
||||||
@@ -119,21 +119,21 @@ data class ResidenceWithTasks(
|
|||||||
@SerialName("is_primary_owner") val isPrimaryOwner: Boolean = false,
|
@SerialName("is_primary_owner") val isPrimaryOwner: Boolean = false,
|
||||||
@SerialName("user_count") val userCount: Int = 1,
|
@SerialName("user_count") val userCount: Int = 1,
|
||||||
val name: String,
|
val name: String,
|
||||||
@SerialName("property_type") val propertyType: String,
|
@SerialName("property_type") val propertyType: String? = null,
|
||||||
@SerialName("street_address") val streetAddress: String,
|
@SerialName("street_address") val streetAddress: String? = null,
|
||||||
@SerialName("apartment_unit") val apartmentUnit: String?,
|
@SerialName("apartment_unit") val apartmentUnit: String? = null,
|
||||||
val city: String,
|
val city: String? = null,
|
||||||
@SerialName("state_province") val stateProvince: String,
|
@SerialName("state_province") val stateProvince: String? = null,
|
||||||
@SerialName("postal_code") val postalCode: String,
|
@SerialName("postal_code") val postalCode: String? = null,
|
||||||
val country: String,
|
val country: String? = null,
|
||||||
val bedrooms: Int?,
|
val bedrooms: Int? = null,
|
||||||
val bathrooms: Float?,
|
val bathrooms: Float? = null,
|
||||||
@SerialName("square_footage") val squareFootage: Int?,
|
@SerialName("square_footage") val squareFootage: Int? = null,
|
||||||
@SerialName("lot_size") val lotSize: Float?,
|
@SerialName("lot_size") val lotSize: Float? = null,
|
||||||
@SerialName("year_built") val yearBuilt: Int?,
|
@SerialName("year_built") val yearBuilt: Int? = null,
|
||||||
val description: String?,
|
val description: String? = null,
|
||||||
@SerialName("purchase_date") val purchaseDate: String?,
|
@SerialName("purchase_date") val purchaseDate: String? = null,
|
||||||
@SerialName("purchase_price") val purchasePrice: Double?,
|
@SerialName("purchase_price") val purchasePrice: Double? = null,
|
||||||
@SerialName("is_primary") val isPrimary: Boolean,
|
@SerialName("is_primary") val isPrimary: Boolean,
|
||||||
@SerialName("task_summary") val taskSummary: TaskSummary,
|
@SerialName("task_summary") val taskSummary: TaskSummary,
|
||||||
val tasks: List<TaskDetail>,
|
val tasks: List<TaskDetail>,
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ object AddResidenceRoute
|
|||||||
data class EditResidenceRoute(
|
data class EditResidenceRoute(
|
||||||
val residenceId: Int,
|
val residenceId: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
val propertyType: Int,
|
val propertyType: Int?,
|
||||||
val streetAddress: String,
|
val streetAddress: String?,
|
||||||
val apartmentUnit: String?,
|
val apartmentUnit: String?,
|
||||||
val city: String,
|
val city: String?,
|
||||||
val stateProvince: String,
|
val stateProvince: String?,
|
||||||
val postalCode: String,
|
val postalCode: String?,
|
||||||
val country: String,
|
val country: String?,
|
||||||
val bedrooms: Int?,
|
val bedrooms: Int?,
|
||||||
val bathrooms: Float?,
|
val bathrooms: Float?,
|
||||||
val squareFootage: Int?,
|
val squareFootage: Int?,
|
||||||
|
|||||||
@@ -471,19 +471,29 @@ fun ResidenceDetailScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Address Card
|
// Address Card
|
||||||
|
if (residence.streetAddress != null || residence.city != null ||
|
||||||
|
residence.stateProvince != null || residence.postalCode != null ||
|
||||||
|
residence.country != null) {
|
||||||
item {
|
item {
|
||||||
InfoCard(
|
InfoCard(
|
||||||
icon = Icons.Default.LocationOn,
|
icon = Icons.Default.LocationOn,
|
||||||
title = "Address"
|
title = "Address"
|
||||||
) {
|
) {
|
||||||
|
if (residence.streetAddress != null) {
|
||||||
Text(text = residence.streetAddress)
|
Text(text = residence.streetAddress)
|
||||||
|
}
|
||||||
if (residence.apartmentUnit != null) {
|
if (residence.apartmentUnit != null) {
|
||||||
Text(text = "Unit: ${residence.apartmentUnit}")
|
Text(text = "Unit: ${residence.apartmentUnit}")
|
||||||
}
|
}
|
||||||
Text(text = "${residence.city}, ${residence.stateProvince} ${residence.postalCode}")
|
if (residence.city != null || residence.stateProvince != null || residence.postalCode != null) {
|
||||||
|
Text(text = "${residence.city ?: ""}, ${residence.stateProvince ?: ""} ${residence.postalCode ?: ""}")
|
||||||
|
}
|
||||||
|
if (residence.country != null) {
|
||||||
Text(text = residence.country)
|
Text(text = residence.country)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Property Details Card
|
// Property Details Card
|
||||||
if (residence.bedrooms != null || residence.bathrooms != null ||
|
if (residence.bedrooms != null || residence.bathrooms != null ||
|
||||||
|
|||||||
@@ -56,10 +56,6 @@ fun ResidenceFormScreen(
|
|||||||
|
|
||||||
// Validation errors
|
// Validation errors
|
||||||
var nameError by remember { mutableStateOf("") }
|
var nameError by remember { mutableStateOf("") }
|
||||||
var streetAddressError by remember { mutableStateOf("") }
|
|
||||||
var cityError by remember { mutableStateOf("") }
|
|
||||||
var stateProvinceError by remember { mutableStateOf("") }
|
|
||||||
var postalCodeError by remember { mutableStateOf("") }
|
|
||||||
|
|
||||||
// Handle operation state changes
|
// Handle operation state changes
|
||||||
LaunchedEffect(operationState) {
|
LaunchedEffect(operationState) {
|
||||||
@@ -79,10 +75,12 @@ fun ResidenceFormScreen(
|
|||||||
// Set default/existing property type when types are loaded
|
// Set default/existing property type when types are loaded
|
||||||
LaunchedEffect(propertyTypes, existingResidence) {
|
LaunchedEffect(propertyTypes, existingResidence) {
|
||||||
if (propertyTypes.isNotEmpty() && propertyType == null) {
|
if (propertyTypes.isNotEmpty() && propertyType == null) {
|
||||||
propertyType = if (isEditMode && existingResidence != null) {
|
propertyType = if (isEditMode && existingResidence != null && existingResidence.propertyType != null) {
|
||||||
propertyTypes.find { it.id == existingResidence.propertyType.toInt() }
|
propertyTypes.find { it.id == existingResidence.propertyType.toInt() }
|
||||||
} else {
|
} else if (!isEditMode && propertyTypes.isNotEmpty()) {
|
||||||
propertyTypes.first()
|
propertyTypes.first()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,34 +95,6 @@ fun ResidenceFormScreen(
|
|||||||
nameError = ""
|
nameError = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if (streetAddress.isBlank()) {
|
|
||||||
streetAddressError = "Street address is required"
|
|
||||||
isValid = false
|
|
||||||
} else {
|
|
||||||
streetAddressError = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if (city.isBlank()) {
|
|
||||||
cityError = "City is required"
|
|
||||||
isValid = false
|
|
||||||
} else {
|
|
||||||
cityError = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stateProvince.isBlank()) {
|
|
||||||
stateProvinceError = "State/Province is required"
|
|
||||||
isValid = false
|
|
||||||
} else {
|
|
||||||
stateProvinceError = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if (postalCode.isBlank()) {
|
|
||||||
postalCodeError = "Postal code is required"
|
|
||||||
isValid = false
|
|
||||||
} else {
|
|
||||||
postalCodeError = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return isValid
|
return isValid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,9 +118,9 @@ fun ResidenceFormScreen(
|
|||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
// Required fields section
|
// Basic Information section
|
||||||
Text(
|
Text(
|
||||||
text = "Required Information",
|
text = "Property Details",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.primary
|
color = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
@@ -162,8 +132,10 @@ fun ResidenceFormScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
isError = nameError.isNotEmpty(),
|
isError = nameError.isNotEmpty(),
|
||||||
supportingText = if (nameError.isNotEmpty()) {
|
supportingText = if (nameError.isNotEmpty()) {
|
||||||
{ Text(nameError) }
|
{ Text(nameError, color = MaterialTheme.colorScheme.error) }
|
||||||
} else null
|
} else {
|
||||||
|
{ Text("Required", color = MaterialTheme.colorScheme.error) }
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
ExposedDropdownMenuBox(
|
ExposedDropdownMenuBox(
|
||||||
@@ -174,7 +146,7 @@ fun ResidenceFormScreen(
|
|||||||
value = propertyType?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
value = propertyType?.name?.replaceFirstChar { it.uppercase() } ?: "",
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
label = { Text("Property Type *") },
|
label = { Text("Property Type") },
|
||||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -197,15 +169,18 @@ fun ResidenceFormScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Address section
|
||||||
|
Text(
|
||||||
|
text = "Address",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = streetAddress,
|
value = streetAddress,
|
||||||
onValueChange = { streetAddress = it },
|
onValueChange = { streetAddress = it },
|
||||||
label = { Text("Street Address *") },
|
label = { Text("Street Address") },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth()
|
||||||
isError = streetAddressError.isNotEmpty(),
|
|
||||||
supportingText = if (streetAddressError.isNotEmpty()) {
|
|
||||||
{ Text(streetAddressError) }
|
|
||||||
} else null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
@@ -218,34 +193,22 @@ fun ResidenceFormScreen(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = city,
|
value = city,
|
||||||
onValueChange = { city = it },
|
onValueChange = { city = it },
|
||||||
label = { Text("City *") },
|
label = { Text("City") },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth()
|
||||||
isError = cityError.isNotEmpty(),
|
|
||||||
supportingText = if (cityError.isNotEmpty()) {
|
|
||||||
{ Text(cityError) }
|
|
||||||
} else null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = stateProvince,
|
value = stateProvince,
|
||||||
onValueChange = { stateProvince = it },
|
onValueChange = { stateProvince = it },
|
||||||
label = { Text("State/Province *") },
|
label = { Text("State/Province") },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth()
|
||||||
isError = stateProvinceError.isNotEmpty(),
|
|
||||||
supportingText = if (stateProvinceError.isNotEmpty()) {
|
|
||||||
{ Text(stateProvinceError) }
|
|
||||||
} else null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = postalCode,
|
value = postalCode,
|
||||||
onValueChange = { postalCode = it },
|
onValueChange = { postalCode = it },
|
||||||
label = { Text("Postal Code *") },
|
label = { Text("Postal Code") },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth()
|
||||||
isError = postalCodeError.isNotEmpty(),
|
|
||||||
supportingText = if (postalCodeError.isNotEmpty()) {
|
|
||||||
{ Text(postalCodeError) }
|
|
||||||
} else null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
@@ -340,16 +303,16 @@ fun ResidenceFormScreen(
|
|||||||
// Submit button
|
// Submit button
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (validateForm() && propertyType != null) {
|
if (validateForm()) {
|
||||||
val request = ResidenceCreateRequest(
|
val request = ResidenceCreateRequest(
|
||||||
name = name,
|
name = name,
|
||||||
propertyType = propertyType!!.id,
|
propertyType = propertyType?.id,
|
||||||
streetAddress = streetAddress,
|
streetAddress = streetAddress.ifBlank { null },
|
||||||
apartmentUnit = apartmentUnit.ifBlank { null },
|
apartmentUnit = apartmentUnit.ifBlank { null },
|
||||||
city = city,
|
city = city.ifBlank { null },
|
||||||
stateProvince = stateProvince,
|
stateProvince = stateProvince.ifBlank { null },
|
||||||
postalCode = postalCode,
|
postalCode = postalCode.ifBlank { null },
|
||||||
country = country,
|
country = country.ifBlank { null },
|
||||||
bedrooms = bedrooms.toIntOrNull(),
|
bedrooms = bedrooms.toIntOrNull(),
|
||||||
bathrooms = bathrooms.toFloatOrNull(),
|
bathrooms = bathrooms.toFloatOrNull(),
|
||||||
squareFootage = squareFootage.toIntOrNull(),
|
squareFootage = squareFootage.toIntOrNull(),
|
||||||
@@ -367,7 +330,7 @@ fun ResidenceFormScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
enabled = validateForm() && propertyType != null
|
enabled = validateForm()
|
||||||
) {
|
) {
|
||||||
if (operationState is ApiResult.Loading) {
|
if (operationState is ApiResult.Loading) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
|
|||||||
@@ -30,10 +30,6 @@ struct ResidenceFormView: View {
|
|||||||
|
|
||||||
// Validation errors
|
// Validation errors
|
||||||
@State private var nameError: String = ""
|
@State private var nameError: String = ""
|
||||||
@State private var streetAddressError: String = ""
|
|
||||||
@State private var cityError: String = ""
|
|
||||||
@State private var stateProvinceError: String = ""
|
|
||||||
@State private var postalCodeError: String = ""
|
|
||||||
|
|
||||||
enum Field {
|
enum Field {
|
||||||
case name, streetAddress, apartmentUnit, city, stateProvince, postalCode, country
|
case name, streetAddress, apartmentUnit, city, stateProvince, postalCode, country
|
||||||
@@ -44,12 +40,17 @@ struct ResidenceFormView: View {
|
|||||||
existingResidence != nil
|
existingResidence != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var canSave: Bool {
|
||||||
|
!name.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
Form {
|
Form {
|
||||||
Section(header: Text("Property Details")) {
|
Section {
|
||||||
TextField("Property Name", text: $name)
|
TextField("Property Name", text: $name)
|
||||||
.focused($focusedField, equals: .name)
|
.focused($focusedField, equals: .name)
|
||||||
|
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.nameField)
|
||||||
|
|
||||||
if !nameError.isEmpty {
|
if !nameError.isEmpty {
|
||||||
Text(nameError)
|
Text(nameError)
|
||||||
@@ -63,50 +64,41 @@ struct ResidenceFormView: View {
|
|||||||
Text(type.name).tag(type as ResidenceType?)
|
Text(type.name).tag(type as ResidenceType?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.propertyTypePicker)
|
||||||
|
} header: {
|
||||||
Section(header: Text("Address")) {
|
Text("Property Details")
|
||||||
TextField("Street Address", text: $streetAddress)
|
} footer: {
|
||||||
.focused($focusedField, equals: .streetAddress)
|
Text("Required: Name")
|
||||||
|
|
||||||
if !streetAddressError.isEmpty {
|
|
||||||
Text(streetAddressError)
|
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
TextField("Street Address", text: $streetAddress)
|
||||||
|
.focused($focusedField, equals: .streetAddress)
|
||||||
|
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.streetAddressField)
|
||||||
|
|
||||||
TextField("Apartment/Unit (optional)", text: $apartmentUnit)
|
TextField("Apartment/Unit (optional)", text: $apartmentUnit)
|
||||||
.focused($focusedField, equals: .apartmentUnit)
|
.focused($focusedField, equals: .apartmentUnit)
|
||||||
|
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.apartmentUnitField)
|
||||||
|
|
||||||
TextField("City", text: $city)
|
TextField("City", text: $city)
|
||||||
.focused($focusedField, equals: .city)
|
.focused($focusedField, equals: .city)
|
||||||
|
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.cityField)
|
||||||
if !cityError.isEmpty {
|
|
||||||
Text(cityError)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.red)
|
|
||||||
}
|
|
||||||
|
|
||||||
TextField("State/Province", text: $stateProvince)
|
TextField("State/Province", text: $stateProvince)
|
||||||
.focused($focusedField, equals: .stateProvince)
|
.focused($focusedField, equals: .stateProvince)
|
||||||
|
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.stateProvinceField)
|
||||||
if !stateProvinceError.isEmpty {
|
|
||||||
Text(stateProvinceError)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.red)
|
|
||||||
}
|
|
||||||
|
|
||||||
TextField("Postal Code", text: $postalCode)
|
TextField("Postal Code", text: $postalCode)
|
||||||
.focused($focusedField, equals: .postalCode)
|
.focused($focusedField, equals: .postalCode)
|
||||||
|
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.postalCodeField)
|
||||||
if !postalCodeError.isEmpty {
|
|
||||||
Text(postalCodeError)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.red)
|
|
||||||
}
|
|
||||||
|
|
||||||
TextField("Country", text: $country)
|
TextField("Country", text: $country)
|
||||||
.focused($focusedField, equals: .country)
|
.focused($focusedField, equals: .country)
|
||||||
|
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.countryField)
|
||||||
|
} header: {
|
||||||
|
Text("Address")
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Text("Property Features")) {
|
Section(header: Text("Property Features")) {
|
||||||
@@ -118,6 +110,7 @@ struct ResidenceFormView: View {
|
|||||||
.multilineTextAlignment(.trailing)
|
.multilineTextAlignment(.trailing)
|
||||||
.frame(width: 60)
|
.frame(width: 60)
|
||||||
.focused($focusedField, equals: .bedrooms)
|
.focused($focusedField, equals: .bedrooms)
|
||||||
|
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.bedroomsField)
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
@@ -128,26 +121,32 @@ struct ResidenceFormView: View {
|
|||||||
.multilineTextAlignment(.trailing)
|
.multilineTextAlignment(.trailing)
|
||||||
.frame(width: 60)
|
.frame(width: 60)
|
||||||
.focused($focusedField, equals: .bathrooms)
|
.focused($focusedField, equals: .bathrooms)
|
||||||
|
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.bathroomsField)
|
||||||
}
|
}
|
||||||
|
|
||||||
TextField("Square Footage", text: $squareFootage)
|
TextField("Square Footage", text: $squareFootage)
|
||||||
.keyboardType(.numberPad)
|
.keyboardType(.numberPad)
|
||||||
.focused($focusedField, equals: .squareFootage)
|
.focused($focusedField, equals: .squareFootage)
|
||||||
|
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.squareFootageField)
|
||||||
|
|
||||||
TextField("Lot Size (acres)", text: $lotSize)
|
TextField("Lot Size (acres)", text: $lotSize)
|
||||||
.keyboardType(.decimalPad)
|
.keyboardType(.decimalPad)
|
||||||
.focused($focusedField, equals: .lotSize)
|
.focused($focusedField, equals: .lotSize)
|
||||||
|
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.lotSizeField)
|
||||||
|
|
||||||
TextField("Year Built", text: $yearBuilt)
|
TextField("Year Built", text: $yearBuilt)
|
||||||
.keyboardType(.numberPad)
|
.keyboardType(.numberPad)
|
||||||
.focused($focusedField, equals: .yearBuilt)
|
.focused($focusedField, equals: .yearBuilt)
|
||||||
|
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.yearBuiltField)
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Text("Additional Details")) {
|
Section(header: Text("Additional Details")) {
|
||||||
TextField("Description (optional)", text: $description, axis: .vertical)
|
TextField("Description (optional)", text: $description, axis: .vertical)
|
||||||
.lineLimit(3...6)
|
.lineLimit(3...6)
|
||||||
|
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.descriptionField)
|
||||||
|
|
||||||
Toggle("Primary Residence", isOn: $isPrimary)
|
Toggle("Primary Residence", isOn: $isPrimary)
|
||||||
|
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.isPrimaryToggle)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let errorMessage = viewModel.errorMessage {
|
if let errorMessage = viewModel.errorMessage {
|
||||||
@@ -165,13 +164,15 @@ struct ResidenceFormView: View {
|
|||||||
Button("Cancel") {
|
Button("Cancel") {
|
||||||
isPresented = false
|
isPresented = false
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.formCancelButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button("Save") {
|
Button("Save") {
|
||||||
submitForm()
|
submitForm()
|
||||||
}
|
}
|
||||||
.disabled(viewModel.isLoading)
|
.disabled(!canSave || viewModel.isLoading)
|
||||||
|
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.saveButton)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
@@ -197,7 +198,9 @@ struct ResidenceFormView: View {
|
|||||||
} else {
|
} else {
|
||||||
// Fallback to DataCache directly
|
// Fallback to DataCache directly
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.residenceTypes = DataCache.shared.residenceTypes.value as! [ResidenceType]
|
if let cached = DataCache.shared.residenceTypes.value as? [ResidenceType] {
|
||||||
|
self.residenceTypes = cached
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,12 +210,12 @@ struct ResidenceFormView: View {
|
|||||||
if let residence = existingResidence {
|
if let residence = existingResidence {
|
||||||
// Edit mode - populate fields from existing residence
|
// Edit mode - populate fields from existing residence
|
||||||
name = residence.name
|
name = residence.name
|
||||||
streetAddress = residence.streetAddress
|
streetAddress = residence.streetAddress ?? ""
|
||||||
apartmentUnit = residence.apartmentUnit ?? ""
|
apartmentUnit = residence.apartmentUnit ?? ""
|
||||||
city = residence.city
|
city = residence.city ?? ""
|
||||||
stateProvince = residence.stateProvince
|
stateProvince = residence.stateProvince ?? ""
|
||||||
postalCode = residence.postalCode
|
postalCode = residence.postalCode ?? ""
|
||||||
country = residence.country
|
country = residence.country ?? ""
|
||||||
bedrooms = residence.bedrooms != nil ? "\(residence.bedrooms!)" : ""
|
bedrooms = residence.bedrooms != nil ? "\(residence.bedrooms!)" : ""
|
||||||
bathrooms = residence.bathrooms != nil ? "\(residence.bathrooms!)" : ""
|
bathrooms = residence.bathrooms != nil ? "\(residence.bathrooms!)" : ""
|
||||||
squareFootage = residence.squareFootage != nil ? "\(residence.squareFootage!)" : ""
|
squareFootage = residence.squareFootage != nil ? "\(residence.squareFootage!)" : ""
|
||||||
@@ -222,13 +225,11 @@ struct ResidenceFormView: View {
|
|||||||
isPrimary = residence.isPrimary
|
isPrimary = residence.isPrimary
|
||||||
|
|
||||||
// Set the selected property type
|
// Set the selected property type
|
||||||
selectedPropertyType = residenceTypes.first { $0.id == Int(residence.propertyType) ?? 0 }
|
if let propertyTypeStr = residence.propertyType, let propertyTypeId = Int(propertyTypeStr) {
|
||||||
} else {
|
selectedPropertyType = residenceTypes.first { $0.id == propertyTypeId }
|
||||||
// Add mode - set default property type
|
|
||||||
if selectedPropertyType == nil && !residenceTypes.isEmpty {
|
|
||||||
selectedPropertyType = residenceTypes.first
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// In add mode, leave selectedPropertyType as nil to force user to select
|
||||||
}
|
}
|
||||||
|
|
||||||
private func validateForm() -> Bool {
|
private func validateForm() -> Bool {
|
||||||
@@ -241,57 +242,54 @@ struct ResidenceFormView: View {
|
|||||||
nameError = ""
|
nameError = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if streetAddress.isEmpty {
|
|
||||||
streetAddressError = "Street address is required"
|
|
||||||
isValid = false
|
|
||||||
} else {
|
|
||||||
streetAddressError = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if city.isEmpty {
|
|
||||||
cityError = "City is required"
|
|
||||||
isValid = false
|
|
||||||
} else {
|
|
||||||
cityError = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if stateProvince.isEmpty {
|
|
||||||
stateProvinceError = "State/Province is required"
|
|
||||||
isValid = false
|
|
||||||
} else {
|
|
||||||
stateProvinceError = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if postalCode.isEmpty {
|
|
||||||
postalCodeError = "Postal code is required"
|
|
||||||
isValid = false
|
|
||||||
} else {
|
|
||||||
postalCodeError = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return isValid
|
return isValid
|
||||||
}
|
}
|
||||||
|
|
||||||
private func submitForm() {
|
private func submitForm() {
|
||||||
guard validateForm() else { return }
|
guard validateForm() else { return }
|
||||||
guard let propertyType = selectedPropertyType else {
|
|
||||||
return
|
// Convert optional numeric fields to Kotlin types
|
||||||
}
|
let bedroomsValue: KotlinInt? = {
|
||||||
|
guard !bedrooms.isEmpty, let value = Int32(bedrooms) else { return nil }
|
||||||
|
return KotlinInt(int: value)
|
||||||
|
}()
|
||||||
|
let bathroomsValue: KotlinFloat? = {
|
||||||
|
guard !bathrooms.isEmpty, let value = Float(bathrooms) else { return nil }
|
||||||
|
return KotlinFloat(float: value)
|
||||||
|
}()
|
||||||
|
let squareFootageValue: KotlinInt? = {
|
||||||
|
guard !squareFootage.isEmpty, let value = Int32(squareFootage) else { return nil }
|
||||||
|
return KotlinInt(int: value)
|
||||||
|
}()
|
||||||
|
let lotSizeValue: KotlinFloat? = {
|
||||||
|
guard !lotSize.isEmpty, let value = Float(lotSize) else { return nil }
|
||||||
|
return KotlinFloat(float: value)
|
||||||
|
}()
|
||||||
|
let yearBuiltValue: KotlinInt? = {
|
||||||
|
guard !yearBuilt.isEmpty, let value = Int32(yearBuilt) else { return nil }
|
||||||
|
return KotlinInt(int: value)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Convert propertyType to KotlinInt if it exists
|
||||||
|
let propertyTypeValue: KotlinInt? = {
|
||||||
|
guard let type = selectedPropertyType else { return nil }
|
||||||
|
return KotlinInt(int: Int32(type.id))
|
||||||
|
}()
|
||||||
|
|
||||||
let request = ResidenceCreateRequest(
|
let request = ResidenceCreateRequest(
|
||||||
name: name,
|
name: name,
|
||||||
propertyType: Int32(propertyType.id),
|
propertyType: propertyTypeValue,
|
||||||
streetAddress: streetAddress,
|
streetAddress: streetAddress.isEmpty ? nil : streetAddress,
|
||||||
apartmentUnit: apartmentUnit.isEmpty ? nil : apartmentUnit,
|
apartmentUnit: apartmentUnit.isEmpty ? nil : apartmentUnit,
|
||||||
city: city,
|
city: city.isEmpty ? nil : city,
|
||||||
stateProvince: stateProvince,
|
stateProvince: stateProvince.isEmpty ? nil : stateProvince,
|
||||||
postalCode: postalCode,
|
postalCode: postalCode.isEmpty ? nil : postalCode,
|
||||||
country: country,
|
country: country.isEmpty ? nil : country,
|
||||||
bedrooms: Int32(bedrooms) as? KotlinInt,
|
bedrooms: bedroomsValue,
|
||||||
bathrooms: Float(bathrooms) as? KotlinFloat,
|
bathrooms: bathroomsValue,
|
||||||
squareFootage: Int32(squareFootage) as? KotlinInt,
|
squareFootage: squareFootageValue,
|
||||||
lotSize: Float(lotSize) as? KotlinFloat,
|
lotSize: lotSizeValue,
|
||||||
yearBuilt: Int32(yearBuilt) as? KotlinInt,
|
yearBuilt: yearBuiltValue,
|
||||||
description: description.isEmpty ? nil : description,
|
description: description.isEmpty ? nil : description,
|
||||||
purchaseDate: nil,
|
purchaseDate: nil,
|
||||||
purchasePrice: nil,
|
purchasePrice: nil,
|
||||||
|
|||||||
@@ -16,10 +16,12 @@ struct PropertyHeaderCard: View {
|
|||||||
.font(.title2)
|
.font(.title2)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
|
|
||||||
Text(residence.propertyType)
|
if let propertyType = residence.propertyType {
|
||||||
|
Text(propertyType)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
@@ -27,15 +29,19 @@ struct PropertyHeaderCard: View {
|
|||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Label(residence.streetAddress, systemImage: "mappin.circle.fill")
|
if let streetAddress = residence.streetAddress {
|
||||||
|
Label(streetAddress, systemImage: "mappin.circle.fill")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
|
}
|
||||||
|
|
||||||
Text("\(residence.city), \(residence.stateProvince) \(residence.postalCode)")
|
if residence.city != nil || residence.stateProvince != nil || residence.postalCode != nil {
|
||||||
|
Text("\(residence.city ?? ""), \(residence.stateProvince ?? "") \(residence.postalCode ?? "")")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
if !residence.country.isEmpty {
|
if let country = residence.country, !country.isEmpty {
|
||||||
Text(residence.country)
|
Text(country)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,12 +26,14 @@ struct ResidenceCard: View {
|
|||||||
.foregroundColor(Color(.label))
|
.foregroundColor(Color(.label))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
|
||||||
Text(residence.propertyType)
|
if let propertyType = residence.propertyType {
|
||||||
|
Text(propertyType)
|
||||||
.font(.caption.weight(.medium))
|
.font(.caption.weight(.medium))
|
||||||
.foregroundColor(Color(.tertiaryLabel))
|
.foregroundColor(Color(.tertiaryLabel))
|
||||||
.textCase(.uppercase)
|
.textCase(.uppercase)
|
||||||
.tracking(0.5)
|
.tracking(0.5)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@@ -49,24 +51,28 @@ struct ResidenceCard: View {
|
|||||||
|
|
||||||
// Address
|
// Address
|
||||||
VStack(alignment: .leading, spacing: AppSpacing.xxs) {
|
VStack(alignment: .leading, spacing: AppSpacing.xxs) {
|
||||||
|
if let streetAddress = residence.streetAddress {
|
||||||
HStack(spacing: AppSpacing.xxs) {
|
HStack(spacing: AppSpacing.xxs) {
|
||||||
Image(systemName: "mappin.circle.fill")
|
Image(systemName: "mappin.circle.fill")
|
||||||
.font(.system(size: 12, weight: .medium))
|
.font(.system(size: 12, weight: .medium))
|
||||||
.foregroundColor(Color(.tertiaryLabel))
|
.foregroundColor(Color(.tertiaryLabel))
|
||||||
Text(residence.streetAddress)
|
Text(streetAddress)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.foregroundColor(Color(.secondaryLabel))
|
.foregroundColor(Color(.secondaryLabel))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if residence.city != nil || residence.stateProvince != nil {
|
||||||
HStack(spacing: AppSpacing.xxs) {
|
HStack(spacing: AppSpacing.xxs) {
|
||||||
Image(systemName: "location.fill")
|
Image(systemName: "location.fill")
|
||||||
.font(.system(size: 12, weight: .medium))
|
.font(.system(size: 12, weight: .medium))
|
||||||
.foregroundColor(Color(.tertiaryLabel))
|
.foregroundColor(Color(.tertiaryLabel))
|
||||||
Text("\(residence.city), \(residence.stateProvince)")
|
Text("\(residence.city ?? ""), \(residence.stateProvince ?? "")")
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.foregroundColor(Color(.secondaryLabel))
|
.foregroundColor(Color(.secondaryLabel))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.padding(.vertical, AppSpacing.xs)
|
.padding(.vertical, AppSpacing.xs)
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|||||||
Reference in New Issue
Block a user