|
|
|
|
@@ -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()
|
|
|
|
|
) {
|
|
|
|
|
@@ -122,7 +124,9 @@ fun BookingDetailsTabsScreen(
|
|
|
|
|
details = detailsState.details,
|
|
|
|
|
guestId = guestId,
|
|
|
|
|
isLoading = detailsState.isLoading,
|
|
|
|
|
error = detailsState.error
|
|
|
|
|
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()
|
|
|
|
|
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)}" }.orEmpty()
|
|
|
|
|
value = parsed?.let { "${it.format(dateFormatter)} ${it.format(timeFormatter)}" }
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
GuestDetailRow(label = "Estimated Check Out Time", value = "")
|
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|