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 guestName: String? = null,
val guestPhone: String? = null, val guestPhone: String? = null,
val guestNationality: 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 guestAddressText: String? = null,
val guestSignatureUrl: String? = null, val guestSignatureUrl: String? = null,
val vehicleNumbers: List<String> = emptyList(),
val roomNumbers: List<Int> = emptyList(), val roomNumbers: List<Int> = emptyList(),
val fromCity: String? = null, val fromCity: String? = null,
val toCity: String? = null, val toCity: String? = null,

View File

@@ -5,6 +5,7 @@ data class GuestDto(
val name: String? = null, val name: String? = null,
val phoneE164: String? = null, val phoneE164: String? = null,
val nationality: String? = null, val nationality: String? = null,
val age: String? = null,
val addressText: String? = null, val addressText: String? = null,
val vehicleNumbers: List<String> = emptyList(), val vehicleNumbers: List<String> = emptyList(),
val averageScore: Double? = null val averageScore: Double? = null
@@ -15,6 +16,7 @@ data class GuestCreateRequest(
val phoneE164: String? = null, val phoneE164: String? = null,
val name: String? = null, val name: String? = null,
val nationality: String? = null, val nationality: String? = null,
val age: String? = null,
val addressText: String? = null val addressText: String? = null
) )
@@ -22,6 +24,7 @@ data class GuestUpdateRequest(
val phoneE164: String? = null, val phoneE164: String? = null,
val name: String? = null, val name: String? = null,
val nationality: String? = null, val nationality: String? = null,
val age: String? = null,
val addressText: String? = null val addressText: String? = null
) )

View File

@@ -93,6 +93,13 @@ fun GuestInfoScreen(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(12.dp)) 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( OutlinedTextField(
value = state.addressText, value = state.addressText,
onValueChange = viewModel::onAddressChange, onValueChange = viewModel::onAddressChange,

View File

@@ -4,6 +4,7 @@ data class GuestInfoState(
val phoneE164: String = "", val phoneE164: String = "",
val name: String = "", val name: String = "",
val nationality: String = "", val nationality: String = "",
val age: String = "",
val addressText: String = "", val addressText: String = "",
val vehicleNumbers: List<String> = emptyList(), val vehicleNumbers: List<String> = emptyList(),
val isLoading: Boolean = false, val isLoading: Boolean = false,

View File

@@ -30,6 +30,10 @@ class GuestInfoViewModel : ViewModel() {
_state.update { it.copy(nationality = value, error = null) } _state.update { it.copy(nationality = value, error = null) }
} }
fun onAgeChange(value: String) {
_state.update { it.copy(age = value, error = null) }
}
fun onAddressChange(value: String) { fun onAddressChange(value: String) {
_state.update { it.copy(addressText = value, error = null) } _state.update { it.copy(addressText = value, error = null) }
} }
@@ -40,6 +44,7 @@ class GuestInfoViewModel : ViewModel() {
phoneE164 = guest?.phoneE164 ?: phone.orEmpty(), phoneE164 = guest?.phoneE164 ?: phone.orEmpty(),
name = guest?.name.orEmpty(), name = guest?.name.orEmpty(),
nationality = guest?.nationality.orEmpty(), nationality = guest?.nationality.orEmpty(),
age = guest?.age.orEmpty(),
addressText = guest?.addressText.orEmpty(), addressText = guest?.addressText.orEmpty(),
vehicleNumbers = guest?.vehicleNumbers ?: emptyList(), vehicleNumbers = guest?.vehicleNumbers ?: emptyList(),
error = null error = null
@@ -61,6 +66,7 @@ class GuestInfoViewModel : ViewModel() {
phoneE164 = guest.phoneE164 ?: fallbackPhone.orEmpty(), phoneE164 = guest.phoneE164 ?: fallbackPhone.orEmpty(),
name = guest.name.orEmpty(), name = guest.name.orEmpty(),
nationality = guest.nationality.orEmpty(), nationality = guest.nationality.orEmpty(),
age = guest.age.orEmpty(),
addressText = guest.addressText.orEmpty(), addressText = guest.addressText.orEmpty(),
vehicleNumbers = guest.vehicleNumbers ?: emptyList(), vehicleNumbers = guest.vehicleNumbers ?: emptyList(),
isLoading = false, isLoading = false,
@@ -102,6 +108,7 @@ class GuestInfoViewModel : ViewModel() {
phoneE164 = current.phoneE164.trim().ifBlank { null }, phoneE164 = current.phoneE164.trim().ifBlank { null },
name = current.name.trim().ifBlank { null }, name = current.name.trim().ifBlank { null },
nationality = current.nationality.trim().ifBlank { null }, nationality = current.nationality.trim().ifBlank { null },
age = current.age.trim().ifBlank { null },
addressText = current.addressText.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.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -61,6 +62,8 @@ import kotlinx.coroutines.launch
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.LocalDate
import java.time.Period
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@@ -88,6 +91,10 @@ fun BookingDetailsTabsScreen(
LaunchedEffect(propertyId, bookingId, guestId) { LaunchedEffect(propertyId, bookingId, guestId) {
staysViewModel.load(propertyId, bookingId) staysViewModel.load(propertyId, bookingId)
detailsViewModel.load(propertyId, bookingId) detailsViewModel.load(propertyId, bookingId)
detailsViewModel.startStream(propertyId, bookingId)
}
DisposableEffect(propertyId, bookingId) {
onDispose { detailsViewModel.stopStream() }
} }
Scaffold( Scaffold(
@@ -206,8 +213,14 @@ private fun GuestInfoTabContent(
} }
SectionCard(title = "Details") { SectionCard(title = "Details") {
GuestDetailRow(label = "Name", value = details?.guestName) 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 = "Address", value = details?.guestAddressText)
GuestDetailRow(label = "Phone number", value = details?.guestPhone) 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 = "Coming From", value = details?.fromCity)
GuestDetailRow(label = "Going To", value = details?.toCity) GuestDetailRow(label = "Going To", value = details?.toCity)
GuestDetailRow(label = "Relation", value = details?.memberRelation) GuestDetailRow(label = "Relation", value = details?.memberRelation)
@@ -355,6 +368,16 @@ private fun SectionCard(
Spacer(modifier = Modifier.height(12.dp)) 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 @Composable
private fun SignaturePreview( private fun SignaturePreview(
propertyId: String, propertyId: String,

View File

@@ -3,14 +3,30 @@ package com.android.trisolarispms.ui.roomstay
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient 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.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch 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() { class BookingDetailsViewModel : ViewModel() {
private val _state = MutableStateFlow(BookingDetailsState()) private val _state = MutableStateFlow(BookingDetailsState())
val state: StateFlow<BookingDetailsState> = _state 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) { fun load(propertyId: String, bookingId: String) {
if (propertyId.isBlank() || bookingId.isBlank()) return 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)
}
}
} }