This commit is contained in:
Trey t
2025-11-05 13:52:02 -06:00
parent 2be3a5a3a8
commit 5deac95818
10 changed files with 981 additions and 14 deletions

View File

@@ -270,7 +270,8 @@ fun AddNewTaskDialog(
intervalDays = intervalDays.toIntOrNull(),
priority = priority.id,
dueDate = dueDate,
estimatedCost = estimatedCost.ifBlank { null }
estimatedCost = estimatedCost.ifBlank { null },
status = 9
)
)
}

View File

@@ -0,0 +1,369 @@
package com.mycrib.android.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.mycrib.android.viewmodel.ResidenceViewModel
import com.mycrib.repository.LookupsRepository
import com.mycrib.shared.models.Residence
import com.mycrib.shared.models.ResidenceCreateRequest
import com.mycrib.shared.models.ResidenceType
import com.mycrib.shared.network.ApiResult
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditResidenceScreen(
residence: Residence,
onNavigateBack: () -> Unit,
onResidenceUpdated: () -> Unit,
viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }
) {
var name by remember { mutableStateOf(residence.name) }
var propertyType by remember { mutableStateOf<ResidenceType?>(null) }
var streetAddress by remember { mutableStateOf(residence.streetAddress) }
var apartmentUnit by remember { mutableStateOf(residence.apartmentUnit ?: "") }
var city by remember { mutableStateOf(residence.city) }
var stateProvince by remember { mutableStateOf(residence.stateProvince) }
var postalCode by remember { mutableStateOf(residence.postalCode) }
var country by remember { mutableStateOf(residence.country) }
var bedrooms by remember { mutableStateOf(residence.bedrooms?.toString() ?: "") }
var bathrooms by remember { mutableStateOf(residence.bathrooms?.toString() ?: "") }
var squareFootage by remember { mutableStateOf(residence.squareFootage?.toString() ?: "") }
var lotSize by remember { mutableStateOf(residence.lotSize?.toString() ?: "") }
var yearBuilt by remember { mutableStateOf(residence.yearBuilt?.toString() ?: "") }
var description by remember { mutableStateOf(residence.description ?: "") }
var isPrimary by remember { mutableStateOf(residence.isPrimary) }
var expanded by remember { mutableStateOf(false) }
val updateState by viewModel.updateResidenceState.collectAsState()
val propertyTypes by LookupsRepository.residenceTypes.collectAsState()
// Validation errors
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 update state changes
LaunchedEffect(updateState) {
when (updateState) {
is ApiResult.Success -> {
viewModel.resetUpdateState()
onResidenceUpdated()
}
else -> {}
}
}
// Set property type from residence when types are loaded
LaunchedEffect(propertyTypes, residence) {
if (propertyTypes.isNotEmpty() && propertyType == null) {
propertyType = residence.propertyType.let { pt ->
propertyTypes.find { it.id == pt.toInt() }
} ?: propertyTypes.first()
}
}
fun validateForm(): Boolean {
var isValid = true
if (name.isBlank()) {
nameError = "Name is required"
isValid = false
} else {
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
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Edit Residence") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Required fields section
Text(
text = "Required Information",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary
)
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Property Name *") },
modifier = Modifier.fillMaxWidth(),
isError = nameError.isNotEmpty(),
supportingText = if (nameError.isNotEmpty()) {
{ Text(nameError) }
} else null
)
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = it }
) {
OutlinedTextField(
value = propertyType?.name?.replaceFirstChar { it.uppercase() } ?: "",
onValueChange = {},
readOnly = true,
label = { Text("Property Type *") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
modifier = Modifier
.fillMaxWidth()
.menuAnchor(),
enabled = propertyTypes.isNotEmpty()
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
propertyTypes.forEach { type ->
DropdownMenuItem(
text = { Text(type.name.replaceFirstChar { it.uppercase() }) },
onClick = {
propertyType = type
expanded = false
}
)
}
}
}
OutlinedTextField(
value = streetAddress,
onValueChange = { streetAddress = it },
label = { Text("Street Address *") },
modifier = Modifier.fillMaxWidth(),
isError = streetAddressError.isNotEmpty(),
supportingText = if (streetAddressError.isNotEmpty()) {
{ Text(streetAddressError) }
} else null
)
OutlinedTextField(
value = apartmentUnit,
onValueChange = { apartmentUnit = it },
label = { Text("Apartment/Unit #") },
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = city,
onValueChange = { city = it },
label = { Text("City *") },
modifier = Modifier.fillMaxWidth(),
isError = cityError.isNotEmpty(),
supportingText = if (cityError.isNotEmpty()) {
{ Text(cityError) }
} else null
)
OutlinedTextField(
value = stateProvince,
onValueChange = { stateProvince = it },
label = { Text("State/Province *") },
modifier = Modifier.fillMaxWidth(),
isError = stateProvinceError.isNotEmpty(),
supportingText = if (stateProvinceError.isNotEmpty()) {
{ Text(stateProvinceError) }
} else null
)
OutlinedTextField(
value = postalCode,
onValueChange = { postalCode = it },
label = { Text("Postal Code *") },
modifier = Modifier.fillMaxWidth(),
isError = postalCodeError.isNotEmpty(),
supportingText = if (postalCodeError.isNotEmpty()) {
{ Text(postalCodeError) }
} else null
)
OutlinedTextField(
value = country,
onValueChange = { country = it },
label = { Text("Country") },
modifier = Modifier.fillMaxWidth()
)
// Optional fields section
Divider()
Text(
text = "Optional Details",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = bedrooms,
onValueChange = { bedrooms = it.filter { char -> char.isDigit() } },
label = { Text("Bedrooms") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.weight(1f)
)
OutlinedTextField(
value = bathrooms,
onValueChange = { bathrooms = it },
label = { Text("Bathrooms") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = Modifier.weight(1f)
)
}
OutlinedTextField(
value = squareFootage,
onValueChange = { squareFootage = it.filter { char -> char.isDigit() } },
label = { Text("Square Footage") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = lotSize,
onValueChange = { lotSize = it },
label = { Text("Lot Size (acres)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = yearBuilt,
onValueChange = { yearBuilt = it.filter { char -> char.isDigit() } },
label = { Text("Year Built") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = description,
onValueChange = { description = it },
label = { Text("Description") },
modifier = Modifier.fillMaxWidth(),
minLines = 3,
maxLines = 5
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Primary Residence")
Switch(
checked = isPrimary,
onCheckedChange = { isPrimary = it }
)
}
// Error message
if (updateState is ApiResult.Error) {
Text(
text = (updateState as ApiResult.Error).message,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
}
// Submit button
Button(
onClick = {
if (validateForm() && propertyType != null) {
viewModel.updateResidence(
residenceId = residence.id,
request = ResidenceCreateRequest(
name = name,
propertyType = propertyType!!.id,
streetAddress = streetAddress,
apartmentUnit = apartmentUnit.ifBlank { null },
city = city,
stateProvince = stateProvince,
postalCode = postalCode,
country = country,
bedrooms = bedrooms.toIntOrNull(),
bathrooms = bathrooms.toFloatOrNull(),
squareFootage = squareFootage.toIntOrNull(),
lotSize = lotSize.toFloatOrNull(),
yearBuilt = yearBuilt.toIntOrNull(),
description = description.ifBlank { null },
isPrimary = isPrimary
)
)
}
},
modifier = Modifier.fillMaxWidth(),
enabled = validateForm() && propertyType != null
) {
if (updateState is ApiResult.Loading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary
)
} else {
Text("Update Residence")
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}
}

View File

@@ -27,6 +27,7 @@ import com.mycrib.shared.network.ApiResult
fun ResidenceDetailScreen(
residenceId: Int,
onNavigateBack: () -> Unit,
onNavigateToEditResidence: (Residence) -> Unit,
residenceViewModel: ResidenceViewModel = viewModel { ResidenceViewModel() },
taskCompletionViewModel: TaskCompletionViewModel = viewModel { TaskCompletionViewModel() },
taskViewModel: TaskViewModel = viewModel { TaskViewModel() }
@@ -63,9 +64,8 @@ fun ResidenceDetailScreen(
LaunchedEffect(taskAddNewTaskState) {
when (taskAddNewTaskState) {
is ApiResult.Success -> {
showCompleteDialog = false
selectedTask = null
taskCompletionViewModel.resetCreateState()
showNewTaskDialog = false
taskViewModel.resetAddTaskState()
residenceViewModel.loadResidenceTasks(residenceId)
}
else -> {}
@@ -113,6 +113,17 @@ fun ResidenceDetailScreen(
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
},
actions = {
// Edit button - only show when residence is loaded
if (residenceState is ApiResult.Success) {
IconButton(onClick = {
val residence = (residenceState as ApiResult.Success<Residence>).data
onNavigateToEditResidence(residence)
}) {
Icon(Icons.Default.Edit, contentDescription = "Edit Residence")
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
)
@@ -264,7 +275,7 @@ fun ResidenceDetailScreen(
}
// Description Card
if (residence.description != null) {
if (residence.description != null && !residence.description.isEmpty()) {
item {
InfoCard(
icon = Icons.Default.Description,