Room lists filters and room type availability
This commit is contained in:
@@ -56,4 +56,16 @@ interface RoomApi {
|
||||
@Query("from") from: String,
|
||||
@Query("to") to: String
|
||||
): Response<List<RoomAvailabilityRangeResponse>>
|
||||
|
||||
@GET("properties/{propertyId}/rooms/available")
|
||||
suspend fun listAvailableRooms(
|
||||
@Path("propertyId") propertyId: String
|
||||
): Response<List<RoomDto>>
|
||||
|
||||
@GET("properties/{propertyId}/rooms/by-type/{roomTypeCode}")
|
||||
suspend fun listRoomsByType(
|
||||
@Path("propertyId") propertyId: String,
|
||||
@Path("roomTypeCode") roomTypeCode: String,
|
||||
@Query("availableOnly") availableOnly: Boolean? = null
|
||||
): Response<List<RoomDto>>
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ data class RoomDto(
|
||||
val roomNumber: Int? = null,
|
||||
val roomTypeCode: String? = null,
|
||||
val roomTypeName: String? = null,
|
||||
val maxOccupancy: Int? = null,
|
||||
val floor: Int? = null,
|
||||
val hasNfc: Boolean? = null,
|
||||
val active: Boolean? = null,
|
||||
|
||||
@@ -5,5 +5,7 @@ import com.android.trisolarispms.data.api.model.RoomDto
|
||||
data class RoomListState(
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val rooms: List<RoomDto> = emptyList()
|
||||
val rooms: List<RoomDto> = emptyList(),
|
||||
val showAll: Boolean = false,
|
||||
val roomTypeCode: String? = null
|
||||
)
|
||||
|
||||
@@ -12,13 +12,24 @@ class RoomListViewModel : ViewModel() {
|
||||
private val _state = MutableStateFlow(RoomListState())
|
||||
val state: StateFlow<RoomListState> = _state
|
||||
|
||||
fun load(propertyId: String) {
|
||||
fun load(propertyId: String, showAll: Boolean = false, roomTypeCode: String? = null) {
|
||||
if (propertyId.isBlank()) return
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
_state.update { it.copy(isLoading = true, error = null, showAll = showAll, roomTypeCode = roomTypeCode) }
|
||||
try {
|
||||
val api = ApiClient.create()
|
||||
val response = api.listRooms(propertyId)
|
||||
val trimmedCode = roomTypeCode?.trim().orEmpty()
|
||||
val response = if (trimmedCode.isNotBlank()) {
|
||||
api.listRoomsByType(
|
||||
propertyId = propertyId,
|
||||
roomTypeCode = trimmedCode,
|
||||
availableOnly = if (showAll) false else true
|
||||
)
|
||||
} else if (showAll) {
|
||||
api.listRooms(propertyId)
|
||||
} else {
|
||||
api.listAvailableRooms(propertyId)
|
||||
}
|
||||
if (response.isSuccessful) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
@@ -35,4 +46,12 @@ class RoomListViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setShowAll(propertyId: String, showAll: Boolean) {
|
||||
load(propertyId, showAll, _state.value.roomTypeCode)
|
||||
}
|
||||
|
||||
fun setRoomTypeFilter(propertyId: String, roomTypeCode: String?) {
|
||||
load(propertyId, _state.value.showAll, roomTypeCode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
package com.android.trisolarispms.ui.room
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Category
|
||||
import androidx.compose.material.icons.filled.Hotel
|
||||
import androidx.compose.material.icons.filled.Layers
|
||||
import androidx.compose.material.icons.filled.Groups
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -21,6 +33,9 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -29,6 +44,10 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.android.trisolarispms.ui.roomtype.RoomTypeListViewModel
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -39,12 +58,16 @@ fun RoomsScreen(
|
||||
onViewRoomTypes: () -> Unit,
|
||||
canManageRooms: Boolean,
|
||||
onEditRoom: (com.android.trisolarispms.data.api.model.RoomDto) -> Unit,
|
||||
viewModel: RoomListViewModel = viewModel()
|
||||
viewModel: RoomListViewModel = viewModel(),
|
||||
roomTypeListViewModel: RoomTypeListViewModel = viewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val roomTypeState by roomTypeListViewModel.state.collectAsState()
|
||||
val showTypeMenu = remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(propertyId) {
|
||||
viewModel.load(propertyId)
|
||||
viewModel.load(propertyId, showAll = false)
|
||||
roomTypeListViewModel.load(propertyId)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@@ -86,29 +109,153 @@ fun RoomsScreen(
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
if (!state.isLoading && state.error == null) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
) {
|
||||
FilterChip(
|
||||
selected = state.showAll,
|
||||
onClick = { viewModel.setShowAll(propertyId, !state.showAll) },
|
||||
label = { Text(if (state.showAll) "Showing all" else "Showing available") }
|
||||
)
|
||||
Box {
|
||||
val selectedType = state.roomTypeCode
|
||||
FilterChip(
|
||||
selected = !selectedType.isNullOrBlank(),
|
||||
onClick = { showTypeMenu.value = true },
|
||||
label = { Text("Type: ${selectedType ?: "All"}") }
|
||||
)
|
||||
DropdownMenu(
|
||||
expanded = showTypeMenu.value,
|
||||
onDismissRequest = { showTypeMenu.value = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("All types") },
|
||||
onClick = {
|
||||
showTypeMenu.value = false
|
||||
viewModel.setRoomTypeFilter(propertyId, null)
|
||||
}
|
||||
)
|
||||
roomTypeState.items.forEach { type ->
|
||||
val code = type.code ?: return@forEach
|
||||
val label = type.name?.let { "$code • $it" } ?: code
|
||||
DropdownMenuItem(
|
||||
text = { Text(label) },
|
||||
onClick = {
|
||||
showTypeMenu.value = false
|
||||
viewModel.setRoomTypeFilter(propertyId, code)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val availableCount = state.rooms.size
|
||||
Text(
|
||||
text = "Available rooms: $availableCount",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
if (state.rooms.isEmpty()) {
|
||||
Text(text = "No rooms found")
|
||||
} else {
|
||||
state.rooms.forEach { room ->
|
||||
val label = room.roomNumber?.toString() ?: "-"
|
||||
val details = listOfNotNull(
|
||||
room.floor?.let { "Floor $it" },
|
||||
room.roomTypeName ?: room.roomTypeCode
|
||||
).joinToString(" • ")
|
||||
val isDimmed = (room.active == false) || (room.maintenance == true)
|
||||
val alpha = if (isDimmed) 0.5f else 1f
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.alpha(alpha)
|
||||
.clickable(enabled = room.id != null) {
|
||||
onEditRoom(room)
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
items(state.rooms) { room ->
|
||||
val label = room.roomNumber?.toString() ?: "-"
|
||||
val category = room.roomTypeName ?: room.roomTypeCode ?: "Unassigned"
|
||||
val floorLabel = room.floor?.let { "Floor $it" }.orEmpty()
|
||||
val isDimmed = (room.active == false) || (room.maintenance == true)
|
||||
val alpha = if (isDimmed) 0.5f else 1f
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.alpha(alpha)
|
||||
.combinedClickable(
|
||||
enabled = room.id != null,
|
||||
onClick = {},
|
||||
onLongClick = { onEditRoom(room) }
|
||||
),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Hotel,
|
||||
contentDescription = "Room",
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(text = label, style = MaterialTheme.typography.titleMedium)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Category,
|
||||
contentDescription = "Category",
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = category,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
val maxOcc = room.maxOccupancy
|
||||
if (maxOcc != null && maxOcc > 0) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Groups,
|
||||
contentDescription = "Max occupancy",
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = "Max $maxOcc",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
if (floorLabel.isNotBlank()) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Layers,
|
||||
contentDescription = "Floor",
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = floorLabel,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
if (room.maintenance == true) {
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Text(
|
||||
text = "Maintenance",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(vertical = 10.dp)
|
||||
) {
|
||||
Text(text = label, style = MaterialTheme.typography.titleMedium)
|
||||
if (details.isNotBlank()) {
|
||||
Text(text = details, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,7 @@
|
||||
package com.android.trisolarispms.ui.roomtype
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
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.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
|
||||
@Composable
|
||||
@@ -41,122 +13,16 @@ fun AddRoomTypeScreen(
|
||||
viewModel: RoomTypeFormViewModel = viewModel(),
|
||||
amenityViewModel: AmenityListViewModel = viewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val amenityState by amenityViewModel.state.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
amenityViewModel.load()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Add Room Type") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { viewModel.submit(propertyId, onSave) }) {
|
||||
Icon(Icons.Default.Done, contentDescription = "Save")
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors()
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = state.code,
|
||||
onValueChange = viewModel::onCodeChange,
|
||||
label = { Text("Code") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.name,
|
||||
onValueChange = viewModel::onNameChange,
|
||||
label = { Text("Name") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.baseOccupancy,
|
||||
onValueChange = viewModel::onBaseOccupancyChange,
|
||||
label = { Text("Base Occupancy") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.maxOccupancy,
|
||||
onValueChange = viewModel::onMaxOccupancyChange,
|
||||
label = { Text("Max Occupancy") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.sqFeet,
|
||||
onValueChange = viewModel::onSqFeetChange,
|
||||
label = { Text("Sq Ft") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.bathroomSqFeet,
|
||||
onValueChange = viewModel::onBathroomSqFeetChange,
|
||||
label = { Text("Bathroom Sq Ft") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(text = "Amenities", style = MaterialTheme.typography.titleSmall)
|
||||
if (amenityState.items.isEmpty()) {
|
||||
Text(text = "No amenities available", style = MaterialTheme.typography.bodySmall)
|
||||
} else {
|
||||
amenityState.items.forEach { amenity ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(vertical = 4.dp)
|
||||
) {
|
||||
Checkbox(
|
||||
checked = state.amenityIds.contains(amenity.id),
|
||||
onCheckedChange = { amenity.id?.let { viewModel.onAmenityToggle(it) } }
|
||||
)
|
||||
Text(text = amenity.name ?: "", modifier = Modifier.padding(start = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.otaAliases,
|
||||
onValueChange = viewModel::onAliasesChange,
|
||||
label = { Text("OTA Aliases (comma separated)") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
state.error?.let {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
androidx.compose.runtime.LaunchedEffect(Unit) {
|
||||
viewModel.resetForm()
|
||||
}
|
||||
RoomTypeFormScreen(
|
||||
title = "Add Room Type",
|
||||
propertyId = propertyId,
|
||||
onBack = onBack,
|
||||
onSave = { viewModel.submit(propertyId, onSave) },
|
||||
codeEditable = true,
|
||||
viewModel = viewModel,
|
||||
amenityViewModel = amenityViewModel
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,58 +1,29 @@
|
||||
package com.android.trisolarispms.ui.roomtype
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.filled.DragIndicator
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.compose.AsyncImage
|
||||
import com.android.trisolarispms.data.api.ApiConstants
|
||||
import com.android.trisolarispms.data.api.model.RoomTypeDto
|
||||
import com.android.trisolarispms.ui.roomimage.RoomImageGridItem
|
||||
import com.android.trisolarispms.ui.roomimage.ImagePreviewDialog
|
||||
import com.android.trisolarispms.ui.roomimage.ReorderableImageGrid
|
||||
import com.android.trisolarispms.ui.roomimage.RoomImageGridItem
|
||||
import com.android.trisolarispms.ui.roomimage.RoomImageViewModel
|
||||
|
||||
@Composable
|
||||
@@ -66,16 +37,14 @@ fun EditRoomTypeScreen(
|
||||
amenityViewModel: AmenityListViewModel = viewModel(),
|
||||
roomImageViewModel: RoomImageViewModel = viewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val amenityState by amenityViewModel.state.collectAsState()
|
||||
val roomImageState by roomImageViewModel.state.collectAsState()
|
||||
val showDeleteConfirm = remember { mutableStateOf(false) }
|
||||
val orderedImages = remember { mutableStateOf<List<com.android.trisolarispms.data.api.model.ImageDto>>(emptyList()) }
|
||||
val originalOrderIds = remember { mutableStateOf<List<String>>(emptyList()) }
|
||||
val previewUrl = remember { mutableStateOf<String?>(null) }
|
||||
val showAmenityDialog = remember { mutableStateOf(false) }
|
||||
val amenitySearch = remember { mutableStateOf("") }
|
||||
val gridState = rememberLazyGridState()
|
||||
|
||||
val gridState = androidx.compose.foundation.lazy.grid.rememberLazyGridState()
|
||||
|
||||
val saveRoomTypeImageOrder = remember(roomType.code, orderedImages.value) {
|
||||
{
|
||||
val code = roomType.code.orEmpty()
|
||||
@@ -101,9 +70,6 @@ fun EditRoomTypeScreen(
|
||||
LaunchedEffect(roomType.id) {
|
||||
viewModel.setRoomType(roomType)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
amenityViewModel.load()
|
||||
}
|
||||
LaunchedEffect(roomType.code) {
|
||||
val code = roomType.code.orEmpty()
|
||||
if (code.isNotBlank()) {
|
||||
@@ -115,159 +81,55 @@ fun EditRoomTypeScreen(
|
||||
originalOrderIds.value = roomImageState.images.mapNotNull { it.id }
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Modify Room Type") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { viewModel.submitUpdate(propertyId, roomType.id.orEmpty(), onSave) }) {
|
||||
Icon(Icons.Default.Done, contentDescription = "Save")
|
||||
}
|
||||
if (!roomType.id.isNullOrBlank()) {
|
||||
IconButton(onClick = { showDeleteConfirm.value = true }) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Delete Room Type")
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors()
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(24.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.Top
|
||||
RoomTypeFormScreen(
|
||||
title = "Modify Room Type",
|
||||
propertyId = propertyId,
|
||||
onBack = onBack,
|
||||
onSave = { viewModel.submitUpdate(propertyId, roomType.id.orEmpty(), onSave) },
|
||||
codeEditable = false,
|
||||
onDelete = { if (!roomType.id.isNullOrBlank()) showDeleteConfirm.value = true },
|
||||
viewModel = viewModel,
|
||||
amenityViewModel = amenityViewModel
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = state.code,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Code") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.name,
|
||||
onValueChange = viewModel::onNameChange,
|
||||
label = { Text("Name") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.baseOccupancy,
|
||||
onValueChange = viewModel::onBaseOccupancyChange,
|
||||
label = { Text("Base Occupancy") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.maxOccupancy,
|
||||
onValueChange = viewModel::onMaxOccupancyChange,
|
||||
label = { Text("Max Occupancy") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.sqFeet,
|
||||
onValueChange = viewModel::onSqFeetChange,
|
||||
label = { Text("Sq Ft") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.bathroomSqFeet,
|
||||
onValueChange = viewModel::onBathroomSqFeetChange,
|
||||
label = { Text("Bathroom Sq Ft") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.otaAliases,
|
||||
onValueChange = viewModel::onAliasesChange,
|
||||
label = { Text("OTA Aliases (comma separated)") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = "Amenities", style = MaterialTheme.typography.titleSmall)
|
||||
Button(onClick = { showAmenityDialog.value = true }) {
|
||||
Text("Edit")
|
||||
Text(text = "Room Type Images", style = androidx.compose.material3.MaterialTheme.typography.titleSmall)
|
||||
val hasOrderChanged = orderedImages.value.mapNotNull { it.id } != originalOrderIds.value
|
||||
if (orderedImages.value.isNotEmpty() && hasOrderChanged) {
|
||||
Button(onClick = saveRoomTypeImageOrder) {
|
||||
Text("Save order")
|
||||
}
|
||||
}
|
||||
val selectedCount = state.amenityIds.size
|
||||
Text(
|
||||
text = if (selectedCount == 0) "No amenities selected" else "$selectedCount selected",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = "Room Type Images", style = MaterialTheme.typography.titleSmall)
|
||||
val hasOrderChanged = orderedImages.value.mapNotNull { it.id } != originalOrderIds.value
|
||||
if (orderedImages.value.isNotEmpty() && hasOrderChanged) {
|
||||
Button(onClick = saveRoomTypeImageOrder) {
|
||||
Text("Save order")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (roomImageState.isLoading) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
androidx.compose.material3.CircularProgressIndicator()
|
||||
} else if (orderedImages.value.isEmpty()) {
|
||||
Text(text = "No images yet", style = MaterialTheme.typography.bodySmall)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
ReorderableImageGrid(
|
||||
images = orderedImages.value,
|
||||
gridState = gridState,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(320.dp),
|
||||
onOrderChange = { list ->
|
||||
orderedImages.value = list
|
||||
},
|
||||
onDragEnd = {}
|
||||
) { image, dragHandleModifier, _ ->
|
||||
RoomImageGridItem(
|
||||
image = image,
|
||||
modifier = dragHandleModifier,
|
||||
onPreview = { previewUrl.value = image.url ?: image.thumbnailUrl },
|
||||
showTags = true
|
||||
)
|
||||
}
|
||||
}
|
||||
state.error?.let {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
if (roomImageState.isLoading) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
androidx.compose.material3.CircularProgressIndicator()
|
||||
} else if (orderedImages.value.isEmpty()) {
|
||||
Text(text = "No images yet", style = androidx.compose.material3.MaterialTheme.typography.bodySmall)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
ReorderableImageGrid(
|
||||
images = orderedImages.value,
|
||||
gridState = gridState,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(320.dp),
|
||||
onOrderChange = { list ->
|
||||
orderedImages.value = list
|
||||
},
|
||||
onDragEnd = {}
|
||||
) { image, dragHandleModifier, _ ->
|
||||
RoomImageGridItem(
|
||||
image = image,
|
||||
modifier = dragHandleModifier,
|
||||
onPreview = { previewUrl.value = image.url ?: image.thumbnailUrl },
|
||||
showTags = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,90 +155,9 @@ fun EditRoomTypeScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (showAmenityDialog.value) {
|
||||
val query = amenitySearch.value.trim()
|
||||
val filtered = if (query.isBlank()) {
|
||||
amenityState.items
|
||||
} else {
|
||||
amenityState.items.filter {
|
||||
val name = it.name.orEmpty()
|
||||
val category = it.category.orEmpty()
|
||||
name.contains(query, ignoreCase = true) || category.contains(query, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
AlertDialog(
|
||||
onDismissRequest = { showAmenityDialog.value = false },
|
||||
title = { Text("Select Amenities") },
|
||||
text = {
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = amenitySearch.value,
|
||||
onValueChange = { amenitySearch.value = it },
|
||||
label = { Text("Search") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
if (amenityState.items.isEmpty()) {
|
||||
Text(text = "No amenities available", style = MaterialTheme.typography.bodySmall)
|
||||
} else if (filtered.isEmpty()) {
|
||||
Text(text = "No matches", style = MaterialTheme.typography.bodySmall)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(320.dp)
|
||||
) {
|
||||
items(filtered) { amenity ->
|
||||
val id = amenity.id ?: return@items
|
||||
val iconKey = amenity.iconKey.orEmpty().removeSuffix(".png")
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = state.amenityIds.contains(id),
|
||||
onCheckedChange = { viewModel.onAmenityToggle(id) }
|
||||
)
|
||||
if (iconKey.isNotBlank()) {
|
||||
AsyncImage(
|
||||
model = "${ApiConstants.BASE_URL}icons/png/$iconKey.png",
|
||||
contentDescription = amenity.name ?: iconKey,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.padding(end = 8.dp)
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(text = amenity.name.orEmpty())
|
||||
if (!amenity.category.isNullOrBlank()) {
|
||||
Text(
|
||||
text = amenity.category.orEmpty(),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { showAmenityDialog.value = false }) {
|
||||
Text("Done")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val preview = previewUrl.value
|
||||
if (!preview.isNullOrBlank()) {
|
||||
com.android.trisolarispms.ui.roomimage.ImagePreviewDialog(
|
||||
ImagePreviewDialog(
|
||||
imageUrl = preview,
|
||||
onDismiss = { previewUrl.value = null }
|
||||
)
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
package com.android.trisolarispms.ui.roomtype
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
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 coil.compose.AsyncImage
|
||||
import com.android.trisolarispms.data.api.ApiConstants
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun RoomTypeFormScreen(
|
||||
title: String,
|
||||
propertyId: String,
|
||||
onBack: () -> Unit,
|
||||
onSave: () -> Unit,
|
||||
codeEditable: Boolean,
|
||||
onDelete: (() -> Unit)? = null,
|
||||
viewModel: RoomTypeFormViewModel = viewModel(),
|
||||
amenityViewModel: AmenityListViewModel = viewModel(),
|
||||
extraContent: @Composable (() -> Unit)? = null
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val amenityState by amenityViewModel.state.collectAsState()
|
||||
val showAmenityDialog = remember { mutableStateOf(false) }
|
||||
val amenitySearch = remember { mutableStateOf("") }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
amenityViewModel.load()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(title) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onSave) {
|
||||
Icon(Icons.Default.Done, contentDescription = "Save")
|
||||
}
|
||||
if (onDelete != null) {
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Delete Room Type")
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors()
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(24.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = state.code,
|
||||
onValueChange = { value ->
|
||||
if (codeEditable) {
|
||||
viewModel.onCodeChange(value)
|
||||
}
|
||||
},
|
||||
readOnly = !codeEditable,
|
||||
label = { Text("Code") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.name,
|
||||
onValueChange = viewModel::onNameChange,
|
||||
label = { Text("Name") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.baseOccupancy,
|
||||
onValueChange = viewModel::onBaseOccupancyChange,
|
||||
label = { Text("Base Occupancy") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.maxOccupancy,
|
||||
onValueChange = viewModel::onMaxOccupancyChange,
|
||||
label = { Text("Max Occupancy") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.sqFeet,
|
||||
onValueChange = viewModel::onSqFeetChange,
|
||||
label = { Text("Sq Ft") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.bathroomSqFeet,
|
||||
onValueChange = viewModel::onBathroomSqFeetChange,
|
||||
label = { Text("Bathroom Sq Ft") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = "Amenities", style = MaterialTheme.typography.titleSmall)
|
||||
Button(onClick = { showAmenityDialog.value = true }) {
|
||||
Text("Edit")
|
||||
}
|
||||
}
|
||||
val selectedCount = state.amenityIds.size
|
||||
Text(
|
||||
text = if (selectedCount == 0) "No amenities selected" else "$selectedCount selected",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.otaAliases,
|
||||
onValueChange = viewModel::onAliasesChange,
|
||||
label = { Text("OTA Aliases (comma separated)") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
extraContent?.let {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
it()
|
||||
}
|
||||
|
||||
state.error?.let {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showAmenityDialog.value) {
|
||||
val query = amenitySearch.value.trim()
|
||||
val filtered = if (query.isBlank()) {
|
||||
amenityState.items
|
||||
} else {
|
||||
amenityState.items.filter {
|
||||
val name = it.name.orEmpty()
|
||||
val category = it.category.orEmpty()
|
||||
name.contains(query, ignoreCase = true) || category.contains(query, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
AlertDialog(
|
||||
onDismissRequest = { showAmenityDialog.value = false },
|
||||
title = { Text("Select Amenities") },
|
||||
text = {
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = amenitySearch.value,
|
||||
onValueChange = { amenitySearch.value = it },
|
||||
label = { Text("Search") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
if (amenityState.items.isEmpty()) {
|
||||
Text(text = "No amenities available", style = MaterialTheme.typography.bodySmall)
|
||||
} else if (filtered.isEmpty()) {
|
||||
Text(text = "No matches", style = MaterialTheme.typography.bodySmall)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(320.dp)
|
||||
) {
|
||||
items(filtered) { amenity ->
|
||||
val id = amenity.id ?: return@items
|
||||
val iconKey = amenity.iconKey.orEmpty().removeSuffix(".png")
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = state.amenityIds.contains(id),
|
||||
onCheckedChange = { viewModel.onAmenityToggle(id) }
|
||||
)
|
||||
if (iconKey.isNotBlank()) {
|
||||
AsyncImage(
|
||||
model = "${ApiConstants.BASE_URL}icons/png/$iconKey.png",
|
||||
contentDescription = amenity.name ?: iconKey,
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.padding(end = 8.dp)
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(text = amenity.name.orEmpty())
|
||||
if (!amenity.category.isNullOrBlank()) {
|
||||
Text(
|
||||
text = amenity.category.orEmpty(),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { showAmenityDialog.value = false }) {
|
||||
Text("Done")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,10 @@ class RoomTypeFormViewModel : ViewModel() {
|
||||
private val _state = MutableStateFlow(RoomTypeFormState())
|
||||
val state: StateFlow<RoomTypeFormState> = _state
|
||||
|
||||
fun resetForm() {
|
||||
_state.update { RoomTypeFormState() }
|
||||
}
|
||||
|
||||
fun setRoomType(type: com.android.trisolarispms.data.api.model.RoomTypeDto) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
|
||||
@@ -6,5 +6,6 @@ data class RoomTypeListState(
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val items: List<RoomTypeDto> = emptyList(),
|
||||
val imageByTypeCode: Map<String, com.android.trisolarispms.data.api.model.ImageDto?> = emptyMap()
|
||||
val imageByTypeCode: Map<String, com.android.trisolarispms.data.api.model.ImageDto?> = emptyMap(),
|
||||
val availableCountByTypeCode: Map<String, Int> = emptyMap()
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ class RoomTypeListViewModel : ViewModel() {
|
||||
)
|
||||
}
|
||||
loadRoomTypeImages(propertyId, items)
|
||||
loadRoomTypeAvailableCounts(propertyId, items)
|
||||
} else {
|
||||
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
|
||||
}
|
||||
@@ -59,4 +60,30 @@ class RoomTypeListViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadRoomTypeAvailableCounts(propertyId: String, items: List<com.android.trisolarispms.data.api.model.RoomTypeDto>) {
|
||||
viewModelScope.launch {
|
||||
val api = ApiClient.create()
|
||||
val updates = mutableMapOf<String, Int>()
|
||||
for (item in items) {
|
||||
val code = item.code?.trim().orEmpty()
|
||||
if (code.isBlank()) continue
|
||||
try {
|
||||
val response = api.listRoomsByType(
|
||||
propertyId = propertyId,
|
||||
roomTypeCode = code,
|
||||
availableOnly = true
|
||||
)
|
||||
if (response.isSuccessful) {
|
||||
updates[code] = response.body().orEmpty().size
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// Ignore per-item failures.
|
||||
}
|
||||
}
|
||||
if (updates.isNotEmpty()) {
|
||||
_state.update { it.copy(availableCountByTypeCode = it.availableCountByTypeCode + updates) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.AspectRatio
|
||||
import androidx.compose.material.icons.filled.Bed
|
||||
import androidx.compose.material.icons.filled.Groups
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -105,11 +108,61 @@ fun RoomTypesScreen(
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "${item.code} • ${item.name}",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = "${item.code} • ${item.name}",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
item.maxOccupancy?.let { max ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Groups,
|
||||
contentDescription = "Max occupancy",
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = max.toString(),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
state.availableCountByTypeCode[item.code.orEmpty()]?.let { count ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bed,
|
||||
contentDescription = "Available rooms",
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = count.toString(),
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
item.sqFeet?.let { sq ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.AspectRatio,
|
||||
contentDescription = "Room size",
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = "${sq} sq ft",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user