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,
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",

View File

@@ -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<List<AmenityDto>>
@GET("amenities")
suspend fun listAmenities(): Response<List<AmenityDto>>
@POST("properties/{propertyId}/amenities")
@POST("amenities")
suspend fun createAmenity(
@Path("propertyId") propertyId: String,
@Body body: AmenityCreateRequest
): Response<AmenityDto>
@PUT("properties/{propertyId}/amenities/{amenityId}")
@PUT("amenities/{amenityId}")
suspend fun updateAmenity(
@Path("propertyId") propertyId: String,
@Path("amenityId") amenityId: String,
@Body body: AmenityUpdateRequest
): Response<AmenityDto>
@DELETE("properties/{propertyId}/amenities/{amenityId}")
@DELETE("amenities/{amenityId}")
suspend fun deleteAmenity(
@Path("propertyId") propertyId: String,
@Path("amenityId") amenityId: String
): Response<Unit>
}

View File

@@ -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
)

View File

@@ -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
}

View File

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

View File

@@ -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)

View File

@@ -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(

View File

@@ -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)
}
}
}
}

View File

@@ -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

View File

@@ -14,15 +14,30 @@ class AmenityFormViewModel : ViewModel() {
private val _state = MutableStateFlow(AmenityFormState())
val state: StateFlow<AmenityFormState> = _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()

View File

@@ -12,13 +12,12 @@ class AmenityListViewModel : ViewModel() {
private val _state = MutableStateFlow(AmenityListState())
val state: StateFlow<AmenityListState> = _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(

View File

@@ -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)

View File

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