create booking : improve form and ui further
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user