createBooking: change checkout date based on property policy while editing checking date

This commit is contained in:
androidlover5842
2026-02-04 14:58:16 +05:30
parent 3a90aa848d
commit 9d942d6411
4 changed files with 99 additions and 15 deletions

View File

@@ -112,6 +112,22 @@ data class BookingBillableNightsResponse(
val billableNights: Long? = null val billableNights: Long? = null
) )
data class BookingExpectedCheckoutPreviewRequest(
val checkInAt: String,
val billableNights: Int? = null,
val billingMode: BookingBillingMode? = null,
val billingCheckinTime: String? = null,
val billingCheckoutTime: String? = null
)
data class BookingExpectedCheckoutPreviewResponse(
val expectedCheckOutAt: String? = null,
val billableNights: Int? = null,
val billingMode: BookingBillingMode? = null,
val billingCheckinTime: String? = null,
val billingCheckoutTime: String? = null
)
data class BookingDetailsResponse( data class BookingDetailsResponse(
val id: String? = null, val id: String? = null,
val status: String? = null, val status: String? = null,

View File

@@ -13,6 +13,8 @@ import com.android.trisolarispms.data.api.model.BookingListItem
import com.android.trisolarispms.data.api.model.BookingBulkCheckInRequest import com.android.trisolarispms.data.api.model.BookingBulkCheckInRequest
import com.android.trisolarispms.data.api.model.BookingBillableNightsRequest import com.android.trisolarispms.data.api.model.BookingBillableNightsRequest
import com.android.trisolarispms.data.api.model.BookingBillableNightsResponse import com.android.trisolarispms.data.api.model.BookingBillableNightsResponse
import com.android.trisolarispms.data.api.model.BookingExpectedCheckoutPreviewRequest
import com.android.trisolarispms.data.api.model.BookingExpectedCheckoutPreviewResponse
import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest
import com.android.trisolarispms.data.api.model.BookingDetailsResponse import com.android.trisolarispms.data.api.model.BookingDetailsResponse
import com.android.trisolarispms.data.api.model.BookingBalanceResponse import com.android.trisolarispms.data.api.model.BookingBalanceResponse
@@ -69,6 +71,12 @@ interface BookingApi {
@Body body: BookingBillableNightsRequest @Body body: BookingBillableNightsRequest
): Response<BookingBillableNightsResponse> ): Response<BookingBillableNightsResponse>
@POST("properties/{propertyId}/bookings/expected-checkout-preview")
suspend fun previewExpectedCheckout(
@Path("propertyId") propertyId: String,
@Body body: BookingExpectedCheckoutPreviewRequest
): Response<BookingExpectedCheckoutPreviewResponse>
@POST("properties/{propertyId}/bookings/{bookingId}/billing-policy") @POST("properties/{propertyId}/bookings/{bookingId}/billing-policy")
suspend fun updateBookingBillingPolicy( suspend fun updateBookingBillingPolicy(
@Path("propertyId") propertyId: String, @Path("propertyId") propertyId: String,

View File

@@ -66,24 +66,36 @@ fun BookingCreateScreen(
val transportOptions = listOf("CAR", "BIKE", "TRAIN", "PLANE", "BUS", "FOOT", "CYCLE", "OTHER") val transportOptions = listOf("CAR", "BIKE", "TRAIN", "PLANE", "BUS", "FOOT", "CYCLE", "OTHER")
val billingModeMenuExpanded = remember { mutableStateOf(false) } val billingModeMenuExpanded = remember { mutableStateOf(false) }
val displayFormatter = remember { DateTimeFormatter.ofPattern("dd-MM-yy HH:mm") } val displayFormatter = remember { DateTimeFormatter.ofPattern("dd-MM-yy HH:mm") }
val timeFormatter = remember { DateTimeFormatter.ofPattern("HH:mm") }
val phoneCountryMenuExpanded = remember { mutableStateOf(false) } val phoneCountryMenuExpanded = remember { mutableStateOf(false) }
val phoneCountries = remember { phoneCountryOptions() } val phoneCountries = remember { phoneCountryOptions() }
val phoneCountrySearch = remember { mutableStateOf("") } val phoneCountrySearch = remember { mutableStateOf("") }
val applyCheckInSelection: (LocalDate, String) -> Unit = { date, time ->
checkInDate.value = date
checkInTime.value = time
val checkInAt = formatBookingIso(date, time)
viewModel.onExpectedCheckInAtChange(checkInAt)
viewModel.autoSetBillingFromCheckIn(checkInAt)
viewModel.refreshExpectedCheckoutPreview(propertyId)
}
LaunchedEffect(propertyId) { LaunchedEffect(propertyId) {
viewModel.reset() viewModel.reset()
viewModel.loadBillingPolicy(propertyId) viewModel.loadBillingPolicy(propertyId)
val now = OffsetDateTime.now() val now = OffsetDateTime.now()
checkInDate.value = now.toLocalDate()
checkInTime.value = now.format(DateTimeFormatter.ofPattern("HH:mm"))
val nowIso = now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
viewModel.onExpectedCheckInAtChange(nowIso)
viewModel.autoSetBillingFromCheckIn(nowIso)
checkInNow.value = true checkInNow.value = true
val defaultCheckoutDate = now.toLocalDate().plusDays(1) val defaultCheckoutDate = now.toLocalDate().plusDays(1)
checkOutDate.value = defaultCheckoutDate checkOutDate.value = defaultCheckoutDate
checkOutTime.value = "11:00" checkOutTime.value = "11:00"
viewModel.onExpectedCheckOutAtChange(formatBookingIso(defaultCheckoutDate, checkOutTime.value)) viewModel.onExpectedCheckOutAtChange(formatBookingIso(defaultCheckoutDate, checkOutTime.value))
applyCheckInSelection(now.toLocalDate(), now.format(timeFormatter))
}
LaunchedEffect(state.expectedCheckOutAt) {
val parsed = runCatching { OffsetDateTime.parse(state.expectedCheckOutAt) }.getOrNull() ?: return@LaunchedEffect
checkOutDate.value = parsed.toLocalDate()
checkOutTime.value = parsed.format(timeFormatter)
} }
SaveTopBarScaffold( SaveTopBarScaffold(
@@ -111,11 +123,7 @@ fun BookingCreateScreen(
checkInNow.value = enabled checkInNow.value = enabled
if (enabled) { if (enabled) {
val now = OffsetDateTime.now() val now = OffsetDateTime.now()
checkInDate.value = now.toLocalDate() applyCheckInSelection(now.toLocalDate(), now.format(timeFormatter))
checkInTime.value = now.format(DateTimeFormatter.ofPattern("HH:mm"))
val nowIso = now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
viewModel.onExpectedCheckInAtChange(nowIso)
viewModel.autoSetBillingFromCheckIn(nowIso)
} else { } else {
viewModel.onExpectedCheckInAtChange("") viewModel.onExpectedCheckInAtChange("")
} }
@@ -181,6 +189,7 @@ fun BookingCreateScreen(
onClick = { onClick = {
billingModeMenuExpanded.value = false billingModeMenuExpanded.value = false
viewModel.onBillingModeChange(mode) viewModel.onBillingModeChange(mode)
viewModel.refreshExpectedCheckoutPreview(propertyId)
} }
) )
} }
@@ -191,7 +200,10 @@ fun BookingCreateScreen(
BookingTimePickerTextField( BookingTimePickerTextField(
value = state.billingCheckoutTime, value = state.billingCheckoutTime,
label = { Text("Billing check-out (HH:mm)") }, label = { Text("Billing check-out (HH:mm)") },
onTimeSelected = viewModel::onBillingCheckoutTimeChange, onTimeSelected = { selectedTime ->
viewModel.onBillingCheckoutTimeChange(selectedTime)
viewModel.refreshExpectedCheckoutPreview(propertyId)
},
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
} }
@@ -481,10 +493,7 @@ fun BookingCreateScreen(
minDate = LocalDate.now(), minDate = LocalDate.now(),
onDismiss = { showCheckInPicker.value = false }, onDismiss = { showCheckInPicker.value = false },
onConfirm = { date, time -> onConfirm = { date, time ->
checkInDate.value = date applyCheckInSelection(date, time)
checkInTime.value = time
val formatted = formatBookingIso(date, time)
viewModel.onExpectedCheckInAtChange(formatted)
showCheckInPicker.value = false showCheckInPicker.value = false
} }
) )

View File

@@ -6,6 +6,7 @@ import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.BookingBillingMode import com.android.trisolarispms.data.api.model.BookingBillingMode
import com.android.trisolarispms.data.api.model.BookingCreateRequest import com.android.trisolarispms.data.api.model.BookingCreateRequest
import com.android.trisolarispms.data.api.model.BookingCreateResponse import com.android.trisolarispms.data.api.model.BookingCreateResponse
import com.android.trisolarispms.data.api.model.BookingExpectedCheckoutPreviewRequest
import com.android.trisolarispms.data.api.model.GuestDto import com.android.trisolarispms.data.api.model.GuestDto
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -14,10 +15,16 @@ import kotlinx.coroutines.launch
import java.time.OffsetDateTime import java.time.OffsetDateTime
class BookingCreateViewModel : ViewModel() { class BookingCreateViewModel : ViewModel() {
private companion object {
const val DEFAULT_PREVIEW_BILLABLE_NIGHTS = 1
}
private val _state = MutableStateFlow(BookingCreateState()) private val _state = MutableStateFlow(BookingCreateState())
val state: StateFlow<BookingCreateState> = _state val state: StateFlow<BookingCreateState> = _state
private var expectedCheckoutPreviewRequestId: Long = 0
fun reset() { fun reset() {
expectedCheckoutPreviewRequestId = 0
_state.value = BookingCreateState() _state.value = BookingCreateState()
} }
@@ -76,6 +83,50 @@ class BookingCreateViewModel : ViewModel() {
_state.update { it.copy(billingCheckoutTime = value, error = null) } _state.update { it.copy(billingCheckoutTime = value, error = null) }
} }
fun refreshExpectedCheckoutPreview(propertyId: String) {
if (propertyId.isBlank()) return
val requestBody = buildExpectedCheckoutPreviewRequest(_state.value) ?: return
val requestId = ++expectedCheckoutPreviewRequestId
viewModelScope.launch {
try {
val api = ApiClient.create()
val response = api.previewExpectedCheckout(
propertyId = propertyId,
body = requestBody
)
val expectedCheckOutAt = response.body()?.expectedCheckOutAt?.trim().orEmpty()
if (!response.isSuccessful || expectedCheckOutAt.isBlank() || requestId != expectedCheckoutPreviewRequestId) {
return@launch
}
_state.update { current ->
if (requestId != expectedCheckoutPreviewRequestId) {
current
} else {
current.copy(expectedCheckOutAt = expectedCheckOutAt, error = null)
}
}
} catch (_: Exception) {
// Keep user-entered check-out on preview failures.
}
}
}
private fun buildExpectedCheckoutPreviewRequest(state: BookingCreateState): BookingExpectedCheckoutPreviewRequest? {
val expectedCheckInAt = state.expectedCheckInAt.trim()
if (expectedCheckInAt.isBlank()) return null
val customBillingCheckoutTime = state.billingCheckoutTime.trim().ifBlank { null }
return BookingExpectedCheckoutPreviewRequest(
checkInAt = expectedCheckInAt,
billableNights = DEFAULT_PREVIEW_BILLABLE_NIGHTS,
billingMode = state.billingMode,
billingCheckoutTime = if (state.billingMode == BookingBillingMode.CUSTOM_WINDOW) {
customBillingCheckoutTime
} else {
null
}
)
}
fun onPhoneCountryChange(value: String) { fun onPhoneCountryChange(value: String) {
val option = findPhoneCountryOption(value) val option = findPhoneCountryOption(value)
_state.update { current -> _state.update { current ->