Update amenities API and category suggestions

This commit is contained in:
androidlover5842
2026-01-27 04:55:28 +05:30
parent 94eb4f9be4
commit 4642102ff5
13 changed files with 177 additions and 57 deletions

View File

@@ -65,10 +65,8 @@ class MainActivity : ComponentActivity() {
isSuperAdmin = state.isSuperAdmin, isSuperAdmin = state.isSuperAdmin,
onAddProperty = { route.value = AppRoute.AddProperty }, onAddProperty = { route.value = AppRoute.AddProperty },
onAmenities = { onAmenities = {
selectedPropertyId.value?.let { propertyId ->
amenitiesReturnRoute.value = AppRoute.Home amenitiesReturnRoute.value = AppRoute.Home
route.value = AppRoute.Amenities(propertyId) route.value = AppRoute.Amenities
}
}, },
refreshKey = refreshKey.value, refreshKey = refreshKey.value,
selectedPropertyId = selectedPropertyId.value, selectedPropertyId = selectedPropertyId.value,
@@ -134,27 +132,24 @@ class MainActivity : ComponentActivity() {
onBack = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) }, onBack = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) },
onSave = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) } onSave = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) }
) )
is AppRoute.Amenities -> AmenitiesScreen( AppRoute.Amenities -> AmenitiesScreen(
propertyId = currentRoute.propertyId,
onBack = { route.value = amenitiesReturnRoute.value }, onBack = { route.value = amenitiesReturnRoute.value },
onAdd = { route.value = AppRoute.AddAmenity(currentRoute.propertyId) }, onAdd = { route.value = AppRoute.AddAmenity },
canManageAmenities = state.isSuperAdmin, canManageAmenities = state.isSuperAdmin,
onEdit = { onEdit = {
selectedAmenity.value = it selectedAmenity.value = it
route.value = AppRoute.EditAmenity(currentRoute.propertyId, it.id ?: "") route.value = AppRoute.EditAmenity(it.id ?: "")
} }
) )
is AppRoute.AddAmenity -> AddAmenityScreen( AppRoute.AddAmenity -> AddAmenityScreen(
propertyId = currentRoute.propertyId, onBack = { route.value = AppRoute.Amenities },
onBack = { route.value = AppRoute.Amenities(currentRoute.propertyId) }, onSave = { route.value = AppRoute.Amenities }
onSave = { route.value = AppRoute.Amenities(currentRoute.propertyId) }
) )
is AppRoute.EditAmenity -> EditAmenityScreen( is AppRoute.EditAmenity -> EditAmenityScreen(
propertyId = currentRoute.propertyId,
amenity = selectedAmenity.value amenity = selectedAmenity.value
?: com.android.trisolarispms.data.api.model.AmenityDto(id = currentRoute.amenityId, name = ""), ?: com.android.trisolarispms.data.api.model.AmenityDto(id = currentRoute.amenityId, name = ""),
onBack = { route.value = AppRoute.Amenities(currentRoute.propertyId) }, onBack = { route.value = AppRoute.Amenities },
onSave = { route.value = AppRoute.Amenities(currentRoute.propertyId) } onSave = { route.value = AppRoute.Amenities }
) )
is AppRoute.AddRoom -> RoomFormScreen( is AppRoute.AddRoom -> RoomFormScreen(
title = "Add Room", title = "Add Room",

View File

@@ -12,25 +12,22 @@ import retrofit2.http.PUT
import retrofit2.http.Path import retrofit2.http.Path
interface AmenityApi { interface AmenityApi {
@GET("properties/{propertyId}/amenities") @GET("amenities")
suspend fun listAmenities(@Path("propertyId") propertyId: String): Response<List<AmenityDto>> suspend fun listAmenities(): Response<List<AmenityDto>>
@POST("properties/{propertyId}/amenities") @POST("amenities")
suspend fun createAmenity( suspend fun createAmenity(
@Path("propertyId") propertyId: String,
@Body body: AmenityCreateRequest @Body body: AmenityCreateRequest
): Response<AmenityDto> ): Response<AmenityDto>
@PUT("properties/{propertyId}/amenities/{amenityId}") @PUT("amenities/{amenityId}")
suspend fun updateAmenity( suspend fun updateAmenity(
@Path("propertyId") propertyId: String,
@Path("amenityId") amenityId: String, @Path("amenityId") amenityId: String,
@Body body: AmenityUpdateRequest @Body body: AmenityUpdateRequest
): Response<AmenityDto> ): Response<AmenityDto>
@DELETE("properties/{propertyId}/amenities/{amenityId}") @DELETE("amenities/{amenityId}")
suspend fun deleteAmenity( suspend fun deleteAmenity(
@Path("propertyId") propertyId: String,
@Path("amenityId") amenityId: String @Path("amenityId") amenityId: String
): Response<Unit> ): Response<Unit>
} }

View File

@@ -2,13 +2,19 @@ package com.android.trisolarispms.data.api.model
data class AmenityDto( data class AmenityDto(
val id: String? = null, val id: String? = null,
val name: String? = null val name: String? = null,
val category: String? = null,
val iconKey: String? = null
) )
data class AmenityCreateRequest( data class AmenityCreateRequest(
val name: String val name: String,
val category: String? = null,
val iconKey: String? = null
) )
data class AmenityUpdateRequest( data class AmenityUpdateRequest(
val name: String val name: String,
val category: String? = null,
val iconKey: String? = null
) )

View File

@@ -10,7 +10,7 @@ sealed interface AppRoute {
data class RoomTypes(val propertyId: String) : AppRoute data class RoomTypes(val propertyId: String) : AppRoute
data class AddRoomType(val propertyId: String) : AppRoute data class AddRoomType(val propertyId: String) : AppRoute
data class EditRoomType(val propertyId: String, val roomTypeId: String) : AppRoute data class EditRoomType(val propertyId: String, val roomTypeId: String) : AppRoute
data class Amenities(val propertyId: String) : AppRoute data object Amenities : AppRoute
data class AddAmenity(val propertyId: String) : AppRoute data object AddAmenity : AppRoute
data class EditAmenity(val propertyId: String, val amenityId: String) : AppRoute data class EditAmenity(val amenityId: String) : AppRoute
} }

View File

@@ -78,7 +78,7 @@ fun HomeScreen(
onAddProperty() onAddProperty()
} }
) )
if (isSuperAdmin && selectedPropertyId != null) { if (isSuperAdmin) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Modify Amenities") }, text = { Text("Modify Amenities") },
onClick = { onClick = {

View File

@@ -1,5 +1,6 @@
package com.android.trisolarispms.ui.roomtype package com.android.trisolarispms.ui.roomtype
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -20,8 +21,10 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@@ -29,12 +32,26 @@ import androidx.lifecycle.viewmodel.compose.viewModel
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
fun AddAmenityScreen( fun AddAmenityScreen(
propertyId: String,
onBack: () -> Unit, onBack: () -> Unit,
onSave: () -> Unit, onSave: () -> Unit,
viewModel: AmenityFormViewModel = viewModel() viewModel: AmenityFormViewModel = viewModel(),
amenityListViewModel: AmenityListViewModel = viewModel()
) { ) {
val state by viewModel.state.collectAsState() 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( Scaffold(
topBar = { topBar = {
@@ -46,7 +63,7 @@ fun AddAmenityScreen(
} }
}, },
actions = { actions = {
IconButton(onClick = { viewModel.submitCreate(propertyId, onSave) }) { IconButton(onClick = { viewModel.submitCreate(onSave) }) {
Icon(Icons.Default.Done, contentDescription = "Save") Icon(Icons.Default.Done, contentDescription = "Save")
} }
}, },
@@ -67,6 +84,35 @@ fun AddAmenityScreen(
label = { Text("Name") }, label = { Text("Name") },
modifier = Modifier.fillMaxWidth() 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 { state.error?.let {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Text(text = it, color = MaterialTheme.colorScheme.error) Text(text = it, color = MaterialTheme.colorScheme.error)

View File

@@ -44,8 +44,8 @@ fun AddRoomTypeScreen(
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val amenityState by amenityViewModel.state.collectAsState() val amenityState by amenityViewModel.state.collectAsState()
LaunchedEffect(propertyId) { LaunchedEffect(Unit) {
amenityViewModel.load(propertyId) amenityViewModel.load()
} }
Scaffold( Scaffold(

View File

@@ -33,7 +33,6 @@ import com.android.trisolarispms.data.api.model.AmenityDto
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
fun AmenitiesScreen( fun AmenitiesScreen(
propertyId: String,
onBack: () -> Unit, onBack: () -> Unit,
onAdd: () -> Unit, onAdd: () -> Unit,
canManageAmenities: Boolean, canManageAmenities: Boolean,
@@ -42,8 +41,8 @@ fun AmenitiesScreen(
) { ) {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
LaunchedEffect(propertyId) { LaunchedEffect(Unit) {
viewModel.load(propertyId) viewModel.load()
} }
Scaffold( Scaffold(
@@ -100,11 +99,15 @@ fun AmenitiesScreen(
.clickable(enabled = canManageAmenities && item.id != null) { onEdit(item) } .clickable(enabled = canManageAmenities && item.id != null) { onEdit(item) }
) )
if (canManageAmenities && item.id != null) { 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") 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)
}
} }
} }
} }

View File

@@ -2,6 +2,8 @@ package com.android.trisolarispms.ui.roomtype
data class AmenityFormState( data class AmenityFormState(
val name: String = "", val name: String = "",
val category: String = "",
val iconKey: String = "",
val isLoading: Boolean = false, val isLoading: Boolean = false,
val error: String? = null, val error: String? = null,
val success: Boolean = false val success: Boolean = false

View File

@@ -14,15 +14,30 @@ class AmenityFormViewModel : ViewModel() {
private val _state = MutableStateFlow(AmenityFormState()) private val _state = MutableStateFlow(AmenityFormState())
val state: StateFlow<AmenityFormState> = _state val state: StateFlow<AmenityFormState> = _state
fun setAmenityName(name: String) { fun setAmenity(amenity: com.android.trisolarispms.data.api.model.AmenityDto) {
_state.update { it.copy(name = name, error = null) } _state.update {
it.copy(
name = amenity.name.orEmpty(),
category = amenity.category.orEmpty(),
iconKey = amenity.iconKey.orEmpty(),
error = null
)
}
} }
fun onNameChange(value: String) { fun onNameChange(value: String) {
_state.update { it.copy(name = value, error = null) } _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() val name = state.value.name.trim()
if (name.isBlank()) { if (name.isBlank()) {
_state.update { it.copy(error = "Name is required") } _state.update { it.copy(error = "Name is required") }
@@ -32,7 +47,13 @@ class AmenityFormViewModel : ViewModel() {
_state.update { it.copy(isLoading = true, error = null) } _state.update { it.copy(isLoading = true, error = null) }
try { try {
val api = ApiClient.create() 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) { if (response.isSuccessful) {
_state.update { it.copy(isLoading = false, success = true) } _state.update { it.copy(isLoading = false, success = true) }
onDone() 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() val name = state.value.name.trim()
if (name.isBlank()) { if (name.isBlank()) {
_state.update { it.copy(error = "Name is required") } _state.update { it.copy(error = "Name is required") }
@@ -55,7 +76,14 @@ class AmenityFormViewModel : ViewModel() {
_state.update { it.copy(isLoading = true, error = null) } _state.update { it.copy(isLoading = true, error = null) }
try { try {
val api = ApiClient.create() 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) { if (response.isSuccessful) {
_state.update { it.copy(isLoading = false, success = true) } _state.update { it.copy(isLoading = false, success = true) }
onDone() onDone()

View File

@@ -12,13 +12,12 @@ class AmenityListViewModel : ViewModel() {
private val _state = MutableStateFlow(AmenityListState()) private val _state = MutableStateFlow(AmenityListState())
val state: StateFlow<AmenityListState> = _state val state: StateFlow<AmenityListState> = _state
fun load(propertyId: String) { fun load() {
if (propertyId.isBlank()) return
viewModelScope.launch { viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) } _state.update { it.copy(isLoading = true, error = null) }
try { try {
val api = ApiClient.create() val api = ApiClient.create()
val response = api.listAmenities(propertyId) val response = api.listAmenities()
if (response.isSuccessful) { if (response.isSuccessful) {
_state.update { _state.update {
it.copy( it.copy(
@@ -36,13 +35,13 @@ class AmenityListViewModel : ViewModel() {
} }
} }
fun deleteAmenity(propertyId: String, amenityId: String) { fun deleteAmenity(amenityId: String) {
if (propertyId.isBlank() || amenityId.isBlank()) return if (amenityId.isBlank()) return
viewModelScope.launch { viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) } _state.update { it.copy(isLoading = true, error = null) }
try { try {
val api = ApiClient.create() val api = ApiClient.create()
val response = api.deleteAmenity(propertyId, amenityId) val response = api.deleteAmenity(amenityId)
if (response.isSuccessful) { if (response.isSuccessful) {
_state.update { current -> _state.update { current ->
current.copy( current.copy(

View File

@@ -1,5 +1,6 @@
package com.android.trisolarispms.ui.roomtype package com.android.trisolarispms.ui.roomtype
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -23,6 +24,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@@ -31,16 +33,29 @@ import com.android.trisolarispms.data.api.model.AmenityDto
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
fun EditAmenityScreen( fun EditAmenityScreen(
propertyId: String,
amenity: AmenityDto, amenity: AmenityDto,
onBack: () -> Unit, onBack: () -> Unit,
onSave: () -> Unit, onSave: () -> Unit,
viewModel: AmenityFormViewModel = viewModel() viewModel: AmenityFormViewModel = viewModel(),
amenityListViewModel: AmenityListViewModel = viewModel()
) { ) {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val amenityState by amenityListViewModel.state.collectAsState()
LaunchedEffect(amenity.id) { 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( Scaffold(
@@ -55,7 +70,7 @@ fun EditAmenityScreen(
actions = { actions = {
IconButton(onClick = { IconButton(onClick = {
val id = amenity.id.orEmpty() val id = amenity.id.orEmpty()
viewModel.submitUpdate(propertyId, id, onSave) viewModel.submitUpdate(id, onSave)
}) { }) {
Icon(Icons.Default.Done, contentDescription = "Save") Icon(Icons.Default.Done, contentDescription = "Save")
} }
@@ -77,6 +92,35 @@ fun EditAmenityScreen(
label = { Text("Name") }, label = { Text("Name") },
modifier = Modifier.fillMaxWidth() 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 { state.error?.let {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Text(text = it, color = MaterialTheme.colorScheme.error) Text(text = it, color = MaterialTheme.colorScheme.error)

View File

@@ -49,8 +49,8 @@ fun EditRoomTypeScreen(
LaunchedEffect(roomType.id) { LaunchedEffect(roomType.id) {
viewModel.setRoomType(roomType) viewModel.setRoomType(roomType)
} }
LaunchedEffect(propertyId) { LaunchedEffect(Unit) {
amenityViewModel.load(propertyId) amenityViewModel.load()
} }
Scaffold( Scaffold(