createBooking: change checkout date based on property policy while editing checking date
This commit is contained in:
@@ -112,6 +112,22 @@ data class BookingBillableNightsResponse(
|
||||
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(
|
||||
val id: String? = null,
|
||||
val status: String? = null,
|
||||
|
||||
@@ -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.BookingBillableNightsRequest
|
||||
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.BookingDetailsResponse
|
||||
import com.android.trisolarispms.data.api.model.BookingBalanceResponse
|
||||
@@ -69,6 +71,12 @@ interface BookingApi {
|
||||
@Body body: BookingBillableNightsRequest
|
||||
): 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")
|
||||
suspend fun updateBookingBillingPolicy(
|
||||
@Path("propertyId") propertyId: String,
|
||||
|
||||
@@ -66,24 +66,36 @@ fun BookingCreateScreen(
|
||||
val transportOptions = listOf("CAR", "BIKE", "TRAIN", "PLANE", "BUS", "FOOT", "CYCLE", "OTHER")
|
||||
val billingModeMenuExpanded = remember { mutableStateOf(false) }
|
||||
val displayFormatter = remember { DateTimeFormatter.ofPattern("dd-MM-yy HH:mm") }
|
||||
val timeFormatter = remember { DateTimeFormatter.ofPattern("HH:mm") }
|
||||
val phoneCountryMenuExpanded = remember { mutableStateOf(false) }
|
||||
val phoneCountries = remember { phoneCountryOptions() }
|
||||
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) {
|
||||
viewModel.reset()
|
||||
viewModel.loadBillingPolicy(propertyId)
|
||||
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
|
||||
val defaultCheckoutDate = now.toLocalDate().plusDays(1)
|
||||
checkOutDate.value = defaultCheckoutDate
|
||||
checkOutTime.value = "11:00"
|
||||
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(
|
||||
@@ -111,11 +123,7 @@ fun BookingCreateScreen(
|
||||
checkInNow.value = enabled
|
||||
if (enabled) {
|
||||
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)
|
||||
applyCheckInSelection(now.toLocalDate(), now.format(timeFormatter))
|
||||
} else {
|
||||
viewModel.onExpectedCheckInAtChange("")
|
||||
}
|
||||
@@ -181,6 +189,7 @@ fun BookingCreateScreen(
|
||||
onClick = {
|
||||
billingModeMenuExpanded.value = false
|
||||
viewModel.onBillingModeChange(mode)
|
||||
viewModel.refreshExpectedCheckoutPreview(propertyId)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -191,7 +200,10 @@ fun BookingCreateScreen(
|
||||
BookingTimePickerTextField(
|
||||
value = state.billingCheckoutTime,
|
||||
label = { Text("Billing check-out (HH:mm)") },
|
||||
onTimeSelected = viewModel::onBillingCheckoutTimeChange,
|
||||
onTimeSelected = { selectedTime ->
|
||||
viewModel.onBillingCheckoutTimeChange(selectedTime)
|
||||
viewModel.refreshExpectedCheckoutPreview(propertyId)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
@@ -481,10 +493,7 @@ fun BookingCreateScreen(
|
||||
minDate = LocalDate.now(),
|
||||
onDismiss = { showCheckInPicker.value = false },
|
||||
onConfirm = { date, time ->
|
||||
checkInDate.value = date
|
||||
checkInTime.value = time
|
||||
val formatted = formatBookingIso(date, time)
|
||||
viewModel.onExpectedCheckInAtChange(formatted)
|
||||
applyCheckInSelection(date, time)
|
||||
showCheckInPicker.value = false
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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.BookingCreateRequest
|
||||
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 kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -14,10 +15,16 @@ import kotlinx.coroutines.launch
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
class BookingCreateViewModel : ViewModel() {
|
||||
private companion object {
|
||||
const val DEFAULT_PREVIEW_BILLABLE_NIGHTS = 1
|
||||
}
|
||||
|
||||
private val _state = MutableStateFlow(BookingCreateState())
|
||||
val state: StateFlow<BookingCreateState> = _state
|
||||
private var expectedCheckoutPreviewRequestId: Long = 0
|
||||
|
||||
fun reset() {
|
||||
expectedCheckoutPreviewRequestId = 0
|
||||
_state.value = BookingCreateState()
|
||||
}
|
||||
|
||||
@@ -76,6 +83,50 @@ class BookingCreateViewModel : ViewModel() {
|
||||
_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) {
|
||||
val option = findPhoneCountryOption(value)
|
||||
_state.update { current ->
|
||||
|
||||
Reference in New Issue
Block a user