guestInfo: improve ui

This commit is contained in:
androidlover5842
2026-02-01 02:07:52 +05:30
parent 53300a6a84
commit 2b0f352ced
7 changed files with 136 additions and 0 deletions

View File

@@ -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,

View File

@@ -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
)

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 }
)
)

View File

@@ -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,

View File

@@ -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)
}
}
}