guestInfo: improve ui
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user