wip
This commit is contained in:
@@ -15,6 +15,7 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import com.mycrib.android.ui.screens.AddResidenceScreen
|
import com.mycrib.android.ui.screens.AddResidenceScreen
|
||||||
|
import com.mycrib.android.ui.screens.EditResidenceScreen
|
||||||
import com.mycrib.android.ui.screens.HomeScreen
|
import com.mycrib.android.ui.screens.HomeScreen
|
||||||
import com.mycrib.android.ui.screens.LoginScreen
|
import com.mycrib.android.ui.screens.LoginScreen
|
||||||
import com.mycrib.android.ui.screens.RegisterScreen
|
import com.mycrib.android.ui.screens.RegisterScreen
|
||||||
@@ -30,6 +31,8 @@ import androidx.navigation.compose.composable
|
|||||||
import androidx.navigation.toRoute
|
import androidx.navigation.toRoute
|
||||||
import com.mycrib.navigation.*
|
import com.mycrib.navigation.*
|
||||||
import com.mycrib.repository.LookupsRepository
|
import com.mycrib.repository.LookupsRepository
|
||||||
|
import com.mycrib.shared.models.Residence
|
||||||
|
import com.mycrib.storage.TokenStorage
|
||||||
|
|
||||||
import mycrib.composeapp.generated.resources.Res
|
import mycrib.composeapp.generated.resources.Res
|
||||||
import mycrib.composeapp.generated.resources.compose_multiplatform
|
import mycrib.composeapp.generated.resources.compose_multiplatform
|
||||||
@@ -37,12 +40,12 @@ import mycrib.composeapp.generated.resources.compose_multiplatform
|
|||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
fun App() {
|
fun App() {
|
||||||
var isLoggedIn by remember { mutableStateOf(com.mycrib.storage.TokenStorage.hasToken()) }
|
var isLoggedIn by remember { mutableStateOf(TokenStorage.hasToken()) }
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
// Check for stored token on app start and initialize lookups if logged in
|
// Check for stored token on app start and initialize lookups if logged in
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
isLoggedIn = com.mycrib.storage.TokenStorage.hasToken()
|
isLoggedIn = TokenStorage.hasToken()
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
LookupsRepository.initialize()
|
LookupsRepository.initialize()
|
||||||
}
|
}
|
||||||
@@ -98,7 +101,7 @@ fun App() {
|
|||||||
},
|
},
|
||||||
onLogout = {
|
onLogout = {
|
||||||
// Clear token and lookups on logout
|
// Clear token and lookups on logout
|
||||||
com.mycrib.storage.TokenStorage.clearToken()
|
TokenStorage.clearToken()
|
||||||
LookupsRepository.clear()
|
LookupsRepository.clear()
|
||||||
isLoggedIn = false
|
isLoggedIn = false
|
||||||
navController.navigate(LoginRoute) {
|
navController.navigate(LoginRoute) {
|
||||||
@@ -118,7 +121,7 @@ fun App() {
|
|||||||
},
|
},
|
||||||
onLogout = {
|
onLogout = {
|
||||||
// Clear token and lookups on logout
|
// Clear token and lookups on logout
|
||||||
com.mycrib.storage.TokenStorage.clearToken()
|
TokenStorage.clearToken()
|
||||||
LookupsRepository.clear()
|
LookupsRepository.clear()
|
||||||
isLoggedIn = false
|
isLoggedIn = false
|
||||||
navController.navigate(LoginRoute) {
|
navController.navigate(LoginRoute) {
|
||||||
@@ -139,6 +142,42 @@ fun App() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable<EditResidenceRoute> { backStackEntry ->
|
||||||
|
val route = backStackEntry.toRoute<EditResidenceRoute>()
|
||||||
|
EditResidenceScreen(
|
||||||
|
residence = Residence(
|
||||||
|
id = route.residenceId,
|
||||||
|
name = route.name,
|
||||||
|
propertyType = route.propertyType.toString(), // Will be fetched from lookups
|
||||||
|
streetAddress = route.streetAddress,
|
||||||
|
apartmentUnit = route.apartmentUnit,
|
||||||
|
city = route.city,
|
||||||
|
stateProvince = route.stateProvince,
|
||||||
|
postalCode = route.postalCode,
|
||||||
|
country = route.country,
|
||||||
|
bedrooms = route.bedrooms,
|
||||||
|
bathrooms = route.bathrooms,
|
||||||
|
squareFootage = route.squareFootage,
|
||||||
|
lotSize = route.lotSize,
|
||||||
|
yearBuilt = route.yearBuilt,
|
||||||
|
description = route.description,
|
||||||
|
purchaseDate = null,
|
||||||
|
purchasePrice = null,
|
||||||
|
isPrimary = route.isPrimary,
|
||||||
|
ownerUsername = route.ownerUserName,
|
||||||
|
owner = route.owner,
|
||||||
|
createdAt = route.createdAt,
|
||||||
|
updatedAt = route.updatedAt
|
||||||
|
),
|
||||||
|
onNavigateBack = {
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
onResidenceUpdated = {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
composable<TasksRoute> {
|
composable<TasksRoute> {
|
||||||
TasksScreen(
|
TasksScreen(
|
||||||
onNavigateBack = {
|
onNavigateBack = {
|
||||||
@@ -153,6 +192,32 @@ fun App() {
|
|||||||
residenceId = route.residenceId,
|
residenceId = route.residenceId,
|
||||||
onNavigateBack = {
|
onNavigateBack = {
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
onNavigateToEditResidence = { residence ->
|
||||||
|
navController.navigate(
|
||||||
|
EditResidenceRoute(
|
||||||
|
residenceId = residence.id,
|
||||||
|
name = residence.name,
|
||||||
|
propertyType = residence.propertyType.toInt(),
|
||||||
|
streetAddress = residence.streetAddress,
|
||||||
|
apartmentUnit = residence.apartmentUnit,
|
||||||
|
city = residence.city,
|
||||||
|
stateProvince = residence.stateProvince,
|
||||||
|
postalCode = residence.postalCode,
|
||||||
|
country = residence.country,
|
||||||
|
bedrooms = residence.bedrooms,
|
||||||
|
bathrooms = residence.bathrooms,
|
||||||
|
squareFootage = residence.squareFootage,
|
||||||
|
lotSize = residence.lotSize,
|
||||||
|
yearBuilt = residence.yearBuilt,
|
||||||
|
description = residence.description,
|
||||||
|
isPrimary = residence.isPrimary,
|
||||||
|
ownerUserName = residence.ownerUsername,
|
||||||
|
createdAt = residence.createdAt,
|
||||||
|
updatedAt = residence.updatedAt,
|
||||||
|
owner = residence.owner
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ data class CustomTask (
|
|||||||
@SerialName("created_by") val createdBy: Int,
|
@SerialName("created_by") val createdBy: Int,
|
||||||
@SerialName("created_by_username") val createdByUsername: String,
|
@SerialName("created_by_username") val createdByUsername: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val description: String?,
|
val description: String? = null,
|
||||||
val category: String,
|
val category: String,
|
||||||
val priority: String,
|
val priority: String,
|
||||||
val status: String,
|
val status: String,
|
||||||
@SerialName("due_date") val dueDate: String,
|
@SerialName("due_date") val dueDate: String,
|
||||||
@SerialName("estimated_cost") val estimatedCost: String?,
|
@SerialName("estimated_cost") val estimatedCost: String? = null,
|
||||||
@SerialName("actual_cost") val actualCost: String?,
|
@SerialName("actual_cost") val actualCost: String? = null,
|
||||||
val notes: String?,
|
val notes: String? = null,
|
||||||
@SerialName("created_at") val createdAt: String,
|
@SerialName("created_at") val createdAt: String,
|
||||||
@SerialName("updated_at") val updatedAt: String,
|
@SerialName("updated_at") val updatedAt: String,
|
||||||
@SerialName("show_completed_button") val showCompletedButton: Boolean = false,
|
@SerialName("show_completed_button") val showCompletedButton: Boolean = false,
|
||||||
@@ -43,7 +43,7 @@ data class TaskCreateRequest(
|
|||||||
val frequency: Int,
|
val frequency: Int,
|
||||||
@SerialName("interval_days") val intervalDays: Int? = null,
|
@SerialName("interval_days") val intervalDays: Int? = null,
|
||||||
val priority: Int,
|
val priority: Int,
|
||||||
val status: Int = 9,
|
val status: Int,
|
||||||
@SerialName("due_date") val dueDate: String,
|
@SerialName("due_date") val dueDate: String,
|
||||||
@SerialName("estimated_cost") val estimatedCost: String? = null
|
@SerialName("estimated_cost") val estimatedCost: String? = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.mycrib.navigation
|
package com.mycrib.navigation
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -17,6 +18,30 @@ object ResidencesRoute
|
|||||||
@Serializable
|
@Serializable
|
||||||
object AddResidenceRoute
|
object AddResidenceRoute
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class EditResidenceRoute(
|
||||||
|
val residenceId: Int,
|
||||||
|
val name: String,
|
||||||
|
val propertyType: Int,
|
||||||
|
val streetAddress: String,
|
||||||
|
val apartmentUnit: String?,
|
||||||
|
val city: String,
|
||||||
|
val stateProvince: String,
|
||||||
|
val postalCode: String,
|
||||||
|
val country: String,
|
||||||
|
val bedrooms: Int?,
|
||||||
|
val bathrooms: Float?,
|
||||||
|
val squareFootage: Int?,
|
||||||
|
val lotSize: Float?,
|
||||||
|
val yearBuilt: Int?,
|
||||||
|
val description: String?,
|
||||||
|
val isPrimary: Boolean,
|
||||||
|
val ownerUserName: String,
|
||||||
|
val createdAt: String,
|
||||||
|
val updatedAt: String,
|
||||||
|
val owner: Int?,
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ResidenceDetailRoute(val residenceId: Int)
|
data class ResidenceDetailRoute(val residenceId: Int)
|
||||||
|
|
||||||
|
|||||||
@@ -270,7 +270,8 @@ fun AddNewTaskDialog(
|
|||||||
intervalDays = intervalDays.toIntOrNull(),
|
intervalDays = intervalDays.toIntOrNull(),
|
||||||
priority = priority.id,
|
priority = priority.id,
|
||||||
dueDate = dueDate,
|
dueDate = dueDate,
|
||||||
estimatedCost = estimatedCost.ifBlank { null }
|
estimatedCost = estimatedCost.ifBlank { null },
|
||||||
|
status = 9
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ import com.mycrib.shared.network.ApiResult
|
|||||||
fun ResidenceDetailScreen(
|
fun ResidenceDetailScreen(
|
||||||
residenceId: Int,
|
residenceId: Int,
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
|
onNavigateToEditResidence: (Residence) -> Unit,
|
||||||
residenceViewModel: ResidenceViewModel = viewModel { ResidenceViewModel() },
|
residenceViewModel: ResidenceViewModel = viewModel { ResidenceViewModel() },
|
||||||
taskCompletionViewModel: TaskCompletionViewModel = viewModel { TaskCompletionViewModel() },
|
taskCompletionViewModel: TaskCompletionViewModel = viewModel { TaskCompletionViewModel() },
|
||||||
taskViewModel: TaskViewModel = viewModel { TaskViewModel() }
|
taskViewModel: TaskViewModel = viewModel { TaskViewModel() }
|
||||||
@@ -63,9 +64,8 @@ fun ResidenceDetailScreen(
|
|||||||
LaunchedEffect(taskAddNewTaskState) {
|
LaunchedEffect(taskAddNewTaskState) {
|
||||||
when (taskAddNewTaskState) {
|
when (taskAddNewTaskState) {
|
||||||
is ApiResult.Success -> {
|
is ApiResult.Success -> {
|
||||||
showCompleteDialog = false
|
showNewTaskDialog = false
|
||||||
selectedTask = null
|
taskViewModel.resetAddTaskState()
|
||||||
taskCompletionViewModel.resetCreateState()
|
|
||||||
residenceViewModel.loadResidenceTasks(residenceId)
|
residenceViewModel.loadResidenceTasks(residenceId)
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
@@ -113,6 +113,17 @@ fun ResidenceDetailScreen(
|
|||||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
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(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surface
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
)
|
)
|
||||||
@@ -264,7 +275,7 @@ fun ResidenceDetailScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Description Card
|
// Description Card
|
||||||
if (residence.description != null) {
|
if (residence.description != null && !residence.description.isEmpty()) {
|
||||||
item {
|
item {
|
||||||
InfoCard(
|
InfoCard(
|
||||||
icon = Icons.Default.Description,
|
icon = Icons.Default.Description,
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ class ResidenceViewModel : ViewModel() {
|
|||||||
private val _createResidenceState = MutableStateFlow<ApiResult<Residence>>(ApiResult.Loading)
|
private val _createResidenceState = MutableStateFlow<ApiResult<Residence>>(ApiResult.Loading)
|
||||||
val createResidenceState: StateFlow<ApiResult<Residence>> = _createResidenceState
|
val createResidenceState: StateFlow<ApiResult<Residence>> = _createResidenceState
|
||||||
|
|
||||||
|
private val _updateResidenceState = MutableStateFlow<ApiResult<Residence>>(ApiResult.Loading)
|
||||||
|
val updateResidenceState: StateFlow<ApiResult<Residence>> = _updateResidenceState
|
||||||
|
|
||||||
private val _residenceTasksState = MutableStateFlow<ApiResult<TasksByResidenceResponse>>(ApiResult.Loading)
|
private val _residenceTasksState = MutableStateFlow<ApiResult<TasksByResidenceResponse>>(ApiResult.Loading)
|
||||||
val residenceTasksState: StateFlow<ApiResult<TasksByResidenceResponse>> = _residenceTasksState
|
val residenceTasksState: StateFlow<ApiResult<TasksByResidenceResponse>> = _residenceTasksState
|
||||||
|
|
||||||
@@ -98,10 +101,26 @@ class ResidenceViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateResidence(residenceId: Int, request: ResidenceCreateRequest) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_updateResidenceState.value = ApiResult.Loading
|
||||||
|
val token = TokenStorage.getToken()
|
||||||
|
if (token != null) {
|
||||||
|
_updateResidenceState.value = residenceApi.updateResidence(token, residenceId, request)
|
||||||
|
} else {
|
||||||
|
_updateResidenceState.value = ApiResult.Error("Not authenticated", 401)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun resetCreateState() {
|
fun resetCreateState() {
|
||||||
_createResidenceState.value = ApiResult.Loading
|
_createResidenceState.value = ApiResult.Loading
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun resetUpdateState() {
|
||||||
|
_updateResidenceState.value = ApiResult.Loading
|
||||||
|
}
|
||||||
|
|
||||||
fun loadMyResidences() {
|
fun loadMyResidences() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_myResidencesState.value = ApiResult.Loading
|
_myResidencesState.value = ApiResult.Loading
|
||||||
|
|||||||
365
iosApp/iosApp/EditResidenceView.swift
Normal file
365
iosApp/iosApp/EditResidenceView.swift
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import ComposeApp
|
||||||
|
|
||||||
|
struct EditResidenceView: View {
|
||||||
|
let residence: Residence
|
||||||
|
@Binding var isPresented: Bool
|
||||||
|
@StateObject private var viewModel = ResidenceViewModel()
|
||||||
|
@StateObject private var lookupsManager = LookupsManager.shared
|
||||||
|
@FocusState private var focusedField: Field?
|
||||||
|
|
||||||
|
// Form fields
|
||||||
|
@State private var name: String = ""
|
||||||
|
@State private var selectedPropertyType: ResidenceType?
|
||||||
|
@State private var streetAddress: String = ""
|
||||||
|
@State private var apartmentUnit: String = ""
|
||||||
|
@State private var city: String = ""
|
||||||
|
@State private var stateProvince: String = ""
|
||||||
|
@State private var postalCode: String = ""
|
||||||
|
@State private var country: String = "USA"
|
||||||
|
@State private var bedrooms: String = ""
|
||||||
|
@State private var bathrooms: String = ""
|
||||||
|
@State private var squareFootage: String = ""
|
||||||
|
@State private var lotSize: String = ""
|
||||||
|
@State private var yearBuilt: String = ""
|
||||||
|
@State private var description: String = ""
|
||||||
|
@State private var isPrimary: Bool = false
|
||||||
|
|
||||||
|
// Validation errors
|
||||||
|
@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 = ""
|
||||||
|
|
||||||
|
// Picker state
|
||||||
|
@State private var showPropertyTypePicker = false
|
||||||
|
|
||||||
|
typealias Field = AddResidenceView.Field
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
ZStack {
|
||||||
|
Color(.systemGroupedBackground)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
VStack(spacing: 24) {
|
||||||
|
// Required Information Section
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
Text("Required Information")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
|
||||||
|
FormTextField(
|
||||||
|
label: "Property Name",
|
||||||
|
text: $name,
|
||||||
|
error: nameError,
|
||||||
|
placeholder: "My Home",
|
||||||
|
focusedField: $focusedField,
|
||||||
|
field: .name
|
||||||
|
)
|
||||||
|
|
||||||
|
// Property Type Picker
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Property Type")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Button(action: {
|
||||||
|
showPropertyTypePicker = true
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Text(selectedPropertyType?.name ?? "Select Type")
|
||||||
|
.foregroundColor(selectedPropertyType == nil ? .gray : .primary)
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "chevron.down")
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemBackground))
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FormTextField(
|
||||||
|
label: "Street Address",
|
||||||
|
text: $streetAddress,
|
||||||
|
error: streetAddressError,
|
||||||
|
placeholder: "123 Main St",
|
||||||
|
focusedField: $focusedField,
|
||||||
|
field: .streetAddress
|
||||||
|
)
|
||||||
|
|
||||||
|
FormTextField(
|
||||||
|
label: "Apartment/Unit (Optional)",
|
||||||
|
text: $apartmentUnit,
|
||||||
|
error: "",
|
||||||
|
placeholder: "Apt 4B",
|
||||||
|
focusedField: $focusedField,
|
||||||
|
field: .apartmentUnit
|
||||||
|
)
|
||||||
|
|
||||||
|
FormTextField(
|
||||||
|
label: "City",
|
||||||
|
text: $city,
|
||||||
|
error: cityError,
|
||||||
|
placeholder: "San Francisco",
|
||||||
|
focusedField: $focusedField,
|
||||||
|
field: .city
|
||||||
|
)
|
||||||
|
|
||||||
|
FormTextField(
|
||||||
|
label: "State/Province",
|
||||||
|
text: $stateProvince,
|
||||||
|
error: stateProvinceError,
|
||||||
|
placeholder: "CA",
|
||||||
|
focusedField: $focusedField,
|
||||||
|
field: .stateProvince
|
||||||
|
)
|
||||||
|
|
||||||
|
FormTextField(
|
||||||
|
label: "Postal Code",
|
||||||
|
text: $postalCode,
|
||||||
|
error: postalCodeError,
|
||||||
|
placeholder: "94102",
|
||||||
|
focusedField: $focusedField,
|
||||||
|
field: .postalCode
|
||||||
|
)
|
||||||
|
|
||||||
|
FormTextField(
|
||||||
|
label: "Country",
|
||||||
|
text: $country,
|
||||||
|
error: "",
|
||||||
|
placeholder: "USA",
|
||||||
|
focusedField: $focusedField,
|
||||||
|
field: .country
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional Information Section
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
Text("Optional Information")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
FormTextField(
|
||||||
|
label: "Bedrooms",
|
||||||
|
text: $bedrooms,
|
||||||
|
error: "",
|
||||||
|
placeholder: "3",
|
||||||
|
focusedField: $focusedField,
|
||||||
|
field: .bedrooms,
|
||||||
|
keyboardType: .numberPad
|
||||||
|
)
|
||||||
|
|
||||||
|
FormTextField(
|
||||||
|
label: "Bathrooms",
|
||||||
|
text: $bathrooms,
|
||||||
|
error: "",
|
||||||
|
placeholder: "2.5",
|
||||||
|
focusedField: $focusedField,
|
||||||
|
field: .bathrooms,
|
||||||
|
keyboardType: .decimalPad
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
FormTextField(
|
||||||
|
label: "Square Footage",
|
||||||
|
text: $squareFootage,
|
||||||
|
error: "",
|
||||||
|
placeholder: "1800",
|
||||||
|
focusedField: $focusedField,
|
||||||
|
field: .squareFootage,
|
||||||
|
keyboardType: .numberPad
|
||||||
|
)
|
||||||
|
|
||||||
|
FormTextField(
|
||||||
|
label: "Lot Size (acres)",
|
||||||
|
text: $lotSize,
|
||||||
|
error: "",
|
||||||
|
placeholder: "0.25",
|
||||||
|
focusedField: $focusedField,
|
||||||
|
field: .lotSize,
|
||||||
|
keyboardType: .decimalPad
|
||||||
|
)
|
||||||
|
|
||||||
|
FormTextField(
|
||||||
|
label: "Year Built",
|
||||||
|
text: $yearBuilt,
|
||||||
|
error: "",
|
||||||
|
placeholder: "2010",
|
||||||
|
focusedField: $focusedField,
|
||||||
|
field: .yearBuilt,
|
||||||
|
keyboardType: .numberPad
|
||||||
|
)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Description")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
TextEditor(text: $description)
|
||||||
|
.frame(height: 100)
|
||||||
|
.padding(8)
|
||||||
|
.background(Color(.systemBackground))
|
||||||
|
.cornerRadius(8)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.stroke(Color.gray.opacity(0.3), lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Toggle("Primary Residence", isOn: $isPrimary)
|
||||||
|
.font(.subheadline)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit Button
|
||||||
|
Button(action: submitForm) {
|
||||||
|
HStack {
|
||||||
|
if viewModel.isLoading {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
|
} else {
|
||||||
|
Text("Update Property")
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding()
|
||||||
|
.background(viewModel.isLoading ? Color.gray : Color.blue)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.cornerRadius(12)
|
||||||
|
}
|
||||||
|
.disabled(viewModel.isLoading)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Edit Residence")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button("Cancel") {
|
||||||
|
isPresented = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showPropertyTypePicker) {
|
||||||
|
PropertyTypePickerView(
|
||||||
|
propertyTypes: lookupsManager.residenceTypes,
|
||||||
|
selectedType: $selectedPropertyType,
|
||||||
|
isPresented: $showPropertyTypePicker
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
populateFields()
|
||||||
|
}
|
||||||
|
.alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) {
|
||||||
|
Button("OK") {
|
||||||
|
viewModel.clearError()
|
||||||
|
}
|
||||||
|
} message: {
|
||||||
|
Text(viewModel.errorMessage ?? "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func populateFields() {
|
||||||
|
// Populate fields from the existing residence
|
||||||
|
name = residence.name
|
||||||
|
streetAddress = residence.streetAddress
|
||||||
|
apartmentUnit = residence.apartmentUnit ?? ""
|
||||||
|
city = residence.city
|
||||||
|
stateProvince = residence.stateProvince
|
||||||
|
postalCode = residence.postalCode
|
||||||
|
country = residence.country
|
||||||
|
bedrooms = residence.bedrooms != nil ? "\(residence.bedrooms!)" : ""
|
||||||
|
bathrooms = residence.bathrooms != nil ? "\(residence.bathrooms!)" : ""
|
||||||
|
squareFootage = residence.squareFootage != nil ? "\(residence.squareFootage!)" : ""
|
||||||
|
lotSize = residence.lotSize != nil ? "\(residence.lotSize!)" : ""
|
||||||
|
yearBuilt = residence.yearBuilt != nil ? "\(residence.yearBuilt!)" : ""
|
||||||
|
description = residence.description_ ?? ""
|
||||||
|
isPrimary = residence.isPrimary
|
||||||
|
|
||||||
|
// Set the selected property type
|
||||||
|
selectedPropertyType = lookupsManager.residenceTypes.first { $0.id == Int(residence.propertyType) ?? 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func validateForm() -> Bool {
|
||||||
|
var isValid = true
|
||||||
|
|
||||||
|
if name.isEmpty {
|
||||||
|
nameError = "Name is required"
|
||||||
|
isValid = false
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
private func submitForm() {
|
||||||
|
guard validateForm() else { return }
|
||||||
|
guard let propertyType = selectedPropertyType else {
|
||||||
|
// Show error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = ResidenceCreateRequest(
|
||||||
|
name: name,
|
||||||
|
propertyType: Int32(propertyType.id),
|
||||||
|
streetAddress: streetAddress,
|
||||||
|
apartmentUnit: apartmentUnit.isEmpty ? nil : apartmentUnit,
|
||||||
|
city: city,
|
||||||
|
stateProvince: stateProvince,
|
||||||
|
postalCode: postalCode,
|
||||||
|
country: country,
|
||||||
|
bedrooms: Int32(bedrooms) as? KotlinInt,
|
||||||
|
bathrooms: Float(bathrooms) as? KotlinFloat,
|
||||||
|
squareFootage: Int32(squareFootage) as? KotlinInt,
|
||||||
|
lotSize: Float(lotSize) as? KotlinFloat,
|
||||||
|
yearBuilt: Int32(yearBuilt) as? KotlinInt,
|
||||||
|
description: description.isEmpty ? nil : description,
|
||||||
|
purchaseDate: nil,
|
||||||
|
purchasePrice: nil,
|
||||||
|
isPrimary: isPrimary
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModel.updateResidence(id: residence.id, request: request) { success in
|
||||||
|
if success {
|
||||||
|
isPresented = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ struct ResidenceDetailView: View {
|
|||||||
@State private var isLoadingTasks = false
|
@State private var isLoadingTasks = false
|
||||||
@State private var tasksError: String?
|
@State private var tasksError: String?
|
||||||
@State private var showAddTask = false
|
@State private var showAddTask = false
|
||||||
|
@State private var showEditResidence = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -47,6 +48,16 @@ struct ResidenceDetailView: View {
|
|||||||
.navigationTitle("Property Details")
|
.navigationTitle("Property Details")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
if viewModel.selectedResidence != nil {
|
||||||
|
Button(action: {
|
||||||
|
showEditResidence = true
|
||||||
|
}) {
|
||||||
|
Text("Edit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
showAddTask = true
|
showAddTask = true
|
||||||
@@ -58,12 +69,23 @@ struct ResidenceDetailView: View {
|
|||||||
.sheet(isPresented: $showAddTask) {
|
.sheet(isPresented: $showAddTask) {
|
||||||
AddTaskView(residenceId: residenceId, isPresented: $showAddTask)
|
AddTaskView(residenceId: residenceId, isPresented: $showAddTask)
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $showEditResidence) {
|
||||||
|
if let residence = viewModel.selectedResidence {
|
||||||
|
EditResidenceView(residence: residence, isPresented: $showEditResidence)
|
||||||
|
}
|
||||||
|
}
|
||||||
.onChange(of: showAddTask) { isShowing in
|
.onChange(of: showAddTask) { isShowing in
|
||||||
if !isShowing {
|
if !isShowing {
|
||||||
// Refresh tasks when sheet is dismissed
|
// Refresh tasks when sheet is dismissed
|
||||||
loadResidenceWithTasks()
|
loadResidenceWithTasks()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: showEditResidence) { isShowing in
|
||||||
|
if !isShowing {
|
||||||
|
// Refresh residence data when edit sheet is dismissed
|
||||||
|
loadResidenceData()
|
||||||
|
}
|
||||||
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
loadResidenceData()
|
loadResidenceData()
|
||||||
}
|
}
|
||||||
@@ -296,6 +318,69 @@ struct TaskCard: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ForEach(task.completions, id: \.id) { completion in
|
||||||
|
Spacer().frame(height: 12)
|
||||||
|
|
||||||
|
// Card equivalent
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
// Top row: date + rating badge
|
||||||
|
HStack {
|
||||||
|
Text(completion.completionDate.components(separatedBy: "T").first ?? "")
|
||||||
|
.font(.body.weight(.bold))
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if let rating = completion.rating {
|
||||||
|
Text("\(rating)★")
|
||||||
|
.font(.caption.weight(.bold))
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(Color(.tertiarySystemFill))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Completed by
|
||||||
|
if let name = completion.completedByName {
|
||||||
|
Text("By: \(name)")
|
||||||
|
.font(.subheadline.weight(.medium))
|
||||||
|
.padding(.top, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cost
|
||||||
|
if let cost = completion.actualCost {
|
||||||
|
Text("Cost: $\(cost)")
|
||||||
|
.font(.subheadline.weight(.medium))
|
||||||
|
.foregroundColor(.teal) // tertiary equivalent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(16)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
.fill(Color.secondary.opacity(0.15)) // surfaceVariant equivalent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if task.showCompletedButton {
|
||||||
|
Button(action: {}) {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "checkmark.circle.fill") // SF Symbol
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
Spacer().frame(width: 8)
|
||||||
|
Text("Complete Task")
|
||||||
|
.font(.title3.weight(.semibold)) // ≈ Material titleSmall + SemiBold
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent) // gives filled look
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(16)
|
.padding(16)
|
||||||
|
|||||||
@@ -118,6 +118,33 @@ class ResidenceViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateResidence(id: Int32, request: ResidenceCreateRequest, completion: @escaping (Bool) -> Void) {
|
||||||
|
guard let token = tokenStorage.getToken() else {
|
||||||
|
errorMessage = "Not authenticated"
|
||||||
|
completion(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
errorMessage = nil
|
||||||
|
|
||||||
|
residenceApi.updateResidence(token: token, id: id, request: request) { result, error in
|
||||||
|
if let successResult = result as? ApiResultSuccess<Residence> {
|
||||||
|
self.selectedResidence = successResult.data
|
||||||
|
self.isLoading = false
|
||||||
|
completion(true)
|
||||||
|
} else if let errorResult = result as? ApiResultError {
|
||||||
|
self.errorMessage = errorResult.message
|
||||||
|
self.isLoading = false
|
||||||
|
completion(false)
|
||||||
|
} else if let error = error {
|
||||||
|
self.errorMessage = error.localizedDescription
|
||||||
|
self.isLoading = false
|
||||||
|
completion(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func clearError() {
|
func clearError() {
|
||||||
errorMessage = nil
|
errorMessage = nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user