ability to see stays from booking id on click

This commit is contained in:
androidlover5842
2026-01-29 14:07:01 +05:30
parent 5f522ca3ab
commit dffa8b14cd
10 changed files with 295 additions and 22 deletions

View File

@@ -21,6 +21,7 @@ import com.android.trisolarispms.ui.guest.GuestSignatureScreen
import com.android.trisolarispms.ui.roomstay.ManageRoomStayRatesScreen
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelectScreen
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelection
import com.android.trisolarispms.ui.roomstay.BookingRoomStaysScreen
import com.android.trisolarispms.ui.home.HomeScreen
import com.android.trisolarispms.ui.property.AddPropertyScreen
import com.android.trisolarispms.ui.room.RoomFormScreen
@@ -142,6 +143,10 @@ class MainActivity : ComponentActivity() {
currentRoute.fromAt,
currentRoute.toAt
)
is AppRoute.BookingRoomStays -> route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
}
}
@@ -251,6 +256,12 @@ class MainActivity : ComponentActivity() {
toAt = toAt
)
}
},
onViewBookingStays = { booking ->
route.value = AppRoute.BookingRoomStays(
propertyId = currentRoute.propertyId,
bookingId = booking.id.orEmpty()
)
}
)
is AppRoute.ManageRoomStaySelect -> ManageRoomStaySelectScreen(
@@ -338,6 +349,16 @@ class MainActivity : ComponentActivity() {
)
}
)
is AppRoute.BookingRoomStays -> BookingRoomStaysScreen(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
onBack = {
route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
}
)
is AppRoute.Rooms -> RoomsScreen(
propertyId = currentRoute.propertyId,
onBack = {

View File

@@ -13,6 +13,9 @@ data class BookingCreateRequest(
val expectedCheckOutAt: String,
val source: String? = null,
val guestPhoneE164: String? = null,
val fromCity: String? = null,
val toCity: String? = null,
val memberRelation: String? = null,
val transportMode: String? = null,
val childCount: Int? = null,
val maleCount: Int? = null,

View File

@@ -31,6 +31,10 @@ sealed interface AppRoute {
val fromAt: String,
val toAt: String?
) : AppRoute
data class BookingRoomStays(
val propertyId: String,
val bookingId: String
) : AppRoute
data object AddProperty : AppRoute
data class ActiveRoomStays(val propertyId: String, val propertyName: String) : AppRoute
data class Rooms(val propertyId: String) : AppRoute

View File

@@ -11,7 +11,9 @@ 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.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.CalendarMonth
@@ -75,6 +77,8 @@ fun BookingCreateScreen(
val checkInNow = remember { mutableStateOf(true) }
val sourceMenuExpanded = remember { mutableStateOf(false) }
val sourceOptions = listOf("WALKIN", "OTA", "AGENT")
val relationMenuExpanded = remember { mutableStateOf(false) }
val relationOptions = listOf("FRIENDS", "FAMILY", "GROUP", "ALONE")
val transportMenuExpanded = remember { mutableStateOf(false) }
val transportOptions = listOf("CAR", "BIKE", "TRAIN", "PLANE", "BUS", "FOOT", "CYCLE", "OTHER")
val displayFormatter = remember { DateTimeFormatter.ofPattern("dd-MM-yy HH:mm") }
@@ -117,7 +121,8 @@ fun BookingCreateScreen(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(24.dp),
.padding(24.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.Top
) {
Row(
@@ -316,6 +321,52 @@ fun BookingCreateScreen(
}
}
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
value = state.fromCity,
onValueChange = viewModel::onFromCityChange,
label = { Text("From City (optional)") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
value = state.toCity,
onValueChange = viewModel::onToCityChange,
label = { Text("To City (optional)") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
ExposedDropdownMenuBox(
expanded = relationMenuExpanded.value,
onExpandedChange = { relationMenuExpanded.value = !relationMenuExpanded.value }
) {
OutlinedTextField(
value = state.memberRelation,
onValueChange = {},
readOnly = true,
label = { Text("Member Relation (optional)") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = relationMenuExpanded.value)
},
modifier = Modifier
.fillMaxWidth()
.menuAnchor()
)
ExposedDropdownMenu(
expanded = relationMenuExpanded.value,
onDismissRequest = { relationMenuExpanded.value = false }
) {
relationOptions.forEach { option ->
DropdownMenuItem(
text = { Text(option) },
onClick = {
relationMenuExpanded.value = false
viewModel.onMemberRelationChange(option)
}
)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
ExposedDropdownMenuBox(
expanded = transportMenuExpanded.value,
onExpandedChange = { transportMenuExpanded.value = !transportMenuExpanded.value }

View File

@@ -9,6 +9,9 @@ data class BookingCreateState(
val expectedCheckInAt: String = "",
val expectedCheckOutAt: String = "",
val source: String = "WALKIN",
val fromCity: String = "",
val toCity: String = "",
val memberRelation: String = "",
val transportMode: String = "CAR",
val childCount: String = "",
val maleCount: String = "",

View File

@@ -88,6 +88,18 @@ class BookingCreateViewModel : ViewModel() {
_state.update { it.copy(source = value, error = null) }
}
fun onFromCityChange(value: String) {
_state.update { it.copy(fromCity = value, error = null) }
}
fun onToCityChange(value: String) {
_state.update { it.copy(toCity = value, error = null) }
}
fun onMemberRelationChange(value: String) {
_state.update { it.copy(memberRelation = value, error = null) }
}
fun onTransportModeChange(value: String) {
_state.update { it.copy(transportMode = value, error = null) }
}
@@ -142,6 +154,9 @@ class BookingCreateViewModel : ViewModel() {
expectedCheckOutAt = checkOut,
source = current.source.trim().ifBlank { null },
guestPhoneE164 = phone,
fromCity = current.fromCity.trim().ifBlank { null },
toCity = current.toCity.trim().ifBlank { null },
memberRelation = current.memberRelation.trim().ifBlank { null },
transportMode = current.transportMode.trim().ifBlank { null },
childCount = childCount,
maleCount = maleCount,

View File

@@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowBack
@@ -29,6 +29,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -39,6 +40,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.trisolarispms.data.api.model.BookingListItem
import java.time.Duration
import java.time.OffsetDateTime
@Composable
@OptIn(ExperimentalMaterial3Api::class)
@@ -49,6 +52,7 @@ fun ActiveRoomStaysScreen(
onViewRooms: () -> Unit,
onCreateBooking: () -> Unit,
onManageRoomStay: (BookingListItem) -> Unit,
onViewBookingStays: (BookingListItem) -> Unit,
viewModel: ActiveRoomStaysViewModel = viewModel()
) {
val state by viewModel.state.collectAsState()
@@ -113,29 +117,14 @@ fun ActiveRoomStaysScreen(
items(state.checkedInBookings) { booking ->
CheckedInBookingCard(
booking = booking,
onClick = { selectedBooking.value = booking }
onClick = { selectedBooking.value = booking },
onLongClick = { onViewBookingStays(booking) }
)
}
}
Spacer(modifier = Modifier.height(16.dp))
}
if (state.items.isEmpty()) {
Text(text = "No active room stays")
} else {
state.items.forEach { item ->
val roomLine = "Room ${item.roomNumber ?: "-"}${item.roomTypeName ?: ""}".trim()
Text(text = roomLine, style = MaterialTheme.typography.titleMedium)
val guestLine = listOfNotNull(item.guestName, item.guestPhone).joinToString("")
if (guestLine.isNotBlank()) {
Text(text = guestLine, style = MaterialTheme.typography.bodyMedium)
}
val timeLine = listOfNotNull(item.fromAt, item.expectedCheckoutAt).joinToString("")
if (timeLine.isNotBlank()) {
Text(text = timeLine, style = MaterialTheme.typography.bodySmall)
}
Spacer(modifier = Modifier.height(12.dp))
}
Text(text = "No checked-in bookings")
}
}
}
@@ -174,11 +163,15 @@ fun ActiveRoomStaysScreen(
@Composable
private fun CheckedInBookingCard(
booking: BookingListItem,
onClick: () -> Unit
onClick: () -> Unit,
onLongClick: () -> Unit
) {
Card(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
modifier = Modifier.clickable(onClick = onClick)
modifier = Modifier.combinedClickable(
onClick = onClick,
onLongClick = onLongClick
)
) {
Column(modifier = Modifier.padding(12.dp)) {
Text(
@@ -204,6 +197,30 @@ private fun CheckedInBookingCard(
Spacer(modifier = Modifier.height(6.dp))
Text(text = notes, style = MaterialTheme.typography.bodySmall)
}
val checkInAt = booking.checkInAt?.takeIf { it.isNotBlank() }
val checkOutAt = booking.expectedCheckOutAt?.takeIf { it.isNotBlank() }
if (checkInAt != null && checkOutAt != null) {
val now = OffsetDateTime.now()
val start = runCatching { OffsetDateTime.parse(checkInAt) }.getOrNull()
val end = runCatching { OffsetDateTime.parse(checkOutAt) }.getOrNull()
if (start != null && end != null) {
val total = Duration.between(start, end).toMinutes().coerceAtLeast(0)
val remaining = Duration.between(now, end).toMinutes().coerceAtLeast(0)
val hoursLeft = (remaining / 60).coerceAtLeast(0)
val progress = if (total > 0) {
((total - remaining).toFloat() / total.toFloat()).coerceIn(0f, 1f)
} else {
0f
}
Spacer(modifier = Modifier.height(8.dp))
Text(text = "$hoursLeft hours", style = MaterialTheme.typography.bodySmall)
Spacer(modifier = Modifier.height(6.dp))
LinearProgressIndicator(
progress = { progress },
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
}

View File

@@ -0,0 +1,106 @@
package com.android.trisolarispms.ui.roomstay
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.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun BookingRoomStaysScreen(
propertyId: String,
bookingId: String,
onBack: () -> Unit,
viewModel: BookingRoomStaysViewModel = viewModel()
) {
val state by viewModel.state.collectAsState()
LaunchedEffect(propertyId, bookingId) {
viewModel.load(propertyId, bookingId)
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Room Stays") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
}
},
colors = TopAppBarDefaults.topAppBarColors()
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "Show all (including checkout)")
Switch(
checked = state.showAll,
onCheckedChange = viewModel::toggleShowAll
)
}
Spacer(modifier = Modifier.height(12.dp))
if (state.isLoading) {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(8.dp))
}
state.error?.let {
Text(text = it, color = MaterialTheme.colorScheme.error)
Spacer(modifier = Modifier.height(8.dp))
}
if (!state.isLoading && state.error == null) {
if (state.stays.isEmpty()) {
Text(text = "No stays found")
} else {
state.stays.forEach { stay ->
val roomLine = "Room ${stay.roomNumber ?: "-"}${stay.roomTypeName ?: ""}".trim()
Text(text = roomLine, style = MaterialTheme.typography.titleMedium)
val guestLine = listOfNotNull(stay.guestName, stay.guestPhone).joinToString("")
if (guestLine.isNotBlank()) {
Text(text = guestLine, style = MaterialTheme.typography.bodyMedium)
}
val timeLine = listOfNotNull(stay.fromAt, stay.expectedCheckoutAt).joinToString("")
if (timeLine.isNotBlank()) {
Text(text = timeLine, style = MaterialTheme.typography.bodySmall)
}
Spacer(modifier = Modifier.height(12.dp))
}
}
}
}
}
}

View File

@@ -0,0 +1,10 @@
package com.android.trisolarispms.ui.roomstay
import com.android.trisolarispms.data.api.model.ActiveRoomStayDto
data class BookingRoomStaysState(
val isLoading: Boolean = false,
val error: String? = null,
val stays: List<ActiveRoomStayDto> = emptyList(),
val showAll: Boolean = false
)

View File

@@ -0,0 +1,43 @@
package com.android.trisolarispms.ui.roomstay
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class BookingRoomStaysViewModel : ViewModel() {
private val _state = MutableStateFlow(BookingRoomStaysState())
val state: StateFlow<BookingRoomStaysState> = _state
fun toggleShowAll(value: Boolean) {
_state.update { it.copy(showAll = value) }
}
fun load(propertyId: String, bookingId: String) {
if (propertyId.isBlank() || bookingId.isBlank()) return
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
val response = api.listActiveRoomStays(propertyId)
if (response.isSuccessful) {
val filtered = response.body().orEmpty().filter { it.bookingId == bookingId }
_state.update {
it.copy(
isLoading = false,
stays = filtered,
error = null
)
}
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") }
}
}
}
}