From 4642102ff5cfc71dd0cd16673a4fcbb813723201 Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Tue, 27 Jan 2026 04:55:28 +0530 Subject: [PATCH] Update amenities API and category suggestions --- .../com/android/trisolarispms/MainActivity.kt | 25 ++++----- .../trisolarispms/data/api/AmenityApi.kt | 13 ++--- .../data/api/model/AmenityModels.kt | 12 +++-- .../com/android/trisolarispms/ui/AppRoute.kt | 6 +-- .../trisolarispms/ui/home/HomeScreen.kt | 2 +- .../ui/roomtype/AddAmenityScreen.kt | 52 +++++++++++++++++-- .../ui/roomtype/AddRoomTypeScreen.kt | 4 +- .../ui/roomtype/AmenitiesScreen.kt | 11 ++-- .../ui/roomtype/AmenityFormState.kt | 2 + .../ui/roomtype/AmenityFormViewModel.kt | 40 +++++++++++--- .../ui/roomtype/AmenityListViewModel.kt | 11 ++-- .../ui/roomtype/EditAmenityScreen.kt | 52 +++++++++++++++++-- .../ui/roomtype/EditRoomTypeScreen.kt | 4 +- 13 files changed, 177 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/com/android/trisolarispms/MainActivity.kt b/app/src/main/java/com/android/trisolarispms/MainActivity.kt index 89e0b16..d5845e2 100644 --- a/app/src/main/java/com/android/trisolarispms/MainActivity.kt +++ b/app/src/main/java/com/android/trisolarispms/MainActivity.kt @@ -65,10 +65,8 @@ class MainActivity : ComponentActivity() { isSuperAdmin = state.isSuperAdmin, onAddProperty = { route.value = AppRoute.AddProperty }, onAmenities = { - selectedPropertyId.value?.let { propertyId -> - amenitiesReturnRoute.value = AppRoute.Home - route.value = AppRoute.Amenities(propertyId) - } + amenitiesReturnRoute.value = AppRoute.Home + route.value = AppRoute.Amenities }, refreshKey = refreshKey.value, selectedPropertyId = selectedPropertyId.value, @@ -134,27 +132,24 @@ class MainActivity : ComponentActivity() { onBack = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) }, onSave = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) } ) - is AppRoute.Amenities -> AmenitiesScreen( - propertyId = currentRoute.propertyId, + AppRoute.Amenities -> AmenitiesScreen( onBack = { route.value = amenitiesReturnRoute.value }, - onAdd = { route.value = AppRoute.AddAmenity(currentRoute.propertyId) }, + onAdd = { route.value = AppRoute.AddAmenity }, canManageAmenities = state.isSuperAdmin, onEdit = { selectedAmenity.value = it - route.value = AppRoute.EditAmenity(currentRoute.propertyId, it.id ?: "") + route.value = AppRoute.EditAmenity(it.id ?: "") } ) - is AppRoute.AddAmenity -> AddAmenityScreen( - propertyId = currentRoute.propertyId, - onBack = { route.value = AppRoute.Amenities(currentRoute.propertyId) }, - onSave = { route.value = AppRoute.Amenities(currentRoute.propertyId) } + AppRoute.AddAmenity -> AddAmenityScreen( + onBack = { route.value = AppRoute.Amenities }, + onSave = { route.value = AppRoute.Amenities } ) is AppRoute.EditAmenity -> EditAmenityScreen( - propertyId = currentRoute.propertyId, amenity = selectedAmenity.value ?: com.android.trisolarispms.data.api.model.AmenityDto(id = currentRoute.amenityId, name = ""), - onBack = { route.value = AppRoute.Amenities(currentRoute.propertyId) }, - onSave = { route.value = AppRoute.Amenities(currentRoute.propertyId) } + onBack = { route.value = AppRoute.Amenities }, + onSave = { route.value = AppRoute.Amenities } ) is AppRoute.AddRoom -> RoomFormScreen( title = "Add Room", diff --git a/app/src/main/java/com/android/trisolarispms/data/api/AmenityApi.kt b/app/src/main/java/com/android/trisolarispms/data/api/AmenityApi.kt index 9376fa5..7b74b26 100644 --- a/app/src/main/java/com/android/trisolarispms/data/api/AmenityApi.kt +++ b/app/src/main/java/com/android/trisolarispms/data/api/AmenityApi.kt @@ -12,25 +12,22 @@ import retrofit2.http.PUT import retrofit2.http.Path interface AmenityApi { - @GET("properties/{propertyId}/amenities") - suspend fun listAmenities(@Path("propertyId") propertyId: String): Response> + @GET("amenities") + suspend fun listAmenities(): Response> - @POST("properties/{propertyId}/amenities") + @POST("amenities") suspend fun createAmenity( - @Path("propertyId") propertyId: String, @Body body: AmenityCreateRequest ): Response - @PUT("properties/{propertyId}/amenities/{amenityId}") + @PUT("amenities/{amenityId}") suspend fun updateAmenity( - @Path("propertyId") propertyId: String, @Path("amenityId") amenityId: String, @Body body: AmenityUpdateRequest ): Response - @DELETE("properties/{propertyId}/amenities/{amenityId}") + @DELETE("amenities/{amenityId}") suspend fun deleteAmenity( - @Path("propertyId") propertyId: String, @Path("amenityId") amenityId: String ): Response } diff --git a/app/src/main/java/com/android/trisolarispms/data/api/model/AmenityModels.kt b/app/src/main/java/com/android/trisolarispms/data/api/model/AmenityModels.kt index dd76f9b..a866859 100644 --- a/app/src/main/java/com/android/trisolarispms/data/api/model/AmenityModels.kt +++ b/app/src/main/java/com/android/trisolarispms/data/api/model/AmenityModels.kt @@ -2,13 +2,19 @@ package com.android.trisolarispms.data.api.model data class AmenityDto( val id: String? = null, - val name: String? = null + val name: String? = null, + val category: String? = null, + val iconKey: String? = null ) data class AmenityCreateRequest( - val name: String + val name: String, + val category: String? = null, + val iconKey: String? = null ) data class AmenityUpdateRequest( - val name: String + val name: String, + val category: String? = null, + val iconKey: String? = null ) diff --git a/app/src/main/java/com/android/trisolarispms/ui/AppRoute.kt b/app/src/main/java/com/android/trisolarispms/ui/AppRoute.kt index 7a23e1e..2c55b38 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/AppRoute.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/AppRoute.kt @@ -10,7 +10,7 @@ sealed interface AppRoute { data class RoomTypes(val propertyId: String) : AppRoute data class AddRoomType(val propertyId: String) : AppRoute data class EditRoomType(val propertyId: String, val roomTypeId: String) : AppRoute - data class Amenities(val propertyId: String) : AppRoute - data class AddAmenity(val propertyId: String) : AppRoute - data class EditAmenity(val propertyId: String, val amenityId: String) : AppRoute + data object Amenities : AppRoute + data object AddAmenity : AppRoute + data class EditAmenity(val amenityId: String) : AppRoute } diff --git a/app/src/main/java/com/android/trisolarispms/ui/home/HomeScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/home/HomeScreen.kt index 2577f29..922e9b2 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/home/HomeScreen.kt @@ -78,7 +78,7 @@ fun HomeScreen( onAddProperty() } ) - if (isSuperAdmin && selectedPropertyId != null) { + if (isSuperAdmin) { DropdownMenuItem( text = { Text("Modify Amenities") }, onClick = { diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomtype/AddAmenityScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomtype/AddAmenityScreen.kt index 6c85102..909e48f 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomtype/AddAmenityScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomtype/AddAmenityScreen.kt @@ -1,5 +1,6 @@ package com.android.trisolarispms.ui.roomtype +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -20,8 +21,10 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -29,12 +32,26 @@ import androidx.lifecycle.viewmodel.compose.viewModel @Composable @OptIn(ExperimentalMaterial3Api::class) fun AddAmenityScreen( - propertyId: String, onBack: () -> Unit, onSave: () -> Unit, - viewModel: AmenityFormViewModel = viewModel() + viewModel: AmenityFormViewModel = viewModel(), + amenityListViewModel: AmenityListViewModel = viewModel() ) { val state by viewModel.state.collectAsState() + val amenityState by amenityListViewModel.state.collectAsState() + + LaunchedEffect(Unit) { + amenityListViewModel.load() + } + + val categorySuggestions = remember(state.category, amenityState.items) { + val all = amenityState.items.mapNotNull { it.category?.trim() } + .filter { it.isNotBlank() } + .distinct() + .sorted() + if (state.category.isBlank()) all + else all.filter { it.contains(state.category, ignoreCase = true) } + } Scaffold( topBar = { @@ -46,7 +63,7 @@ fun AddAmenityScreen( } }, actions = { - IconButton(onClick = { viewModel.submitCreate(propertyId, onSave) }) { + IconButton(onClick = { viewModel.submitCreate(onSave) }) { Icon(Icons.Default.Done, contentDescription = "Save") } }, @@ -67,6 +84,35 @@ fun AddAmenityScreen( label = { Text("Name") }, modifier = Modifier.fillMaxWidth() ) + Spacer(modifier = Modifier.height(12.dp)) + + OutlinedTextField( + value = state.category, + onValueChange = viewModel::onCategoryChange, + label = { Text("Category") }, + modifier = Modifier.fillMaxWidth() + ) + if (categorySuggestions.isNotEmpty()) { + Spacer(modifier = Modifier.height(6.dp)) + categorySuggestions.take(5).forEach { suggestion -> + Text( + text = suggestion, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .padding(vertical = 2.dp) + .clickable { viewModel.onCategoryChange(suggestion) } + ) + } + } + Spacer(modifier = Modifier.height(12.dp)) + + OutlinedTextField( + value = state.iconKey, + onValueChange = viewModel::onIconKeyChange, + label = { Text("Icon Key") }, + modifier = Modifier.fillMaxWidth() + ) state.error?.let { Spacer(modifier = Modifier.height(12.dp)) Text(text = it, color = MaterialTheme.colorScheme.error) diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomtype/AddRoomTypeScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomtype/AddRoomTypeScreen.kt index 44b37e3..d7f61eb 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomtype/AddRoomTypeScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomtype/AddRoomTypeScreen.kt @@ -44,8 +44,8 @@ fun AddRoomTypeScreen( val state by viewModel.state.collectAsState() val amenityState by amenityViewModel.state.collectAsState() - LaunchedEffect(propertyId) { - amenityViewModel.load(propertyId) + LaunchedEffect(Unit) { + amenityViewModel.load() } Scaffold( diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomtype/AmenitiesScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomtype/AmenitiesScreen.kt index 3eddabf..1e40615 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomtype/AmenitiesScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomtype/AmenitiesScreen.kt @@ -33,7 +33,6 @@ import com.android.trisolarispms.data.api.model.AmenityDto @Composable @OptIn(ExperimentalMaterial3Api::class) fun AmenitiesScreen( - propertyId: String, onBack: () -> Unit, onAdd: () -> Unit, canManageAmenities: Boolean, @@ -42,8 +41,8 @@ fun AmenitiesScreen( ) { val state by viewModel.state.collectAsState() - LaunchedEffect(propertyId) { - viewModel.load(propertyId) + LaunchedEffect(Unit) { + viewModel.load() } Scaffold( @@ -100,11 +99,15 @@ fun AmenitiesScreen( .clickable(enabled = canManageAmenities && item.id != null) { onEdit(item) } ) if (canManageAmenities && item.id != null) { - IconButton(onClick = { viewModel.deleteAmenity(propertyId, item.id) }) { + IconButton(onClick = { viewModel.deleteAmenity(item.id) }) { Icon(Icons.Default.Delete, contentDescription = "Delete Amenity") } } } + val meta = listOfNotNull(item.category, item.iconKey).joinToString(" • ") + if (meta.isNotBlank()) { + Text(text = meta, style = MaterialTheme.typography.bodySmall) + } } } } diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomtype/AmenityFormState.kt b/app/src/main/java/com/android/trisolarispms/ui/roomtype/AmenityFormState.kt index 85595ec..f12b68e 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomtype/AmenityFormState.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomtype/AmenityFormState.kt @@ -2,6 +2,8 @@ package com.android.trisolarispms.ui.roomtype data class AmenityFormState( val name: String = "", + val category: String = "", + val iconKey: String = "", val isLoading: Boolean = false, val error: String? = null, val success: Boolean = false diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomtype/AmenityFormViewModel.kt b/app/src/main/java/com/android/trisolarispms/ui/roomtype/AmenityFormViewModel.kt index 5e81c72..b7f3705 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomtype/AmenityFormViewModel.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomtype/AmenityFormViewModel.kt @@ -14,15 +14,30 @@ class AmenityFormViewModel : ViewModel() { private val _state = MutableStateFlow(AmenityFormState()) val state: StateFlow = _state - fun setAmenityName(name: String) { - _state.update { it.copy(name = name, error = null) } + fun setAmenity(amenity: com.android.trisolarispms.data.api.model.AmenityDto) { + _state.update { + it.copy( + name = amenity.name.orEmpty(), + category = amenity.category.orEmpty(), + iconKey = amenity.iconKey.orEmpty(), + error = null + ) + } } fun onNameChange(value: String) { _state.update { it.copy(name = value, error = null) } } - fun submitCreate(propertyId: String, onDone: () -> Unit) { + fun onCategoryChange(value: String) { + _state.update { it.copy(category = value, error = null) } + } + + fun onIconKeyChange(value: String) { + _state.update { it.copy(iconKey = value, error = null) } + } + + fun submitCreate(onDone: () -> Unit) { val name = state.value.name.trim() if (name.isBlank()) { _state.update { it.copy(error = "Name is required") } @@ -32,7 +47,13 @@ class AmenityFormViewModel : ViewModel() { _state.update { it.copy(isLoading = true, error = null) } try { val api = ApiClient.create() - val response = api.createAmenity(propertyId, AmenityCreateRequest(name)) + val response = api.createAmenity( + AmenityCreateRequest( + name = name, + category = state.value.category.trim().ifBlank { null }, + iconKey = state.value.iconKey.trim().ifBlank { null } + ) + ) if (response.isSuccessful) { _state.update { it.copy(isLoading = false, success = true) } onDone() @@ -45,7 +66,7 @@ class AmenityFormViewModel : ViewModel() { } } - fun submitUpdate(propertyId: String, amenityId: String, onDone: () -> Unit) { + fun submitUpdate(amenityId: String, onDone: () -> Unit) { val name = state.value.name.trim() if (name.isBlank()) { _state.update { it.copy(error = "Name is required") } @@ -55,7 +76,14 @@ class AmenityFormViewModel : ViewModel() { _state.update { it.copy(isLoading = true, error = null) } try { val api = ApiClient.create() - val response = api.updateAmenity(propertyId, amenityId, AmenityUpdateRequest(name)) + val response = api.updateAmenity( + amenityId, + AmenityUpdateRequest( + name = name, + category = state.value.category.trim().ifBlank { null }, + iconKey = state.value.iconKey.trim().ifBlank { null } + ) + ) if (response.isSuccessful) { _state.update { it.copy(isLoading = false, success = true) } onDone() diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomtype/AmenityListViewModel.kt b/app/src/main/java/com/android/trisolarispms/ui/roomtype/AmenityListViewModel.kt index 86bc82c..63e43eb 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomtype/AmenityListViewModel.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomtype/AmenityListViewModel.kt @@ -12,13 +12,12 @@ class AmenityListViewModel : ViewModel() { private val _state = MutableStateFlow(AmenityListState()) val state: StateFlow = _state - fun load(propertyId: String) { - if (propertyId.isBlank()) return + fun load() { viewModelScope.launch { _state.update { it.copy(isLoading = true, error = null) } try { val api = ApiClient.create() - val response = api.listAmenities(propertyId) + val response = api.listAmenities() if (response.isSuccessful) { _state.update { it.copy( @@ -36,13 +35,13 @@ class AmenityListViewModel : ViewModel() { } } - fun deleteAmenity(propertyId: String, amenityId: String) { - if (propertyId.isBlank() || amenityId.isBlank()) return + fun deleteAmenity(amenityId: String) { + if (amenityId.isBlank()) return viewModelScope.launch { _state.update { it.copy(isLoading = true, error = null) } try { val api = ApiClient.create() - val response = api.deleteAmenity(propertyId, amenityId) + val response = api.deleteAmenity(amenityId) if (response.isSuccessful) { _state.update { current -> current.copy( diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomtype/EditAmenityScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomtype/EditAmenityScreen.kt index d442fcd..41bd957 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomtype/EditAmenityScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomtype/EditAmenityScreen.kt @@ -1,5 +1,6 @@ package com.android.trisolarispms.ui.roomtype +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -23,6 +24,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -31,16 +33,29 @@ import com.android.trisolarispms.data.api.model.AmenityDto @Composable @OptIn(ExperimentalMaterial3Api::class) fun EditAmenityScreen( - propertyId: String, amenity: AmenityDto, onBack: () -> Unit, onSave: () -> Unit, - viewModel: AmenityFormViewModel = viewModel() + viewModel: AmenityFormViewModel = viewModel(), + amenityListViewModel: AmenityListViewModel = viewModel() ) { val state by viewModel.state.collectAsState() + val amenityState by amenityListViewModel.state.collectAsState() LaunchedEffect(amenity.id) { - viewModel.setAmenityName(amenity.name.orEmpty()) + viewModel.setAmenity(amenity) + } + LaunchedEffect(Unit) { + amenityListViewModel.load() + } + + val categorySuggestions = remember(state.category, amenityState.items) { + val all = amenityState.items.mapNotNull { it.category?.trim() } + .filter { it.isNotBlank() } + .distinct() + .sorted() + if (state.category.isBlank()) all + else all.filter { it.contains(state.category, ignoreCase = true) } } Scaffold( @@ -55,7 +70,7 @@ fun EditAmenityScreen( actions = { IconButton(onClick = { val id = amenity.id.orEmpty() - viewModel.submitUpdate(propertyId, id, onSave) + viewModel.submitUpdate(id, onSave) }) { Icon(Icons.Default.Done, contentDescription = "Save") } @@ -77,6 +92,35 @@ fun EditAmenityScreen( label = { Text("Name") }, modifier = Modifier.fillMaxWidth() ) + Spacer(modifier = Modifier.height(12.dp)) + + OutlinedTextField( + value = state.category, + onValueChange = viewModel::onCategoryChange, + label = { Text("Category") }, + modifier = Modifier.fillMaxWidth() + ) + if (categorySuggestions.isNotEmpty()) { + Spacer(modifier = Modifier.height(6.dp)) + categorySuggestions.take(5).forEach { suggestion -> + Text( + text = suggestion, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .padding(vertical = 2.dp) + .clickable { viewModel.onCategoryChange(suggestion) } + ) + } + } + Spacer(modifier = Modifier.height(12.dp)) + + OutlinedTextField( + value = state.iconKey, + onValueChange = viewModel::onIconKeyChange, + label = { Text("Icon Key") }, + modifier = Modifier.fillMaxWidth() + ) state.error?.let { Spacer(modifier = Modifier.height(12.dp)) Text(text = it, color = MaterialTheme.colorScheme.error) diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomtype/EditRoomTypeScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomtype/EditRoomTypeScreen.kt index 29da60b..d1754f6 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomtype/EditRoomTypeScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomtype/EditRoomTypeScreen.kt @@ -49,8 +49,8 @@ fun EditRoomTypeScreen( LaunchedEffect(roomType.id) { viewModel.setRoomType(roomType) } - LaunchedEffect(propertyId) { - amenityViewModel.load(propertyId) + LaunchedEffect(Unit) { + amenityViewModel.load() } Scaffold(