diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 24360ed..553e734 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,6 +58,7 @@ dependencies { implementation(libs.coil.compose) implementation(libs.lottie.compose) implementation(libs.calendar.compose) + implementation(libs.libphonenumber) implementation(platform(libs.firebase.bom)) implementation(libs.firebase.auth.ktx) implementation(libs.kotlinx.coroutines.play.services) diff --git a/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateScreen.kt index 058e8fd..64bedbc 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateScreen.kt @@ -3,7 +3,6 @@ package com.android.trisolarispms.ui.booking import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -47,7 +46,6 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.kizitonwose.calendar.compose.HorizontalCalendar import com.kizitonwose.calendar.compose.rememberCalendarState -import com.android.trisolarispms.data.api.model.GuestDto import com.kizitonwose.calendar.core.CalendarDay import com.kizitonwose.calendar.core.CalendarMonth import com.kizitonwose.calendar.core.DayPosition @@ -77,7 +75,12 @@ fun BookingCreateScreen( val checkInNow = remember { mutableStateOf(true) } val sourceMenuExpanded = remember { mutableStateOf(false) } val sourceOptions = listOf("WALKIN", "OTA", "AGENT") + val transportMenuExpanded = remember { mutableStateOf(false) } + val transportOptions = listOf("CAR", "BIKE", "TRAIN", "PLANE", "BUS", "FOOT", "CYCLE", "OTHER") val displayFormatter = remember { DateTimeFormatter.ofPattern("dd-MM-yy HH:mm") } + val phoneCountryMenuExpanded = remember { mutableStateOf(false) } + val phoneCountries = remember { phoneCountryOptions() } + val phoneCountrySearch = remember { mutableStateOf("") } LaunchedEffect(propertyId) { viewModel.reset() @@ -171,12 +174,75 @@ fun BookingCreateScreen( modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(12.dp)) - OutlinedTextField( - value = state.phoneE164, - onValueChange = viewModel::onPhoneChange, - label = { Text("Guest Phone E164 (optional)") }, - modifier = Modifier.fillMaxWidth() - ) + Text(text = "Guest Phone (optional)", style = MaterialTheme.typography.titleSmall) + Spacer(modifier = Modifier.height(6.dp)) + val selectedCountry = findPhoneCountryOption(state.phoneCountryCode) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.Top + ) { + ExposedDropdownMenuBox( + expanded = phoneCountryMenuExpanded.value, + onExpandedChange = { phoneCountryMenuExpanded.value = !phoneCountryMenuExpanded.value }, + modifier = Modifier.weight(0.3f) + ) { + OutlinedTextField( + value = "${selectedCountry.code} +${selectedCountry.dialCode}", + onValueChange = {}, + readOnly = true, + label = { Text("Country") }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = phoneCountryMenuExpanded.value) + }, + modifier = Modifier + .fillMaxWidth() + .menuAnchor() + ) + ExposedDropdownMenu( + expanded = phoneCountryMenuExpanded.value, + onDismissRequest = { phoneCountryMenuExpanded.value = false } + ) { + OutlinedTextField( + value = phoneCountrySearch.value, + onValueChange = { phoneCountrySearch.value = it }, + label = { Text("Search") }, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) + val filteredCountries = phoneCountries.filter { option -> + val query = phoneCountrySearch.value.trim() + if (query.isBlank()) { + true + } else { + option.name.contains(query, ignoreCase = true) || + option.code.contains(query, ignoreCase = true) || + option.dialCode.contains(query) + } + } + filteredCountries.forEach { option -> + DropdownMenuItem( + text = { Text("${option.name} (+${option.dialCode})") }, + onClick = { + phoneCountryMenuExpanded.value = false + phoneCountrySearch.value = "" + viewModel.onPhoneCountryChange(option.code) + } + ) + } + } + } + OutlinedTextField( + value = state.phoneNationalNumber, + onValueChange = viewModel::onPhoneNationalNumberChange, + label = { Text("Number") }, + prefix = { Text("+${selectedCountry.dialCode}") }, + supportingText = { Text("Max ${selectedCountry.maxLength} digits") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), + modifier = Modifier.weight(0.7f) + ) + } Spacer(modifier = Modifier.height(12.dp)) ExposedDropdownMenuBox( expanded = sourceMenuExpanded.value, @@ -208,12 +274,37 @@ fun BookingCreateScreen( } } Spacer(modifier = Modifier.height(12.dp)) - OutlinedTextField( - value = state.transportMode, - onValueChange = viewModel::onTransportModeChange, - label = { Text("Transport Mode (optional)") }, - modifier = Modifier.fillMaxWidth() - ) + ExposedDropdownMenuBox( + expanded = transportMenuExpanded.value, + onExpandedChange = { transportMenuExpanded.value = !transportMenuExpanded.value } + ) { + OutlinedTextField( + value = state.transportMode, + onValueChange = {}, + readOnly = true, + label = { Text("Transport Mode") }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = transportMenuExpanded.value) + }, + modifier = Modifier + .fillMaxWidth() + .menuAnchor() + ) + ExposedDropdownMenu( + expanded = transportMenuExpanded.value, + onDismissRequest = { transportMenuExpanded.value = false } + ) { + transportOptions.forEach { option -> + DropdownMenuItem( + text = { Text(option) }, + onClick = { + transportMenuExpanded.value = false + viewModel.onTransportModeChange(option) + } + ) + } + } + } Spacer(modifier = Modifier.height(12.dp)) OutlinedTextField( value = state.adultCount, diff --git a/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateState.kt b/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateState.kt index cff5728..2d674f3 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateState.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateState.kt @@ -1,11 +1,12 @@ package com.android.trisolarispms.ui.booking data class BookingCreateState( - val phoneE164: String = "", + val phoneCountryCode: String = "IN", + val phoneNationalNumber: String = "", val expectedCheckInAt: String = "", val expectedCheckOutAt: String = "", val source: String = "WALKIN", - val transportMode: String = "", + val transportMode: String = "CAR", val adultCount: String = "", val totalGuestCount: String = "", val notes: String = "", diff --git a/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateViewModel.kt b/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateViewModel.kt index 44536c0..8c77066 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateViewModel.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateViewModel.kt @@ -28,8 +28,18 @@ class BookingCreateViewModel : ViewModel() { _state.update { it.copy(expectedCheckOutAt = value, error = null) } } - fun onPhoneChange(value: String) { - _state.update { it.copy(phoneE164 = value, error = null) } + fun onPhoneCountryChange(value: String) { + val option = findPhoneCountryOption(value) + _state.update { current -> + val trimmed = current.phoneNationalNumber.filter { it.isDigit() }.take(option.maxLength) + current.copy(phoneCountryCode = value, phoneNationalNumber = trimmed, error = null) + } + } + + fun onPhoneNationalNumberChange(value: String) { + val option = findPhoneCountryOption(_state.value.phoneCountryCode) + val trimmed = value.filter { it.isDigit() }.take(option.maxLength) + _state.update { it.copy(phoneNationalNumber = trimmed, error = null) } } fun onSourceChange(value: String) { @@ -66,7 +76,13 @@ class BookingCreateViewModel : ViewModel() { _state.update { it.copy(isLoading = true, error = null) } try { val api = ApiClient.create() - val phone = current.phoneE164.trim().ifBlank { null } + val phoneCountry = findPhoneCountryOption(current.phoneCountryCode) + val phoneDigits = current.phoneNationalNumber.trim() + val phone = if (phoneDigits.isNotBlank()) { + "+${phoneCountry.dialCode}$phoneDigits" + } else { + null + } val existingGuest = if (!phone.isNullOrBlank()) { val guestResponse = api.searchGuests(propertyId, phone = phone) if (guestResponse.isSuccessful) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1185d2b..dee6fd5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,7 @@ vectordrawable = "1.2.0" coilCompose = "2.7.0" lottieCompose = "6.7.1" calendarCompose = "2.6.0" +libphonenumber = "8.13.34" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -50,6 +51,7 @@ androidx-vectordrawable-animated = { group = "androidx.vectordrawable", name = " coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coilCompose" } lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "lottieCompose" } calendar-compose = { group = "com.kizitonwose.calendar", name = "compose", version.ref = "calendarCompose" } +libphonenumber = { group = "com.googlecode.libphonenumber", name = "libphonenumber", version.ref = "libphonenumber" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }