create booking : improve form and ui further

This commit is contained in:
androidlover5842
2026-01-29 09:39:17 +05:30
parent 869e59aaac
commit 799c0b44b9
7 changed files with 172 additions and 27 deletions

View File

@@ -140,7 +140,12 @@ class MainActivity : ComponentActivity() {
) )
is AppRoute.CreateBooking -> BookingCreateScreen( is AppRoute.CreateBooking -> BookingCreateScreen(
propertyId = currentRoute.propertyId, propertyId = currentRoute.propertyId,
onBack = { route.value = AppRoute.Home }, onBack = {
route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
},
onCreated = { response, guest, phone -> onCreated = { response, guest, phone ->
val bookingId = response.id.orEmpty() val bookingId = response.id.orEmpty()
val guestId = (guest?.id ?: response.guestId).orEmpty() val guestId = (guest?.id ?: response.guestId).orEmpty()

View File

@@ -5,6 +5,7 @@ import com.android.trisolarispms.data.api.model.GuestCreateRequest
import com.android.trisolarispms.data.api.model.GuestRatingDto import com.android.trisolarispms.data.api.model.GuestRatingDto
import com.android.trisolarispms.data.api.model.GuestRatingRequest import com.android.trisolarispms.data.api.model.GuestRatingRequest
import com.android.trisolarispms.data.api.model.GuestUpdateRequest import com.android.trisolarispms.data.api.model.GuestUpdateRequest
import com.android.trisolarispms.data.api.model.GuestVisitCountResponse
import com.android.trisolarispms.data.api.model.GuestVehicleDto import com.android.trisolarispms.data.api.model.GuestVehicleDto
import com.android.trisolarispms.data.api.model.GuestVehicleRequest import com.android.trisolarispms.data.api.model.GuestVehicleRequest
import retrofit2.Response import retrofit2.Response
@@ -36,6 +37,12 @@ interface GuestApi {
@Query("vehicleNumber") vehicleNumber: String? = null @Query("vehicleNumber") vehicleNumber: String? = null
): Response<List<GuestDto>> ): Response<List<GuestDto>>
@GET("properties/{propertyId}/guests/visit-count")
suspend fun getGuestVisitCount(
@Path("propertyId") propertyId: String,
@Query("phone") phone: String
): Response<GuestVisitCountResponse>
@GET("properties/{propertyId}/guests/{guestId}") @GET("properties/{propertyId}/guests/{guestId}")
suspend fun getGuest( suspend fun getGuest(
@Path("propertyId") propertyId: String, @Path("propertyId") propertyId: String,

View File

@@ -14,8 +14,10 @@ data class BookingCreateRequest(
val source: String? = null, val source: String? = null,
val guestPhoneE164: String? = null, val guestPhoneE164: String? = null,
val transportMode: String? = null, val transportMode: String? = null,
val adultCount: Int? = null, val childCount: Int? = null,
val totalGuestCount: Int? = null, val maleCount: Int? = null,
val femaleCount: Int? = null,
val expectedGuestCount: Int? = null,
val notes: String? = null val notes: String? = null
) )

View File

@@ -64,3 +64,8 @@ data class GuestDocumentDto(
val extractedData: Map<String, String>? = null, val extractedData: Map<String, String>? = null,
val extractedAt: String? = null val extractedAt: String? = null
) )
data class GuestVisitCountResponse(
val guestId: String? = null,
val bookingCount: Int? = null
)

View File

@@ -174,9 +174,51 @@ fun BookingCreateScreen(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Text(text = "Guest Phone (optional)", style = MaterialTheme.typography.titleSmall)
Spacer(modifier = Modifier.height(6.dp))
val selectedCountry = findPhoneCountryOption(state.phoneCountryCode) val selectedCountry = findPhoneCountryOption(state.phoneCountryCode)
val phoneDigitsLength = state.phoneNationalNumber.length
val phoneIsComplete = phoneDigitsLength == selectedCountry.maxLength
val phoneE164 = if (phoneIsComplete) {
"+${selectedCountry.dialCode}${state.phoneNationalNumber}"
} else {
null
}
LaunchedEffect(propertyId, phoneE164) {
if (phoneE164 != null) {
viewModel.fetchPhoneVisitCount(propertyId, phoneE164)
} else {
viewModel.clearPhoneVisitCount()
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "Guest Phone (optional)", style = MaterialTheme.typography.titleSmall)
if (phoneIsComplete && state.phoneVisitCountLoading) {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Text(text = "|", style = MaterialTheme.typography.titleSmall)
Text(
text = "checking...",
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.titleSmall
)
}
} else {
val count = state.phoneVisitCount ?: 0
if (count > 0) {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Text(text = "|", style = MaterialTheme.typography.titleSmall)
Text(
text = "${count} times visited",
color = Color(0xFF2E7D32),
style = MaterialTheme.typography.titleSmall
)
}
}
}
}
Spacer(modifier = Modifier.height(6.dp))
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -307,20 +349,44 @@ fun BookingCreateScreen(
} }
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField( OutlinedTextField(
value = state.adultCount, value = state.childCount,
onValueChange = viewModel::onAdultCountChange, onValueChange = viewModel::onChildCountChange,
label = { Text("Adult Count (optional)") }, label = { Text("Child Count (optional)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField( Row(
value = state.totalGuestCount, modifier = Modifier.fillMaxWidth(),
onValueChange = viewModel::onTotalGuestCountChange, horizontalArrangement = Arrangement.spacedBy(8.dp),
label = { Text("Total Guest Count (optional)") }, verticalAlignment = Alignment.CenterVertically
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), ) {
modifier = Modifier.fillMaxWidth() OutlinedTextField(
) value = state.maleCount,
onValueChange = viewModel::onMaleCountChange,
label = { Text("Male Count (optional)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.weight(1f)
)
OutlinedTextField(
value = state.femaleCount,
onValueChange = viewModel::onFemaleCountChange,
label = { Text("Female Count (optional)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.weight(1f)
)
}
val showExpectedGuestCount = state.maleCount.isBlank() && state.femaleCount.isBlank()
if (showExpectedGuestCount) {
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
value = state.expectedGuestCount,
onValueChange = viewModel::onExpectedGuestCountChange,
label = { Text("Expected Guest Count (optional)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
}
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField( OutlinedTextField(
value = state.notes, value = state.notes,

View File

@@ -3,12 +3,17 @@ package com.android.trisolarispms.ui.booking
data class BookingCreateState( data class BookingCreateState(
val phoneCountryCode: String = "IN", val phoneCountryCode: String = "IN",
val phoneNationalNumber: String = "", val phoneNationalNumber: String = "",
val phoneVisitCount: Int? = null,
val phoneVisitCountLoading: Boolean = false,
val phoneVisitCountPhone: String? = null,
val expectedCheckInAt: String = "", val expectedCheckInAt: String = "",
val expectedCheckOutAt: String = "", val expectedCheckOutAt: String = "",
val source: String = "WALKIN", val source: String = "WALKIN",
val transportMode: String = "CAR", val transportMode: String = "CAR",
val adultCount: String = "", val childCount: String = "",
val totalGuestCount: String = "", val maleCount: String = "",
val femaleCount: String = "",
val expectedGuestCount: String = "",
val notes: String = "", val notes: String = "",
val isLoading: Boolean = false, val isLoading: Boolean = false,
val error: String? = null val error: String? = null

View File

@@ -31,14 +31,57 @@ class BookingCreateViewModel : ViewModel() {
val option = findPhoneCountryOption(value) val option = findPhoneCountryOption(value)
_state.update { current -> _state.update { current ->
val trimmed = current.phoneNationalNumber.filter { it.isDigit() }.take(option.maxLength) val trimmed = current.phoneNationalNumber.filter { it.isDigit() }.take(option.maxLength)
current.copy(phoneCountryCode = value, phoneNationalNumber = trimmed, error = null) current.copy(
phoneCountryCode = value,
phoneNationalNumber = trimmed,
phoneVisitCount = if (trimmed.length == option.maxLength) current.phoneVisitCount else null,
phoneVisitCountPhone = null,
phoneVisitCountLoading = false,
error = null
)
} }
} }
fun onPhoneNationalNumberChange(value: String) { fun onPhoneNationalNumberChange(value: String) {
val option = findPhoneCountryOption(_state.value.phoneCountryCode) val option = findPhoneCountryOption(_state.value.phoneCountryCode)
val trimmed = value.filter { it.isDigit() }.take(option.maxLength) val trimmed = value.filter { it.isDigit() }.take(option.maxLength)
_state.update { it.copy(phoneNationalNumber = trimmed, error = null) } _state.update {
it.copy(
phoneNationalNumber = trimmed,
phoneVisitCount = if (trimmed.length == option.maxLength) it.phoneVisitCount else null,
error = null
)
}
}
fun clearPhoneVisitCount() {
_state.update { it.copy(phoneVisitCount = null, phoneVisitCountPhone = null, phoneVisitCountLoading = false) }
}
fun fetchPhoneVisitCount(propertyId: String, phoneE164: String) {
val current = _state.value
if (current.phoneVisitCountLoading || current.phoneVisitCountPhone == phoneE164) return
viewModelScope.launch {
_state.update { it.copy(phoneVisitCountLoading = true, phoneVisitCountPhone = phoneE164, error = null) }
try {
val api = ApiClient.create()
val response = api.getGuestVisitCount(propertyId = propertyId, phone = phoneE164)
val body = response.body()
if (response.isSuccessful && body != null) {
_state.update {
it.copy(
phoneVisitCount = body.bookingCount ?: 0,
phoneVisitCountLoading = false,
error = null
)
}
} else {
_state.update { it.copy(phoneVisitCountLoading = false) }
}
} catch (_: Exception) {
_state.update { it.copy(phoneVisitCountLoading = false) }
}
}
} }
fun onSourceChange(value: String) { fun onSourceChange(value: String) {
@@ -49,12 +92,20 @@ class BookingCreateViewModel : ViewModel() {
_state.update { it.copy(transportMode = value, error = null) } _state.update { it.copy(transportMode = value, error = null) }
} }
fun onAdultCountChange(value: String) { fun onChildCountChange(value: String) {
_state.update { it.copy(adultCount = value.filter { it.isDigit() }, error = null) } _state.update { it.copy(childCount = value.filter { it.isDigit() }, error = null) }
} }
fun onTotalGuestCountChange(value: String) { fun onMaleCountChange(value: String) {
_state.update { it.copy(totalGuestCount = value.filter { it.isDigit() }, error = null) } _state.update { it.copy(maleCount = value.filter { it.isDigit() }, error = null) }
}
fun onFemaleCountChange(value: String) {
_state.update { it.copy(femaleCount = value.filter { it.isDigit() }, error = null) }
}
fun onExpectedGuestCountChange(value: String) {
_state.update { it.copy(expectedGuestCount = value.filter { it.isDigit() }, error = null) }
} }
fun onNotesChange(value: String) { fun onNotesChange(value: String) {
@@ -69,8 +120,10 @@ class BookingCreateViewModel : ViewModel() {
_state.update { it.copy(error = "Check-in and check-out are required") } _state.update { it.copy(error = "Check-in and check-out are required") }
return return
} }
val adultCount = current.adultCount.toIntOrNull() val childCount = current.childCount.toIntOrNull()
val totalGuestCount = current.totalGuestCount.toIntOrNull() val maleCount = current.maleCount.toIntOrNull()
val femaleCount = current.femaleCount.toIntOrNull()
val expectedGuestCount = current.expectedGuestCount.toIntOrNull()
viewModelScope.launch { viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) } _state.update { it.copy(isLoading = true, error = null) }
try { try {
@@ -90,8 +143,10 @@ class BookingCreateViewModel : ViewModel() {
source = current.source.trim().ifBlank { null }, source = current.source.trim().ifBlank { null },
guestPhoneE164 = phone, guestPhoneE164 = phone,
transportMode = current.transportMode.trim().ifBlank { null }, transportMode = current.transportMode.trim().ifBlank { null },
adultCount = adultCount, childCount = childCount,
totalGuestCount = totalGuestCount, maleCount = maleCount,
femaleCount = femaleCount,
expectedGuestCount = expectedGuestCount,
notes = current.notes.trim().ifBlank { null } notes = current.notes.trim().ifBlank { null }
) )
) )