guestInfo: improve ui
This commit is contained in:
@@ -86,8 +86,14 @@ data class BookingDetailsResponse(
|
||||
val guestName: String? = null,
|
||||
val guestPhone: String? = null,
|
||||
val guestNationality: String? = null,
|
||||
@com.google.gson.annotations.SerializedName(
|
||||
value = "guestAge",
|
||||
alternate = ["guestDob", "guestDOB", "guest_age"]
|
||||
)
|
||||
val guestAge: String? = null,
|
||||
val guestAddressText: String? = null,
|
||||
val guestSignatureUrl: String? = null,
|
||||
val vehicleNumbers: List<String> = emptyList(),
|
||||
val roomNumbers: List<Int> = emptyList(),
|
||||
val fromCity: String? = null,
|
||||
val toCity: String? = null,
|
||||
|
||||
@@ -5,6 +5,7 @@ data class GuestDto(
|
||||
val name: String? = null,
|
||||
val phoneE164: String? = null,
|
||||
val nationality: String? = null,
|
||||
val age: String? = null,
|
||||
val addressText: String? = null,
|
||||
val vehicleNumbers: List<String> = emptyList(),
|
||||
val averageScore: Double? = null
|
||||
@@ -15,6 +16,7 @@ data class GuestCreateRequest(
|
||||
val phoneE164: String? = null,
|
||||
val name: String? = null,
|
||||
val nationality: String? = null,
|
||||
val age: String? = null,
|
||||
val addressText: String? = null
|
||||
)
|
||||
|
||||
@@ -22,6 +24,7 @@ data class GuestUpdateRequest(
|
||||
val phoneE164: String? = null,
|
||||
val name: String? = null,
|
||||
val nationality: String? = null,
|
||||
val age: String? = null,
|
||||
val addressText: String? = null
|
||||
)
|
||||
|
||||
|
||||
@@ -93,6 +93,13 @@ fun GuestInfoScreen(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = state.age,
|
||||
onValueChange = viewModel::onAgeChange,
|
||||
label = { Text("DOB (dd/MM/yyyy)") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
OutlinedTextField(
|
||||
value = state.addressText,
|
||||
onValueChange = viewModel::onAddressChange,
|
||||
|
||||
@@ -4,6 +4,7 @@ data class GuestInfoState(
|
||||
val phoneE164: String = "",
|
||||
val name: String = "",
|
||||
val nationality: String = "",
|
||||
val age: String = "",
|
||||
val addressText: String = "",
|
||||
val vehicleNumbers: List<String> = emptyList(),
|
||||
val isLoading: Boolean = false,
|
||||
|
||||
@@ -30,6 +30,10 @@ class GuestInfoViewModel : ViewModel() {
|
||||
_state.update { it.copy(nationality = value, error = null) }
|
||||
}
|
||||
|
||||
fun onAgeChange(value: String) {
|
||||
_state.update { it.copy(age = value, error = null) }
|
||||
}
|
||||
|
||||
fun onAddressChange(value: String) {
|
||||
_state.update { it.copy(addressText = value, error = null) }
|
||||
}
|
||||
@@ -40,6 +44,7 @@ class GuestInfoViewModel : ViewModel() {
|
||||
phoneE164 = guest?.phoneE164 ?: phone.orEmpty(),
|
||||
name = guest?.name.orEmpty(),
|
||||
nationality = guest?.nationality.orEmpty(),
|
||||
age = guest?.age.orEmpty(),
|
||||
addressText = guest?.addressText.orEmpty(),
|
||||
vehicleNumbers = guest?.vehicleNumbers ?: emptyList(),
|
||||
error = null
|
||||
@@ -61,6 +66,7 @@ class GuestInfoViewModel : ViewModel() {
|
||||
phoneE164 = guest.phoneE164 ?: fallbackPhone.orEmpty(),
|
||||
name = guest.name.orEmpty(),
|
||||
nationality = guest.nationality.orEmpty(),
|
||||
age = guest.age.orEmpty(),
|
||||
addressText = guest.addressText.orEmpty(),
|
||||
vehicleNumbers = guest.vehicleNumbers ?: emptyList(),
|
||||
isLoading = false,
|
||||
@@ -102,6 +108,7 @@ class GuestInfoViewModel : ViewModel() {
|
||||
phoneE164 = current.phoneE164.trim().ifBlank { null },
|
||||
name = current.name.trim().ifBlank { null },
|
||||
nationality = current.nationality.trim().ifBlank { null },
|
||||
age = current.age.trim().ifBlank { null },
|
||||
addressText = current.addressText.trim().ifBlank { null }
|
||||
)
|
||||
)
|
||||
|
||||
@@ -38,6 +38,7 @@ import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -61,6 +62,8 @@ import kotlinx.coroutines.launch
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.LocalDate
|
||||
import java.time.Period
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@@ -88,6 +91,10 @@ fun BookingDetailsTabsScreen(
|
||||
LaunchedEffect(propertyId, bookingId, guestId) {
|
||||
staysViewModel.load(propertyId, bookingId)
|
||||
detailsViewModel.load(propertyId, bookingId)
|
||||
detailsViewModel.startStream(propertyId, bookingId)
|
||||
}
|
||||
DisposableEffect(propertyId, bookingId) {
|
||||
onDispose { detailsViewModel.stopStream() }
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@@ -206,8 +213,14 @@ private fun GuestInfoTabContent(
|
||||
}
|
||||
SectionCard(title = "Details") {
|
||||
GuestDetailRow(label = "Name", value = details?.guestName)
|
||||
GuestDetailRow(label = "Nationality", value = details?.guestNationality)
|
||||
GuestDetailRow(label = "Age", value = details?.guestAge?.let { formatAge(it) })
|
||||
GuestDetailRow(label = "Address", value = details?.guestAddressText)
|
||||
GuestDetailRow(label = "Phone number", value = details?.guestPhone)
|
||||
GuestDetailRow(
|
||||
label = "Vehicle Numbers",
|
||||
value = details?.vehicleNumbers?.takeIf { it.isNotEmpty() }?.joinToString(", ")
|
||||
)
|
||||
GuestDetailRow(label = "Coming From", value = details?.fromCity)
|
||||
GuestDetailRow(label = "Going To", value = details?.toCity)
|
||||
GuestDetailRow(label = "Relation", value = details?.memberRelation)
|
||||
@@ -355,6 +368,16 @@ private fun SectionCard(
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
|
||||
private fun formatAge(dob: String): String? {
|
||||
val parsed = runCatching {
|
||||
LocalDate.parse(dob, DateTimeFormatter.ofPattern("dd/MM/yyyy"))
|
||||
}.getOrNull() ?: return null
|
||||
val today = LocalDate.now(ZoneId.of("Asia/Kolkata"))
|
||||
if (parsed.isAfter(today)) return null
|
||||
val years = Period.between(parsed, today).years
|
||||
return if (years >= 0) "$years" else null
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SignaturePreview(
|
||||
propertyId: String,
|
||||
|
||||
@@ -3,14 +3,30 @@ package com.android.trisolarispms.ui.roomstay
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.android.trisolarispms.data.api.ApiClient
|
||||
import com.android.trisolarispms.data.api.ApiConstants
|
||||
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.Job
|
||||
import okhttp3.Request
|
||||
import okhttp3.sse.EventSource
|
||||
import okhttp3.sse.EventSourceListener
|
||||
import okhttp3.sse.EventSources
|
||||
|
||||
class BookingDetailsViewModel : ViewModel() {
|
||||
private val _state = MutableStateFlow(BookingDetailsState())
|
||||
val state: StateFlow<BookingDetailsState> = _state
|
||||
private val gson = Gson()
|
||||
private var eventSource: EventSource? = null
|
||||
private var streamKey: String? = null
|
||||
private var lastPropertyId: String? = null
|
||||
private var lastBookingId: String? = null
|
||||
private var retryJob: Job? = null
|
||||
private var retryDelayMs: Long = 2000
|
||||
|
||||
fun load(propertyId: String, bookingId: String) {
|
||||
if (propertyId.isBlank() || bookingId.isBlank()) return
|
||||
@@ -35,4 +51,77 @@ class BookingDetailsViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startStream(propertyId: String, bookingId: String) {
|
||||
if (propertyId.isBlank() || bookingId.isBlank()) return
|
||||
val key = "$propertyId:$bookingId"
|
||||
if (streamKey == key && eventSource != null) return
|
||||
stopStream()
|
||||
streamKey = key
|
||||
lastPropertyId = propertyId
|
||||
lastBookingId = bookingId
|
||||
val client = ApiClient.createOkHttpClient(readTimeoutSeconds = 0)
|
||||
val url = "${ApiConstants.BASE_URL}properties/$propertyId/bookings/$bookingId/stream"
|
||||
val request = Request.Builder().url(url).get().build()
|
||||
eventSource = EventSources.createFactory(client).newEventSource(
|
||||
request,
|
||||
object : EventSourceListener() {
|
||||
override fun onEvent(
|
||||
eventSource: EventSource,
|
||||
id: String?,
|
||||
type: String?,
|
||||
data: String
|
||||
) {
|
||||
if (data.isBlank() || type == "ping") return
|
||||
val details = try {
|
||||
gson.fromJson(data, BookingDetailsResponse::class.java)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
if (details != null) {
|
||||
_state.update { it.copy(isLoading = false, details = details, error = null) }
|
||||
retryDelayMs = 2000
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(
|
||||
eventSource: EventSource,
|
||||
t: Throwable?,
|
||||
response: okhttp3.Response?
|
||||
) {
|
||||
stopStream()
|
||||
scheduleReconnect()
|
||||
}
|
||||
|
||||
override fun onClosed(eventSource: EventSource) {
|
||||
stopStream()
|
||||
scheduleReconnect()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun stopStream() {
|
||||
eventSource?.cancel()
|
||||
eventSource = null
|
||||
streamKey = null
|
||||
retryJob?.cancel()
|
||||
retryJob = null
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
stopStream()
|
||||
}
|
||||
|
||||
private fun scheduleReconnect() {
|
||||
val propertyId = lastPropertyId ?: return
|
||||
val bookingId = lastBookingId ?: return
|
||||
if (retryJob?.isActive == true) return
|
||||
retryJob = viewModelScope.launch {
|
||||
delay(retryDelayMs)
|
||||
retryDelayMs = (retryDelayMs * 2).coerceAtMost(30000)
|
||||
startStream(propertyId, bookingId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user