diff --git a/app/src/main/java/com/android/trisolarispms/MainActivity.kt b/app/src/main/java/com/android/trisolarispms/MainActivity.kt index 5e7a4b1..1d4612b 100644 --- a/app/src/main/java/com/android/trisolarispms/MainActivity.kt +++ b/app/src/main/java/com/android/trisolarispms/MainActivity.kt @@ -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( diff --git a/app/src/main/java/com/android/trisolarispms/data/api/model/BookingModels.kt b/app/src/main/java/com/android/trisolarispms/data/api/model/BookingModels.kt index e5336d0..1a5fc86 100644 --- a/app/src/main/java/com/android/trisolarispms/data/api/model/BookingModels.kt +++ b/app/src/main/java/com/android/trisolarispms/data/api/model/BookingModels.kt @@ -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( diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysScreen.kt index 2a6dcb9..34e18e4 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysScreen.kt @@ -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) { diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt index 40294ae..9a40543 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt @@ -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) }