Update amenities API and category suggestions
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ fun HomeScreen(
|
||||
onAddProperty()
|
||||
}
|
||||
)
|
||||
if (isSuperAdmin && selectedPropertyId != null) {
|
||||
if (isSuperAdmin) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Modify Amenities") },
|
||||
onClick = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -49,8 +49,8 @@ fun EditRoomTypeScreen(
|
||||
LaunchedEffect(roomType.id) {
|
||||
viewModel.setRoomType(roomType)
|
||||
}
|
||||
LaunchedEffect(propertyId) {
|
||||
amenityViewModel.load(propertyId)
|
||||
LaunchedEffect(Unit) {
|
||||
amenityViewModel.load()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
|
||||
Reference in New Issue
Block a user