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()
)
},
onExtendBooking = { booking ->
route.value = AppRoute.BookingExpectedDates(
propertyId = currentRoute.propertyId,
bookingId = booking.id.orEmpty(),
status = booking.status,
expectedCheckInAt = booking.expectedCheckInAt,
expectedCheckOutAt = booking.expectedCheckOutAt
)
},
onOpenBookingDetails = { booking ->
route.value = AppRoute.BookingDetailsTabs(
propertyId = currentRoute.propertyId,
@@ -413,6 +404,22 @@ class MainActivity : ComponentActivity() {
currentRoute.propertyId,
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(

View File

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

View File

@@ -53,7 +53,6 @@ fun ActiveRoomStaysScreen(
onCreateBooking: () -> Unit,
onManageRoomStay: (BookingListItem) -> Unit,
onViewBookingStays: (BookingListItem) -> Unit,
onExtendBooking: (BookingListItem) -> Unit,
onOpenBookingDetails: (BookingListItem) -> Unit,
viewModel: ActiveRoomStaysViewModel = viewModel()
) {
@@ -151,14 +150,6 @@ fun ActiveRoomStaysScreen(
TextButton(onClick = { selectedBooking.value = null }) {
Text("Add photos")
}
TextButton(
onClick = {
selectedBooking.value = null
onExtendBooking(booking)
}
) {
Text("Extend checkout")
}
TextButton(onClick = { selectedBooking.value = null }) {
Text("Checkout")
}
@@ -221,6 +212,15 @@ private fun CheckedInBookingCard(
Spacer(modifier = Modifier.height(6.dp))
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 checkOutAt = booking.expectedCheckOutAt?.takeIf { it.isNotBlank() }
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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
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.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
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.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
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.compose.AsyncImage
import coil.decode.SvgDecoder
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.OkHttpClient
import java.time.OffsetDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import androidx.compose.ui.platform.LocalContext
@Composable
@OptIn(ExperimentalMaterial3Api::class)
@@ -65,6 +65,8 @@ fun BookingDetailsTabsScreen(
bookingId: String,
guestId: String?,
onBack: () -> Unit,
onEditCheckout: (String?, String?) -> Unit,
onEditSignature: (String) -> Unit,
staysViewModel: BookingRoomStaysViewModel = viewModel(),
detailsViewModel: BookingDetailsViewModel = viewModel()
) {
@@ -117,13 +119,15 @@ fun BookingDetailsTabsScreen(
modifier = Modifier.fillMaxSize()
) { page ->
when (page) {
0 -> GuestInfoTabContent(
propertyId = propertyId,
details = detailsState.details,
guestId = guestId,
isLoading = detailsState.isLoading,
error = detailsState.error
)
0 -> GuestInfoTabContent(
propertyId = propertyId,
details = detailsState.details,
guestId = guestId,
isLoading = detailsState.isLoading,
error = detailsState.error,
onEditCheckout = onEditCheckout,
onEditSignature = onEditSignature
)
1 -> BookingRoomStaysTabContent(staysState, staysViewModel)
}
}
@@ -137,7 +141,9 @@ private fun GuestInfoTabContent(
details: BookingDetailsResponse?,
guestId: String?,
isLoading: Boolean,
error: String?
error: String?,
onEditCheckout: (String?, String?) -> Unit,
onEditSignature: (String) -> Unit
) {
val displayZone = remember { ZoneId.of("Asia/Kolkata") }
val dateFormatter = remember { DateTimeFormatter.ofPattern("dd/MM/yyyy") }
@@ -158,14 +164,13 @@ private fun GuestInfoTabContent(
Spacer(modifier = Modifier.height(8.dp))
}
SectionCard(title = "Details") {
GuestDetailRow(label = "Name", value = details?.guestName.orEmpty())
GuestDetailRow(label = "Address", value = details?.guestAddressText.orEmpty())
GuestDetailRow(label = "Phone number", value = details?.guestPhone.orEmpty())
GuestDetailRow(label = "Coming From", value = details?.fromCity.orEmpty())
GuestDetailRow(label = "Going To", value = details?.toCity.orEmpty())
GuestDetailRow(label = "Relation", value = details?.memberRelation.orEmpty())
GuestDetailRow(label = "Mode of transport", value = details?.transportMode.orEmpty())
GuestDetailRow(label = "Vehicle numbers", value = "")
GuestDetailRow(label = "Name", value = details?.guestName)
GuestDetailRow(label = "Address", value = details?.guestAddressText)
GuestDetailRow(label = "Phone number", value = details?.guestPhone)
GuestDetailRow(label = "Coming From", value = details?.fromCity)
GuestDetailRow(label = "Going To", value = details?.toCity)
GuestDetailRow(label = "Relation", value = details?.memberRelation)
GuestDetailRow(label = "Mode of transport", value = details?.transportMode)
}
val checkIn = details?.checkInAt ?: details?.expectedCheckInAt
@@ -175,55 +180,71 @@ private fun GuestInfoTabContent(
val parsed = runCatching { OffsetDateTime.parse(checkIn).atZoneSameInstant(displayZone) }.getOrNull()
GuestDetailRow(
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()) {
val parsed = runCatching { OffsetDateTime.parse(checkOut).atZoneSameInstant(displayZone) }.getOrNull()
GuestDetailRow(
label = "Estimated Check Out Time",
value = parsed?.let { "${it.format(dateFormatter)} ${it.format(timeFormatter)}" }.orEmpty()
)
} else {
GuestDetailRow(label = "Estimated Check Out Time", value = "")
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
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(
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 Males", value = details?.maleCount?.toString().orEmpty())
GuestDetailRow(label = "Total Females", value = details?.femaleCount?.toString().orEmpty())
GuestDetailRow(label = "Total Children", value = details?.childCount?.toString().orEmpty())
GuestDetailRow(label = "Total Guests", value = details?.totalGuestCount?.toString().orEmpty())
GuestDetailRow(label = "Expected Guests", value = details?.expectedGuestCount?.toString().orEmpty())
GuestDetailRow(label = "Total Adults", value = details?.adultCount?.toString())
GuestDetailRow(label = "Total Males", value = details?.maleCount?.toString())
GuestDetailRow(label = "Total Females", value = details?.femaleCount?.toString())
GuestDetailRow(label = "Total Children", value = details?.childCount?.toString())
GuestDetailRow(label = "Total Guests", value = details?.totalGuestCount?.toString())
if (details?.totalGuestCount == null) {
GuestDetailRow(label = "Expected Guests", value = details?.expectedGuestCount?.toString())
}
}
SectionCard(title = "Calculations") {
GuestDetailRow(label = "Amount Collected", value = details?.amountCollected?.toString().orEmpty())
GuestDetailRow(label = "Rent per day", value = details?.totalNightlyRate?.toString().orEmpty())
GuestDetailRow(label = "Expected pay", value = details?.expectedPay?.toString().orEmpty())
GuestDetailRow(label = "Pending", value = details?.pending?.toString().orEmpty())
GuestDetailRow(label = "Amount Collected", value = details?.amountCollected?.toString())
GuestDetailRow(label = "Rent per day", value = details?.totalNightlyRate?.toString())
GuestDetailRow(label = "Expected pay", value = details?.expectedPay?.toString())
GuestDetailRow(label = "Pending", value = details?.pending?.toString())
}
SectionCard(title = "Registered By") {
GuestDetailRow(label = "Name", value = details?.registeredByName.orEmpty())
GuestDetailRow(label = "Phone number", value = details?.registeredByPhone.orEmpty())
GuestDetailRow(label = "Name", value = details?.registeredByName)
GuestDetailRow(label = "Phone number", value = details?.registeredByPhone)
}
Spacer(modifier = Modifier.height(16.dp))
val resolvedGuestId = details?.guestId ?: guestId
SignaturePreview(
propertyId = propertyId,
guestId = details?.guestId ?: guestId,
signatureUrl = details?.guestSignatureUrl
guestId = resolvedGuestId,
signatureUrl = details?.guestSignatureUrl,
onEditSignature = onEditSignature
)
}
}
@Composable
private fun GuestDetailRow(label: String, value: String) {
private fun GuestDetailRow(label: String, value: String?) {
if (value.isNullOrBlank()) return
Column(
modifier = Modifier
.fillMaxWidth()
@@ -235,7 +256,7 @@ private fun GuestDetailRow(label: String, value: String) {
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = value.ifBlank { "-" },
text = value,
style = MaterialTheme.typography.bodyLarge
)
}
@@ -257,7 +278,12 @@ private fun SectionCard(title: String, content: @Composable ColumnScope.() -> Un
}
@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
if (resolvedGuestId.isNullOrBlank() && signatureUrl.isNullOrBlank()) {
Text(text = "Signature", style = MaterialTheme.typography.titleSmall)
@@ -314,6 +340,12 @@ private fun SignaturePreview(propertyId: String, guestId: String?, signatureUrl:
.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,
viewModel: BookingRoomStaysViewModel
) {
val displayZone = remember { ZoneId.of("Asia/Kolkata") }
val dateFormatter = remember { DateTimeFormatter.ofPattern("dd/MM/yy HH:mm") }
Column(
modifier = Modifier
.fillMaxSize()
@@ -354,11 +388,17 @@ private fun BookingRoomStaysTabContent(
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 fromAt = stay.fromAt?.let {
runCatching {
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()) {
Text(text = timeLine, style = MaterialTheme.typography.bodySmall)
}