guest details: improve room stays ui

This commit is contained in:
androidlover5842
2026-01-29 23:19:54 +05:30
parent be820391bc
commit 2d75b88892
4 changed files with 127 additions and 78 deletions

View File

@@ -273,15 +273,6 @@ class MainActivity : ComponentActivity() {
bookingId = booking.id.orEmpty() bookingId = booking.id.orEmpty()
) )
}, },
onExtendBooking = { booking ->
route.value = AppRoute.BookingExpectedDates(
propertyId = currentRoute.propertyId,
bookingId = booking.id.orEmpty(),
status = booking.status,
expectedCheckInAt = booking.expectedCheckInAt,
expectedCheckOutAt = booking.expectedCheckOutAt
)
},
onOpenBookingDetails = { booking -> onOpenBookingDetails = { booking ->
route.value = AppRoute.BookingDetailsTabs( route.value = AppRoute.BookingDetailsTabs(
propertyId = currentRoute.propertyId, propertyId = currentRoute.propertyId,
@@ -413,6 +404,22 @@ class MainActivity : ComponentActivity() {
currentRoute.propertyId, currentRoute.propertyId,
selectedPropertyName.value ?: "Property" selectedPropertyName.value ?: "Property"
) )
},
onEditCheckout = { expectedCheckInAt, expectedCheckOutAt ->
route.value = AppRoute.BookingExpectedDates(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
status = "CHECKED_IN",
expectedCheckInAt = expectedCheckInAt,
expectedCheckOutAt = expectedCheckOutAt
)
},
onEditSignature = { guestId ->
route.value = AppRoute.GuestSignature(
currentRoute.propertyId,
currentRoute.bookingId,
guestId
)
} }
) )
is AppRoute.Rooms -> RoomsScreen( is AppRoute.Rooms -> RoomsScreen(

View File

@@ -56,6 +56,8 @@ data class BookingListItem(
val totalGuestCount: Int? = null, val totalGuestCount: Int? = null,
val expectedGuestCount: Int? = null, val expectedGuestCount: Int? = null,
val notes: String? = null val notes: String? = null
,
val pending: Long? = null
) )
data class BookingBulkCheckInRequest( data class BookingBulkCheckInRequest(

View File

@@ -53,7 +53,6 @@ fun ActiveRoomStaysScreen(
onCreateBooking: () -> Unit, onCreateBooking: () -> Unit,
onManageRoomStay: (BookingListItem) -> Unit, onManageRoomStay: (BookingListItem) -> Unit,
onViewBookingStays: (BookingListItem) -> Unit, onViewBookingStays: (BookingListItem) -> Unit,
onExtendBooking: (BookingListItem) -> Unit,
onOpenBookingDetails: (BookingListItem) -> Unit, onOpenBookingDetails: (BookingListItem) -> Unit,
viewModel: ActiveRoomStaysViewModel = viewModel() viewModel: ActiveRoomStaysViewModel = viewModel()
) { ) {
@@ -151,14 +150,6 @@ fun ActiveRoomStaysScreen(
TextButton(onClick = { selectedBooking.value = null }) { TextButton(onClick = { selectedBooking.value = null }) {
Text("Add photos") Text("Add photos")
} }
TextButton(
onClick = {
selectedBooking.value = null
onExtendBooking(booking)
}
) {
Text("Extend checkout")
}
TextButton(onClick = { selectedBooking.value = null }) { TextButton(onClick = { selectedBooking.value = null }) {
Text("Checkout") Text("Checkout")
} }
@@ -221,6 +212,15 @@ 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 pending = booking.pending
if (pending != null && pending > 0) {
Spacer(modifier = Modifier.height(6.dp))
Text(
text = "$pending",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
}
val checkInAt = booking.checkInAt?.takeIf { it.isNotBlank() } val checkInAt = booking.checkInAt?.takeIf { it.isNotBlank() }
val checkOutAt = booking.expectedCheckOutAt?.takeIf { it.isNotBlank() } val checkOutAt = booking.expectedCheckOutAt?.takeIf { it.isNotBlank() }
if (checkInAt != null && checkOutAt != null) { if (checkInAt != null && checkOutAt != null) {

View File

@@ -3,60 +3,60 @@ package com.android.trisolarispms.ui.roomstay
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
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.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.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.layout.width
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.Edit
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
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
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow import androidx.compose.material3.TabRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
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.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
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.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
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 androidx.compose.foundation.layout.ColumnScope
import kotlinx.coroutines.launch
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
import com.android.trisolarispms.data.api.FirebaseAuthTokenProvider
import com.android.trisolarispms.data.api.ApiConstants
import com.google.firebase.auth.FirebaseAuth
import coil.ImageLoader import coil.ImageLoader
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.decode.SvgDecoder import coil.decode.SvgDecoder
import coil.request.ImageRequest import coil.request.ImageRequest
import com.android.trisolarispms.data.api.ApiConstants
import com.android.trisolarispms.data.api.FirebaseAuthTokenProvider
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
import com.google.firebase.auth.FirebaseAuth
import kotlinx.coroutines.launch
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import androidx.compose.ui.platform.LocalContext
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -65,6 +65,8 @@ fun BookingDetailsTabsScreen(
bookingId: String, bookingId: String,
guestId: String?, guestId: String?,
onBack: () -> Unit, onBack: () -> Unit,
onEditCheckout: (String?, String?) -> Unit,
onEditSignature: (String) -> Unit,
staysViewModel: BookingRoomStaysViewModel = viewModel(), staysViewModel: BookingRoomStaysViewModel = viewModel(),
detailsViewModel: BookingDetailsViewModel = viewModel() detailsViewModel: BookingDetailsViewModel = viewModel()
) { ) {
@@ -117,13 +119,15 @@ fun BookingDetailsTabsScreen(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { page -> ) { page ->
when (page) { when (page) {
0 -> GuestInfoTabContent( 0 -> GuestInfoTabContent(
propertyId = propertyId, propertyId = propertyId,
details = detailsState.details, details = detailsState.details,
guestId = guestId, guestId = guestId,
isLoading = detailsState.isLoading, isLoading = detailsState.isLoading,
error = detailsState.error error = detailsState.error,
) onEditCheckout = onEditCheckout,
onEditSignature = onEditSignature
)
1 -> BookingRoomStaysTabContent(staysState, staysViewModel) 1 -> BookingRoomStaysTabContent(staysState, staysViewModel)
} }
} }
@@ -137,7 +141,9 @@ private fun GuestInfoTabContent(
details: BookingDetailsResponse?, details: BookingDetailsResponse?,
guestId: String?, guestId: String?,
isLoading: Boolean, isLoading: Boolean,
error: String? error: String?,
onEditCheckout: (String?, String?) -> Unit,
onEditSignature: (String) -> Unit
) { ) {
val displayZone = remember { ZoneId.of("Asia/Kolkata") } val displayZone = remember { ZoneId.of("Asia/Kolkata") }
val dateFormatter = remember { DateTimeFormatter.ofPattern("dd/MM/yyyy") } val dateFormatter = remember { DateTimeFormatter.ofPattern("dd/MM/yyyy") }
@@ -158,14 +164,13 @@ private fun GuestInfoTabContent(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
SectionCard(title = "Details") { SectionCard(title = "Details") {
GuestDetailRow(label = "Name", value = details?.guestName.orEmpty()) GuestDetailRow(label = "Name", value = details?.guestName)
GuestDetailRow(label = "Address", value = details?.guestAddressText.orEmpty()) GuestDetailRow(label = "Address", value = details?.guestAddressText)
GuestDetailRow(label = "Phone number", value = details?.guestPhone.orEmpty()) GuestDetailRow(label = "Phone number", value = details?.guestPhone)
GuestDetailRow(label = "Coming From", value = details?.fromCity.orEmpty()) GuestDetailRow(label = "Coming From", value = details?.fromCity)
GuestDetailRow(label = "Going To", value = details?.toCity.orEmpty()) GuestDetailRow(label = "Going To", value = details?.toCity)
GuestDetailRow(label = "Relation", value = details?.memberRelation.orEmpty()) GuestDetailRow(label = "Relation", value = details?.memberRelation)
GuestDetailRow(label = "Mode of transport", value = details?.transportMode.orEmpty()) GuestDetailRow(label = "Mode of transport", value = details?.transportMode)
GuestDetailRow(label = "Vehicle numbers", value = "")
} }
val checkIn = details?.checkInAt ?: details?.expectedCheckInAt val checkIn = details?.checkInAt ?: details?.expectedCheckInAt
@@ -175,55 +180,71 @@ private fun GuestInfoTabContent(
val parsed = runCatching { OffsetDateTime.parse(checkIn).atZoneSameInstant(displayZone) }.getOrNull() val parsed = runCatching { OffsetDateTime.parse(checkIn).atZoneSameInstant(displayZone) }.getOrNull()
GuestDetailRow( GuestDetailRow(
label = "Check In Time", label = "Check In Time",
value = parsed?.let { "${it.format(dateFormatter)} ${it.format(timeFormatter)}" }.orEmpty() value = parsed?.let { "${it.format(dateFormatter)} ${it.format(timeFormatter)}" }
) )
} else {
GuestDetailRow(label = "Check In Time", value = "")
} }
if (!checkOut.isNullOrBlank()) { if (!checkOut.isNullOrBlank()) {
val parsed = runCatching { OffsetDateTime.parse(checkOut).atZoneSameInstant(displayZone) }.getOrNull() val parsed = runCatching { OffsetDateTime.parse(checkOut).atZoneSameInstant(displayZone) }.getOrNull()
GuestDetailRow( Row(
label = "Estimated Check Out Time", modifier = Modifier.fillMaxWidth(),
value = parsed?.let { "${it.format(dateFormatter)} ${it.format(timeFormatter)}" }.orEmpty() horizontalArrangement = Arrangement.SpaceBetween,
) verticalAlignment = Alignment.CenterVertically
} else { ) {
GuestDetailRow(label = "Estimated Check Out Time", value = "") Column(modifier = Modifier.weight(1f)) {
GuestDetailRow(
label = "Estimated Check Out Time",
value = parsed?.let { "${it.format(dateFormatter)} ${it.format(timeFormatter)}" }
)
}
IconButton(
onClick = {
onEditCheckout(details?.expectedCheckInAt, details?.expectedCheckOutAt)
}
) {
Icon(Icons.Default.Edit, contentDescription = "Edit checkout")
}
}
} }
GuestDetailRow( GuestDetailRow(
label = "Rooms Booked", label = "Rooms Booked",
value = details?.roomNumbers?.takeIf { it.isNotEmpty() }?.joinToString(", ").orEmpty() value = details?.roomNumbers?.takeIf { it.isNotEmpty() }?.joinToString(", ")
) )
GuestDetailRow(label = "Total Adults", value = details?.adultCount?.toString().orEmpty()) GuestDetailRow(label = "Total Adults", value = details?.adultCount?.toString())
GuestDetailRow(label = "Total Males", value = details?.maleCount?.toString().orEmpty()) GuestDetailRow(label = "Total Males", value = details?.maleCount?.toString())
GuestDetailRow(label = "Total Females", value = details?.femaleCount?.toString().orEmpty()) GuestDetailRow(label = "Total Females", value = details?.femaleCount?.toString())
GuestDetailRow(label = "Total Children", value = details?.childCount?.toString().orEmpty()) GuestDetailRow(label = "Total Children", value = details?.childCount?.toString())
GuestDetailRow(label = "Total Guests", value = details?.totalGuestCount?.toString().orEmpty()) GuestDetailRow(label = "Total Guests", value = details?.totalGuestCount?.toString())
GuestDetailRow(label = "Expected Guests", value = details?.expectedGuestCount?.toString().orEmpty()) if (details?.totalGuestCount == null) {
GuestDetailRow(label = "Expected Guests", value = details?.expectedGuestCount?.toString())
}
} }
SectionCard(title = "Calculations") { SectionCard(title = "Calculations") {
GuestDetailRow(label = "Amount Collected", value = details?.amountCollected?.toString().orEmpty()) GuestDetailRow(label = "Amount Collected", value = details?.amountCollected?.toString())
GuestDetailRow(label = "Rent per day", value = details?.totalNightlyRate?.toString().orEmpty()) GuestDetailRow(label = "Rent per day", value = details?.totalNightlyRate?.toString())
GuestDetailRow(label = "Expected pay", value = details?.expectedPay?.toString().orEmpty()) GuestDetailRow(label = "Expected pay", value = details?.expectedPay?.toString())
GuestDetailRow(label = "Pending", value = details?.pending?.toString().orEmpty()) GuestDetailRow(label = "Pending", value = details?.pending?.toString())
} }
SectionCard(title = "Registered By") { SectionCard(title = "Registered By") {
GuestDetailRow(label = "Name", value = details?.registeredByName.orEmpty()) GuestDetailRow(label = "Name", value = details?.registeredByName)
GuestDetailRow(label = "Phone number", value = details?.registeredByPhone.orEmpty()) GuestDetailRow(label = "Phone number", value = details?.registeredByPhone)
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
val resolvedGuestId = details?.guestId ?: guestId
SignaturePreview( SignaturePreview(
propertyId = propertyId, propertyId = propertyId,
guestId = details?.guestId ?: guestId, guestId = resolvedGuestId,
signatureUrl = details?.guestSignatureUrl signatureUrl = details?.guestSignatureUrl,
onEditSignature = onEditSignature
) )
} }
} }
@Composable @Composable
private fun GuestDetailRow(label: String, value: String) { private fun GuestDetailRow(label: String, value: String?) {
if (value.isNullOrBlank()) return
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -235,7 +256,7 @@ private fun GuestDetailRow(label: String, value: String) {
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Text( Text(
text = value.ifBlank { "-" }, text = value,
style = MaterialTheme.typography.bodyLarge style = MaterialTheme.typography.bodyLarge
) )
} }
@@ -257,7 +278,12 @@ private fun SectionCard(title: String, content: @Composable ColumnScope.() -> Un
} }
@Composable @Composable
private fun SignaturePreview(propertyId: String, guestId: String?, signatureUrl: String?) { private fun SignaturePreview(
propertyId: String,
guestId: String?,
signatureUrl: String?,
onEditSignature: (String) -> Unit
) {
val resolvedGuestId = guestId val resolvedGuestId = guestId
if (resolvedGuestId.isNullOrBlank() && signatureUrl.isNullOrBlank()) { if (resolvedGuestId.isNullOrBlank() && signatureUrl.isNullOrBlank()) {
Text(text = "Signature", style = MaterialTheme.typography.titleSmall) Text(text = "Signature", style = MaterialTheme.typography.titleSmall)
@@ -314,6 +340,12 @@ private fun SignaturePreview(propertyId: String, guestId: String?, signatureUrl:
.padding(8.dp) .padding(8.dp)
) )
} }
if (signatureUrl.isNullOrBlank() && !resolvedGuestId.isNullOrBlank()) {
Spacer(modifier = Modifier.height(6.dp))
TextButton(onClick = { onEditSignature(resolvedGuestId) }) {
Text("Add signature")
}
}
} }
} }
@@ -322,6 +354,8 @@ private fun BookingRoomStaysTabContent(
state: BookingRoomStaysState, state: BookingRoomStaysState,
viewModel: BookingRoomStaysViewModel viewModel: BookingRoomStaysViewModel
) { ) {
val displayZone = remember { ZoneId.of("Asia/Kolkata") }
val dateFormatter = remember { DateTimeFormatter.ofPattern("dd/MM/yy HH:mm") }
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -354,11 +388,17 @@ private fun BookingRoomStaysTabContent(
state.stays.forEach { stay -> state.stays.forEach { stay ->
val roomLine = "Room ${stay.roomNumber ?: "-"}${stay.roomTypeName ?: ""}".trim() val roomLine = "Room ${stay.roomNumber ?: "-"}${stay.roomTypeName ?: ""}".trim()
Text(text = roomLine, style = MaterialTheme.typography.titleMedium) Text(text = roomLine, style = MaterialTheme.typography.titleMedium)
val guestLine = listOfNotNull(stay.guestName, stay.guestPhone).joinToString("") val fromAt = stay.fromAt?.let {
if (guestLine.isNotBlank()) { runCatching {
Text(text = guestLine, style = MaterialTheme.typography.bodyMedium) OffsetDateTime.parse(it).atZoneSameInstant(displayZone).format(dateFormatter)
}.getOrNull()
} }
val timeLine = listOfNotNull(stay.fromAt, stay.expectedCheckoutAt).joinToString("") val toAt = stay.expectedCheckoutAt?.let {
runCatching {
OffsetDateTime.parse(it).atZoneSameInstant(displayZone).format(dateFormatter)
}.getOrNull()
}
val timeLine = listOfNotNull(fromAt, toAt).joinToString("")
if (timeLine.isNotBlank()) { if (timeLine.isNotBlank()) {
Text(text = timeLine, style = MaterialTheme.typography.bodySmall) Text(text = timeLine, style = MaterialTheme.typography.bodySmall)
} }