diff --git a/app/src/main/java/com/android/trisolarispms/data/api/model/BookingModels.kt b/app/src/main/java/com/android/trisolarispms/data/api/model/BookingModels.kt index 1a5fc86..24c155b 100644 --- a/app/src/main/java/com/android/trisolarispms/data/api/model/BookingModels.kt +++ b/app/src/main/java/com/android/trisolarispms/data/api/model/BookingModels.kt @@ -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 = emptyList(), val roomNumbers: List = emptyList(), val fromCity: String? = null, val toCity: String? = null, diff --git a/app/src/main/java/com/android/trisolarispms/data/api/model/GuestModels.kt b/app/src/main/java/com/android/trisolarispms/data/api/model/GuestModels.kt index 0573348..2229967 100644 --- a/app/src/main/java/com/android/trisolarispms/data/api/model/GuestModels.kt +++ b/app/src/main/java/com/android/trisolarispms/data/api/model/GuestModels.kt @@ -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 = 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 ) diff --git a/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoScreen.kt index 65e9cf7..38b7f0d 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoScreen.kt @@ -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, diff --git a/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoState.kt b/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoState.kt index a79cbf5..33baa6e 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoState.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoState.kt @@ -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 = emptyList(), val isLoading: Boolean = false, diff --git a/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoViewModel.kt b/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoViewModel.kt index b94f631..5862408 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoViewModel.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoViewModel.kt @@ -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 } ) ) diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt index 7cd64ad..88aa909 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt @@ -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, diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsViewModel.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsViewModel.kt index 92018cb..3e93a2b 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsViewModel.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsViewModel.kt @@ -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 = _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) + } + } }