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

View File

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

View File

@@ -31,6 +31,10 @@ sealed interface AppRoute {
val fromAt: String, val fromAt: String,
val toAt: String? val toAt: String?
) : AppRoute ) : AppRoute
data class BookingRoomStays(
val propertyId: String,
val bookingId: String
) : AppRoute
data object AddProperty : AppRoute data object AddProperty : AppRoute
data class ActiveRoomStays(val propertyId: String, val propertyName: String) : AppRoute data class ActiveRoomStays(val propertyId: String, val propertyName: String) : AppRoute
data class Rooms(val propertyId: 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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
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.CalendarMonth import androidx.compose.material.icons.filled.CalendarMonth
@@ -75,6 +77,8 @@ fun BookingCreateScreen(
val checkInNow = remember { mutableStateOf(true) } val checkInNow = remember { mutableStateOf(true) }
val sourceMenuExpanded = remember { mutableStateOf(false) } val sourceMenuExpanded = remember { mutableStateOf(false) }
val sourceOptions = listOf("WALKIN", "OTA", "AGENT") val sourceOptions = listOf("WALKIN", "OTA", "AGENT")
val relationMenuExpanded = remember { mutableStateOf(false) }
val relationOptions = listOf("FRIENDS", "FAMILY", "GROUP", "ALONE")
val transportMenuExpanded = remember { mutableStateOf(false) } val transportMenuExpanded = remember { mutableStateOf(false) }
val transportOptions = listOf("CAR", "BIKE", "TRAIN", "PLANE", "BUS", "FOOT", "CYCLE", "OTHER") val transportOptions = listOf("CAR", "BIKE", "TRAIN", "PLANE", "BUS", "FOOT", "CYCLE", "OTHER")
val displayFormatter = remember { DateTimeFormatter.ofPattern("dd-MM-yy HH:mm") } val displayFormatter = remember { DateTimeFormatter.ofPattern("dd-MM-yy HH:mm") }
@@ -117,7 +121,8 @@ fun BookingCreateScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(padding) .padding(padding)
.padding(24.dp), .padding(24.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.Top verticalArrangement = Arrangement.Top
) { ) {
Row( Row(
@@ -316,6 +321,52 @@ fun BookingCreateScreen(
} }
} }
Spacer(modifier = Modifier.height(12.dp)) 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( ExposedDropdownMenuBox(
expanded = transportMenuExpanded.value, expanded = transportMenuExpanded.value,
onExpandedChange = { transportMenuExpanded.value = !transportMenuExpanded.value } onExpandedChange = { transportMenuExpanded.value = !transportMenuExpanded.value }

View File

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

View File

@@ -88,6 +88,18 @@ class BookingCreateViewModel : ViewModel() {
_state.update { it.copy(source = value, error = null) } _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) { fun onTransportModeChange(value: String) {
_state.update { it.copy(transportMode = value, error = null) } _state.update { it.copy(transportMode = value, error = null) }
} }
@@ -142,6 +154,9 @@ class BookingCreateViewModel : ViewModel() {
expectedCheckOutAt = checkOut, expectedCheckOutAt = checkOut,
source = current.source.trim().ifBlank { null }, source = current.source.trim().ifBlank { null },
guestPhoneE164 = phone, 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 }, transportMode = current.transportMode.trim().ifBlank { null },
childCount = childCount, childCount = childCount,
maleCount = maleCount, 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.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items 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.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowBack 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.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.LinearProgressIndicator
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
@@ -39,6 +40,8 @@ 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 com.android.trisolarispms.data.api.model.BookingListItem import com.android.trisolarispms.data.api.model.BookingListItem
import java.time.Duration
import java.time.OffsetDateTime
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -49,6 +52,7 @@ fun ActiveRoomStaysScreen(
onViewRooms: () -> Unit, onViewRooms: () -> Unit,
onCreateBooking: () -> Unit, onCreateBooking: () -> Unit,
onManageRoomStay: (BookingListItem) -> Unit, onManageRoomStay: (BookingListItem) -> Unit,
onViewBookingStays: (BookingListItem) -> Unit,
viewModel: ActiveRoomStaysViewModel = viewModel() viewModel: ActiveRoomStaysViewModel = viewModel()
) { ) {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
@@ -113,29 +117,14 @@ fun ActiveRoomStaysScreen(
items(state.checkedInBookings) { booking -> items(state.checkedInBookings) { booking ->
CheckedInBookingCard( CheckedInBookingCard(
booking = booking, booking = booking,
onClick = { selectedBooking.value = booking } onClick = { selectedBooking.value = booking },
onLongClick = { onViewBookingStays(booking) }
) )
} }
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
}
if (state.items.isEmpty()) {
Text(text = "No active room stays")
} else { } else {
state.items.forEach { item -> Text(text = "No checked-in bookings")
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))
}
} }
} }
} }
@@ -174,11 +163,15 @@ fun ActiveRoomStaysScreen(
@Composable @Composable
private fun CheckedInBookingCard( private fun CheckedInBookingCard(
booking: BookingListItem, booking: BookingListItem,
onClick: () -> Unit onClick: () -> Unit,
onLongClick: () -> Unit
) { ) {
Card( Card(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), 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)) { Column(modifier = Modifier.padding(12.dp)) {
Text( Text(
@@ -204,6 +197,30 @@ private fun CheckedInBookingCard(
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(6.dp))
Text(text = notes, style = MaterialTheme.typography.bodySmall) 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") }
}
}
}
}