Room lists filters and room type availability

This commit is contained in:
androidlover5842
2026-01-28 05:42:23 +05:30
parent d2d60b5074
commit 6d7edc3022
12 changed files with 636 additions and 448 deletions

View File

@@ -56,4 +56,16 @@ interface RoomApi {
@Query("from") from: String, @Query("from") from: String,
@Query("to") to: String @Query("to") to: String
): Response<List<RoomAvailabilityRangeResponse>> ): 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>>
} }

View File

@@ -25,6 +25,7 @@ data class RoomDto(
val roomNumber: Int? = null, val roomNumber: Int? = null,
val roomTypeCode: String? = null, val roomTypeCode: String? = null,
val roomTypeName: String? = null, val roomTypeName: String? = null,
val maxOccupancy: Int? = null,
val floor: Int? = null, val floor: Int? = null,
val hasNfc: Boolean? = null, val hasNfc: Boolean? = null,
val active: Boolean? = null, val active: Boolean? = null,

View File

@@ -5,5 +5,7 @@ import com.android.trisolarispms.data.api.model.RoomDto
data class RoomListState( data class RoomListState(
val isLoading: Boolean = false, val isLoading: Boolean = false,
val error: String? = null, val error: String? = null,
val rooms: List<RoomDto> = emptyList() val rooms: List<RoomDto> = emptyList(),
val showAll: Boolean = false,
val roomTypeCode: String? = null
) )

View File

@@ -12,13 +12,24 @@ class RoomListViewModel : ViewModel() {
private val _state = MutableStateFlow(RoomListState()) private val _state = MutableStateFlow(RoomListState())
val state: StateFlow<RoomListState> = _state val state: StateFlow<RoomListState> = _state
fun load(propertyId: String) { fun load(propertyId: String, showAll: Boolean = false, roomTypeCode: String? = null) {
if (propertyId.isBlank()) return if (propertyId.isBlank()) return
viewModelScope.launch { viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) } _state.update { it.copy(isLoading = true, error = null, showAll = showAll, roomTypeCode = roomTypeCode) }
try { try {
val api = ApiClient.create() 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) { if (response.isSuccessful) {
_state.update { _state.update {
it.copy( 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)
}
} }

View File

@@ -1,18 +1,30 @@
package com.android.trisolarispms.ui.room package com.android.trisolarispms.ui.room
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
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.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Category 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.CircularProgressIndicator
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@@ -21,6 +33,9 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text 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.material3.FilterChip
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.runtime.Composable 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
@@ -29,6 +44,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel 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 @Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -39,12 +58,16 @@ fun RoomsScreen(
onViewRoomTypes: () -> Unit, onViewRoomTypes: () -> Unit,
canManageRooms: Boolean, canManageRooms: Boolean,
onEditRoom: (com.android.trisolarispms.data.api.model.RoomDto) -> Unit, 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 state by viewModel.state.collectAsState()
val roomTypeState by roomTypeListViewModel.state.collectAsState()
val showTypeMenu = remember { mutableStateOf(false) }
LaunchedEffect(propertyId) { LaunchedEffect(propertyId) {
viewModel.load(propertyId) viewModel.load(propertyId, showAll = false)
roomTypeListViewModel.load(propertyId)
} }
Scaffold( Scaffold(
@@ -86,29 +109,153 @@ fun RoomsScreen(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
if (!state.isLoading && state.error == null) { 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()) { if (state.rooms.isEmpty()) {
Text(text = "No rooms found") Text(text = "No rooms found")
} else { } else {
state.rooms.forEach { room -> LazyVerticalGrid(
val label = room.roomNumber?.toString() ?: "-" columns = GridCells.Fixed(2),
val details = listOfNotNull( horizontalArrangement = Arrangement.spacedBy(12.dp),
room.floor?.let { "Floor $it" }, verticalArrangement = Arrangement.spacedBy(12.dp),
room.roomTypeName ?: room.roomTypeCode modifier = Modifier.fillMaxWidth()
).joinToString("") ) {
val isDimmed = (room.active == false) || (room.maintenance == true) items(state.rooms) { room ->
val alpha = if (isDimmed) 0.5f else 1f val label = room.roomNumber?.toString() ?: "-"
Column( val category = room.roomTypeName ?: room.roomTypeCode ?: "Unassigned"
modifier = Modifier val floorLabel = room.floor?.let { "Floor $it" }.orEmpty()
.fillMaxWidth() val isDimmed = (room.active == false) || (room.maintenance == true)
.alpha(alpha) val alpha = if (isDimmed) 0.5f else 1f
.clickable(enabled = room.id != null) { Card(
onEditRoom(room) 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)
} }
} }
} }

View File

@@ -1,35 +1,7 @@
package com.android.trisolarispms.ui.roomtype 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.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.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 import androidx.lifecycle.viewmodel.compose.viewModel
@Composable @Composable
@@ -41,122 +13,16 @@ fun AddRoomTypeScreen(
viewModel: RoomTypeFormViewModel = viewModel(), viewModel: RoomTypeFormViewModel = viewModel(),
amenityViewModel: AmenityListViewModel = viewModel() amenityViewModel: AmenityListViewModel = viewModel()
) { ) {
val state by viewModel.state.collectAsState() androidx.compose.runtime.LaunchedEffect(Unit) {
val amenityState by amenityViewModel.state.collectAsState() viewModel.resetForm()
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)
}
}
} }
RoomTypeFormScreen(
title = "Add Room Type",
propertyId = propertyId,
onBack = onBack,
onSave = { viewModel.submit(propertyId, onSave) },
codeEditable = true,
viewModel = viewModel,
amenityViewModel = amenityViewModel
)
} }

View File

@@ -1,58 +1,29 @@
package com.android.trisolarispms.ui.roomtype package com.android.trisolarispms.ui.roomtype
import androidx.compose.foundation.layout.Arrangement 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.Row
import androidx.compose.foundation.layout.Spacer 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.fillMaxWidth
import androidx.compose.foundation.layout.height 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.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api 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.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable 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.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment 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.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel 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.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.ReorderableImageGrid
import com.android.trisolarispms.ui.roomimage.RoomImageGridItem
import com.android.trisolarispms.ui.roomimage.RoomImageViewModel import com.android.trisolarispms.ui.roomimage.RoomImageViewModel
@Composable @Composable
@@ -66,16 +37,14 @@ fun EditRoomTypeScreen(
amenityViewModel: AmenityListViewModel = viewModel(), amenityViewModel: AmenityListViewModel = viewModel(),
roomImageViewModel: RoomImageViewModel = viewModel() roomImageViewModel: RoomImageViewModel = viewModel()
) { ) {
val state by viewModel.state.collectAsState()
val amenityState by amenityViewModel.state.collectAsState()
val roomImageState by roomImageViewModel.state.collectAsState() val roomImageState by roomImageViewModel.state.collectAsState()
val showDeleteConfirm = remember { mutableStateOf(false) } val showDeleteConfirm = remember { mutableStateOf(false) }
val orderedImages = remember { mutableStateOf<List<com.android.trisolarispms.data.api.model.ImageDto>>(emptyList()) } val orderedImages = remember { mutableStateOf<List<com.android.trisolarispms.data.api.model.ImageDto>>(emptyList()) }
val originalOrderIds = remember { mutableStateOf<List<String>>(emptyList()) } val originalOrderIds = remember { mutableStateOf<List<String>>(emptyList()) }
val previewUrl = remember { mutableStateOf<String?>(null) } val previewUrl = remember { mutableStateOf<String?>(null) }
val showAmenityDialog = remember { mutableStateOf(false) }
val amenitySearch = remember { mutableStateOf("") } val gridState = androidx.compose.foundation.lazy.grid.rememberLazyGridState()
val gridState = rememberLazyGridState()
val saveRoomTypeImageOrder = remember(roomType.code, orderedImages.value) { val saveRoomTypeImageOrder = remember(roomType.code, orderedImages.value) {
{ {
val code = roomType.code.orEmpty() val code = roomType.code.orEmpty()
@@ -101,9 +70,6 @@ fun EditRoomTypeScreen(
LaunchedEffect(roomType.id) { LaunchedEffect(roomType.id) {
viewModel.setRoomType(roomType) viewModel.setRoomType(roomType)
} }
LaunchedEffect(Unit) {
amenityViewModel.load()
}
LaunchedEffect(roomType.code) { LaunchedEffect(roomType.code) {
val code = roomType.code.orEmpty() val code = roomType.code.orEmpty()
if (code.isNotBlank()) { if (code.isNotBlank()) {
@@ -115,159 +81,55 @@ fun EditRoomTypeScreen(
originalOrderIds.value = roomImageState.images.mapNotNull { it.id } originalOrderIds.value = roomImageState.images.mapNotNull { it.id }
} }
Scaffold( RoomTypeFormScreen(
topBar = { title = "Modify Room Type",
TopAppBar( propertyId = propertyId,
title = { Text("Modify Room Type") }, onBack = onBack,
navigationIcon = { onSave = { viewModel.submitUpdate(propertyId, roomType.id.orEmpty(), onSave) },
IconButton(onClick = onBack) { codeEditable = false,
Icon(Icons.Default.ArrowBack, contentDescription = "Back") onDelete = { if (!roomType.id.isNullOrBlank()) showDeleteConfirm.value = true },
} viewModel = viewModel,
}, amenityViewModel = amenityViewModel
actions = { ) {
IconButton(onClick = { viewModel.submitUpdate(propertyId, roomType.id.orEmpty(), onSave) }) { Row(
Icon(Icons.Default.Done, contentDescription = "Save") modifier = Modifier.fillMaxWidth(),
} horizontalArrangement = Arrangement.SpaceBetween,
if (!roomType.id.isNullOrBlank()) { verticalAlignment = Alignment.CenterVertically
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
) { ) {
OutlinedTextField( Text(text = "Room Type Images", style = androidx.compose.material3.MaterialTheme.typography.titleSmall)
value = state.code, val hasOrderChanged = orderedImages.value.mapNotNull { it.id } != originalOrderIds.value
onValueChange = {}, if (orderedImages.value.isNotEmpty() && hasOrderChanged) {
readOnly = true, Button(onClick = saveRoomTypeImageOrder) {
label = { Text("Code") }, Text("Save order")
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")
} }
} }
val selectedCount = state.amenityIds.size }
Text( if (roomImageState.isLoading) {
text = if (selectedCount == 0) "No amenities selected" else "$selectedCount selected", Spacer(modifier = Modifier.height(8.dp))
style = MaterialTheme.typography.bodySmall androidx.compose.material3.CircularProgressIndicator()
) } else if (orderedImages.value.isEmpty()) {
Spacer(modifier = Modifier.height(16.dp)) Text(text = "No images yet", style = androidx.compose.material3.MaterialTheme.typography.bodySmall)
} else {
Row( Spacer(modifier = Modifier.height(8.dp))
modifier = Modifier.fillMaxWidth(), ReorderableImageGrid(
horizontalArrangement = Arrangement.SpaceBetween, images = orderedImages.value,
verticalAlignment = Alignment.CenterVertically gridState = gridState,
) { verticalArrangement = Arrangement.spacedBy(4.dp),
Text(text = "Room Type Images", style = MaterialTheme.typography.titleSmall) horizontalArrangement = Arrangement.spacedBy(4.dp),
val hasOrderChanged = orderedImages.value.mapNotNull { it.id } != originalOrderIds.value modifier = Modifier
if (orderedImages.value.isNotEmpty() && hasOrderChanged) { .fillMaxWidth()
Button(onClick = saveRoomTypeImageOrder) { .height(320.dp),
Text("Save order") onOrderChange = { list ->
} orderedImages.value = list
} },
} onDragEnd = {}
if (roomImageState.isLoading) { ) { image, dragHandleModifier, _ ->
Spacer(modifier = Modifier.height(8.dp)) RoomImageGridItem(
androidx.compose.material3.CircularProgressIndicator() image = image,
} else if (orderedImages.value.isEmpty()) { modifier = dragHandleModifier,
Text(text = "No images yet", style = MaterialTheme.typography.bodySmall) onPreview = { previewUrl.value = image.url ?: image.thumbnailUrl },
} else { showTags = true
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)
} }
} }
} }
@@ -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 val preview = previewUrl.value
if (!preview.isNullOrBlank()) { if (!preview.isNullOrBlank()) {
com.android.trisolarispms.ui.roomimage.ImagePreviewDialog( ImagePreviewDialog(
imageUrl = preview, imageUrl = preview,
onDismiss = { previewUrl.value = null } onDismiss = { previewUrl.value = null }
) )

View File

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

View File

@@ -14,6 +14,10 @@ class RoomTypeFormViewModel : ViewModel() {
private val _state = MutableStateFlow(RoomTypeFormState()) private val _state = MutableStateFlow(RoomTypeFormState())
val state: StateFlow<RoomTypeFormState> = _state val state: StateFlow<RoomTypeFormState> = _state
fun resetForm() {
_state.update { RoomTypeFormState() }
}
fun setRoomType(type: com.android.trisolarispms.data.api.model.RoomTypeDto) { fun setRoomType(type: com.android.trisolarispms.data.api.model.RoomTypeDto) {
_state.update { _state.update {
it.copy( it.copy(

View File

@@ -6,5 +6,6 @@ data class RoomTypeListState(
val isLoading: Boolean = false, val isLoading: Boolean = false,
val error: String? = null, val error: String? = null,
val items: List<RoomTypeDto> = emptyList(), 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()
) )

View File

@@ -29,6 +29,7 @@ class RoomTypeListViewModel : ViewModel() {
) )
} }
loadRoomTypeImages(propertyId, items) loadRoomTypeImages(propertyId, items)
loadRoomTypeAvailableCounts(propertyId, items)
} else { } else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") } _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) }
}
}
}
} }

View File

@@ -13,6 +13,9 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Add 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.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -105,11 +108,61 @@ fun RoomTypesScreen(
.clip(MaterialTheme.shapes.medium) .clip(MaterialTheme.shapes.medium)
) )
} }
Text( Column(modifier = Modifier.fillMaxWidth()) {
text = "${item.code}${item.name}", Text(
style = MaterialTheme.typography.titleMedium, text = "${item.code}${item.name}",
modifier = Modifier.fillMaxWidth() 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)) Spacer(modifier = Modifier.height(12.dp))
} }