createbooking: improve logic for future bookings
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
package com.android.trisolarispms.core.booking
|
||||||
|
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
|
private val defaultPropertyZone: ZoneId = ZoneId.of("Asia/Kolkata")
|
||||||
|
|
||||||
|
fun isFutureBookingCheckIn(expectedCheckInAt: String?, zoneId: ZoneId = defaultPropertyZone): Boolean {
|
||||||
|
if (expectedCheckInAt.isNullOrBlank()) return false
|
||||||
|
val checkInDate = runCatching { OffsetDateTime.parse(expectedCheckInAt).toLocalDate() }.getOrNull() ?: return false
|
||||||
|
val today = LocalDate.now(zoneId)
|
||||||
|
return checkInDate.isAfter(today)
|
||||||
|
}
|
||||||
@@ -102,6 +102,13 @@ data class BookingExpectedDatesRequest(
|
|||||||
val expectedCheckOutAt: String? = null
|
val expectedCheckOutAt: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class BookingRoomRequestCreateRequest(
|
||||||
|
val roomTypeCode: String,
|
||||||
|
val quantity: Int,
|
||||||
|
val fromAt: String,
|
||||||
|
val toAt: String
|
||||||
|
)
|
||||||
|
|
||||||
data class BookingBillableNightsRequest(
|
data class BookingBillableNightsRequest(
|
||||||
val expectedCheckInAt: String? = null,
|
val expectedCheckInAt: String? = null,
|
||||||
val expectedCheckOutAt: String? = null
|
val expectedCheckOutAt: String? = null
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.android.trisolarispms.data.api.model
|
package com.android.trisolarispms.data.api.model
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
data class RoomCreateRequest(
|
data class RoomCreateRequest(
|
||||||
val roomNumber: Int,
|
val roomNumber: Int,
|
||||||
val floor: Int? = null,
|
val floor: Int? = null,
|
||||||
@@ -47,16 +49,11 @@ data class RoomAvailabilityResponse(
|
|||||||
)
|
)
|
||||||
|
|
||||||
data class RoomAvailabilityRangeResponse(
|
data class RoomAvailabilityRangeResponse(
|
||||||
val roomTypeName: String? = null,
|
@SerializedName(value = "roomTypeCode", alternate = ["code"])
|
||||||
val freeRoomNumbers: List<Int> = emptyList(),
|
|
||||||
val freeCount: Int? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class RoomAvailableRateResponse(
|
|
||||||
val roomId: String? = null,
|
|
||||||
val roomNumber: Int? = null,
|
|
||||||
val roomTypeCode: String? = null,
|
val roomTypeCode: String? = null,
|
||||||
val roomTypeName: String? = null,
|
val roomTypeName: String? = null,
|
||||||
|
val freeRoomNumbers: List<Int> = emptyList(),
|
||||||
|
val freeCount: Int? = null,
|
||||||
val averageRate: Double? = null,
|
val averageRate: Double? = null,
|
||||||
val currency: String? = null,
|
val currency: String? = null,
|
||||||
val ratePlanCode: String? = null
|
val ratePlanCode: String? = null
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.android.trisolarispms.data.api.model.BookingBillableNightsResponse
|
|||||||
import com.android.trisolarispms.data.api.model.BookingExpectedCheckoutPreviewRequest
|
import com.android.trisolarispms.data.api.model.BookingExpectedCheckoutPreviewRequest
|
||||||
import com.android.trisolarispms.data.api.model.BookingExpectedCheckoutPreviewResponse
|
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.BookingRoomRequestCreateRequest
|
||||||
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
|
||||||
import com.android.trisolarispms.data.api.model.BookingBillingPolicyUpdateRequest
|
import com.android.trisolarispms.data.api.model.BookingBillingPolicyUpdateRequest
|
||||||
@@ -64,6 +65,13 @@ interface BookingApi {
|
|||||||
@Body body: BookingExpectedDatesRequest
|
@Body body: BookingExpectedDatesRequest
|
||||||
): Response<Unit>
|
): Response<Unit>
|
||||||
|
|
||||||
|
@POST("properties/{propertyId}/bookings/{bookingId}/room-requests")
|
||||||
|
suspend fun createRoomRequest(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Path("bookingId") bookingId: String,
|
||||||
|
@Body body: BookingRoomRequestCreateRequest
|
||||||
|
): Response<Unit>
|
||||||
|
|
||||||
@POST("properties/{propertyId}/bookings/{bookingId}/billable-nights")
|
@POST("properties/{propertyId}/bookings/{bookingId}/billable-nights")
|
||||||
suspend fun previewBillableNights(
|
suspend fun previewBillableNights(
|
||||||
@Path("propertyId") propertyId: String,
|
@Path("propertyId") propertyId: String,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.android.trisolarispms.data.api.service
|
|||||||
|
|
||||||
import com.android.trisolarispms.data.api.model.RoomAvailabilityRangeResponse
|
import com.android.trisolarispms.data.api.model.RoomAvailabilityRangeResponse
|
||||||
import com.android.trisolarispms.data.api.model.RoomAvailabilityResponse
|
import com.android.trisolarispms.data.api.model.RoomAvailabilityResponse
|
||||||
import com.android.trisolarispms.data.api.model.RoomAvailableRateResponse
|
|
||||||
import com.android.trisolarispms.data.api.model.RoomBoardDto
|
import com.android.trisolarispms.data.api.model.RoomBoardDto
|
||||||
import com.android.trisolarispms.data.api.model.RoomCreateRequest
|
import com.android.trisolarispms.data.api.model.RoomCreateRequest
|
||||||
import com.android.trisolarispms.data.api.model.RoomDto
|
import com.android.trisolarispms.data.api.model.RoomDto
|
||||||
@@ -55,7 +54,8 @@ interface RoomApi {
|
|||||||
suspend fun getRoomAvailabilityRange(
|
suspend fun getRoomAvailabilityRange(
|
||||||
@Path("propertyId") propertyId: String,
|
@Path("propertyId") propertyId: String,
|
||||||
@Query("from") from: String,
|
@Query("from") from: String,
|
||||||
@Query("to") to: String
|
@Query("to") to: String,
|
||||||
|
@Query("ratePlanCode") ratePlanCode: String? = null
|
||||||
): Response<List<RoomAvailabilityRangeResponse>>
|
): Response<List<RoomAvailabilityRangeResponse>>
|
||||||
|
|
||||||
@GET("properties/{propertyId}/rooms/available")
|
@GET("properties/{propertyId}/rooms/available")
|
||||||
@@ -63,14 +63,6 @@ interface RoomApi {
|
|||||||
@Path("propertyId") propertyId: String
|
@Path("propertyId") propertyId: String
|
||||||
): Response<List<RoomDto>>
|
): Response<List<RoomDto>>
|
||||||
|
|
||||||
@GET("properties/{propertyId}/rooms/available-range-with-rate")
|
|
||||||
suspend fun listAvailableRoomsWithRate(
|
|
||||||
@Path("propertyId") propertyId: String,
|
|
||||||
@Query("from") from: String,
|
|
||||||
@Query("to") to: String,
|
|
||||||
@Query("ratePlanCode") ratePlanCode: String? = null
|
|
||||||
): Response<List<RoomAvailableRateResponse>>
|
|
||||||
|
|
||||||
@GET("properties/{propertyId}/rooms/by-type/{roomTypeCode}")
|
@GET("properties/{propertyId}/rooms/by-type/{roomTypeCode}")
|
||||||
suspend fun listRoomsByType(
|
suspend fun listRoomsByType(
|
||||||
@Path("propertyId") propertyId: String,
|
@Path("propertyId") propertyId: String,
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import androidx.compose.material3.ExposedDropdownMenuDefaults
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -55,13 +54,10 @@ fun BookingCreateScreen(
|
|||||||
val checkOutDate = remember { mutableStateOf<LocalDate?>(null) }
|
val checkOutDate = remember { mutableStateOf<LocalDate?>(null) }
|
||||||
val checkInTime = remember { mutableStateOf("12:00") }
|
val checkInTime = remember { mutableStateOf("12:00") }
|
||||||
val checkOutTime = remember { mutableStateOf("11:00") }
|
val checkOutTime = remember { mutableStateOf("11:00") }
|
||||||
val checkInNow = remember { mutableStateOf(true) }
|
|
||||||
val sourceMenuExpanded = remember { mutableStateOf(false) }
|
|
||||||
val sourceOptions = listOf("DIRECT", "AGENT")
|
|
||||||
val relationMenuExpanded = remember { mutableStateOf(false) }
|
val relationMenuExpanded = remember { mutableStateOf(false) }
|
||||||
val relationOptions = listOf("FRIENDS", "FAMILY", "GROUP", "ALONE")
|
val relationOptions = listOf("FRIENDS", "FAMILY", "GROUP", "ALONE")
|
||||||
val transportMenuExpanded = remember { mutableStateOf(false) }
|
val transportMenuExpanded = remember { mutableStateOf(false) }
|
||||||
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 timeFormatter = remember { DateTimeFormatter.ofPattern("HH:mm") }
|
||||||
@@ -88,7 +84,6 @@ fun BookingCreateScreen(
|
|||||||
viewModel.reset()
|
viewModel.reset()
|
||||||
viewModel.loadBillingPolicy(propertyId)
|
viewModel.loadBillingPolicy(propertyId)
|
||||||
val now = OffsetDateTime.now()
|
val now = OffsetDateTime.now()
|
||||||
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"
|
||||||
@@ -115,24 +110,6 @@ fun BookingCreateScreen(
|
|||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
verticalArrangement = Arrangement.Top
|
verticalArrangement = Arrangement.Top
|
||||||
) {
|
) {
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(text = "Check in now")
|
|
||||||
Switch(
|
|
||||||
checked = checkInNow.value,
|
|
||||||
onCheckedChange = { enabled ->
|
|
||||||
checkInNow.value = enabled
|
|
||||||
if (enabled) {
|
|
||||||
val now = OffsetDateTime.now()
|
|
||||||
applyCheckInSelection(now.toLocalDate(), now.format(timeFormatter))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
val checkInDisplay = state.expectedCheckInAt.takeIf { it.isNotBlank() }?.let {
|
val checkInDisplay = state.expectedCheckInAt.takeIf { it.isNotBlank() }?.let {
|
||||||
runCatching { OffsetDateTime.parse(it) }.getOrNull()
|
runCatching { OffsetDateTime.parse(it) }.getOrNull()
|
||||||
}
|
}
|
||||||
@@ -152,7 +129,7 @@ fun BookingCreateScreen(
|
|||||||
checkOutDateText = checkOutDisplay?.format(cardDateFormatter) ?: "--/--/----",
|
checkOutDateText = checkOutDisplay?.format(cardDateFormatter) ?: "--/--/----",
|
||||||
checkOutTimeText = checkOutDisplay?.format(cardTimeFormatter) ?: "--:--",
|
checkOutTimeText = checkOutDisplay?.format(cardTimeFormatter) ?: "--:--",
|
||||||
totalTimeText = totalTimeText,
|
totalTimeText = totalTimeText,
|
||||||
checkInEditable = !checkInNow.value,
|
checkInEditable = true,
|
||||||
onCheckInDateClick = { showCheckInDatePicker.value = true },
|
onCheckInDateClick = { showCheckInDatePicker.value = true },
|
||||||
onCheckInTimeClick = { showCheckInTimePicker.value = true },
|
onCheckInTimeClick = { showCheckInTimePicker.value = true },
|
||||||
onCheckOutDateClick = { showCheckOutDatePicker.value = true },
|
onCheckOutDateClick = { showCheckOutDatePicker.value = true },
|
||||||
@@ -316,36 +293,6 @@ fun BookingCreateScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
ExposedDropdownMenuBox(
|
|
||||||
expanded = sourceMenuExpanded.value,
|
|
||||||
onExpandedChange = { sourceMenuExpanded.value = !sourceMenuExpanded.value }
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.source,
|
|
||||||
onValueChange = {},
|
|
||||||
readOnly = true,
|
|
||||||
label = { Text("Source") },
|
|
||||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = sourceMenuExpanded.value) },
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.menuAnchor()
|
|
||||||
)
|
|
||||||
ExposedDropdownMenu(
|
|
||||||
expanded = sourceMenuExpanded.value,
|
|
||||||
onDismissRequest = { sourceMenuExpanded.value = false }
|
|
||||||
) {
|
|
||||||
sourceOptions.forEach { option ->
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(option) },
|
|
||||||
onClick = {
|
|
||||||
sourceMenuExpanded.value = false
|
|
||||||
viewModel.onSourceChange(option)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = state.fromCity,
|
value = state.fromCity,
|
||||||
onValueChange = viewModel::onFromCityChange,
|
onValueChange = viewModel::onFromCityChange,
|
||||||
@@ -397,7 +344,7 @@ fun BookingCreateScreen(
|
|||||||
onExpandedChange = { transportMenuExpanded.value = !transportMenuExpanded.value }
|
onExpandedChange = { transportMenuExpanded.value = !transportMenuExpanded.value }
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = state.transportMode,
|
value = state.transportMode.ifBlank { "Not set" },
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
label = { Text("Transport Mode") },
|
label = { Text("Transport Mode") },
|
||||||
@@ -413,8 +360,9 @@ fun BookingCreateScreen(
|
|||||||
onDismissRequest = { transportMenuExpanded.value = false }
|
onDismissRequest = { transportMenuExpanded.value = false }
|
||||||
) {
|
) {
|
||||||
transportOptions.forEach { option ->
|
transportOptions.forEach { option ->
|
||||||
|
val optionLabel = option.ifBlank { "Not set" }
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(option) },
|
text = { Text(optionLabel) },
|
||||||
onClick = {
|
onClick = {
|
||||||
transportMenuExpanded.value = false
|
transportMenuExpanded.value = false
|
||||||
viewModel.onTransportModeChange(option)
|
viewModel.onTransportModeChange(option)
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ data class BookingCreateState(
|
|||||||
val expectedCheckOutAt: String = "",
|
val expectedCheckOutAt: String = "",
|
||||||
val billingMode: BookingBillingMode = BookingBillingMode.PROPERTY_POLICY,
|
val billingMode: BookingBillingMode = BookingBillingMode.PROPERTY_POLICY,
|
||||||
val billingCheckoutTime: String = "",
|
val billingCheckoutTime: String = "",
|
||||||
val source: String = "DIRECT",
|
val source: String = "",
|
||||||
val fromCity: String = "",
|
val fromCity: String = "",
|
||||||
val toCity: String = "",
|
val toCity: String = "",
|
||||||
val memberRelation: String = "",
|
val memberRelation: String = "",
|
||||||
val transportMode: String = "CAR",
|
val transportMode: String = "CAR",
|
||||||
|
val isTransportModeAuto: Boolean = true,
|
||||||
val childCount: String = "",
|
val childCount: String = "",
|
||||||
val maleCount: String = "",
|
val maleCount: String = "",
|
||||||
val femaleCount: String = "",
|
val femaleCount: String = "",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.android.trisolarispms.ui.booking
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.android.trisolarispms.core.booking.isFutureBookingCheckIn
|
||||||
import com.android.trisolarispms.data.api.core.ApiClient
|
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
|
||||||
@@ -29,7 +30,10 @@ class BookingCreateViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onExpectedCheckInAtChange(value: String) {
|
fun onExpectedCheckInAtChange(value: String) {
|
||||||
_state.update { it.copy(expectedCheckInAt = value, error = null) }
|
_state.update { current ->
|
||||||
|
val withCheckIn = current.copy(expectedCheckInAt = value, error = null)
|
||||||
|
withCheckIn.withDefaultTransportModeForCheckIn(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onExpectedCheckOutAtChange(value: String) {
|
fun onExpectedCheckOutAtChange(value: String) {
|
||||||
@@ -201,19 +205,34 @@ class BookingCreateViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onTransportModeChange(value: String) {
|
fun onTransportModeChange(value: String) {
|
||||||
_state.update { it.copy(transportMode = value, error = null) }
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
transportMode = value,
|
||||||
|
isTransportModeAuto = false,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onChildCountChange(value: String) {
|
fun onChildCountChange(value: String) {
|
||||||
_state.update { it.copy(childCount = value.filter { it.isDigit() }, error = null) }
|
_state.update { current ->
|
||||||
|
current.copy(childCount = value.filter { it.isDigit() }, error = null)
|
||||||
|
.withDefaultMemberRelationForFamily()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMaleCountChange(value: String) {
|
fun onMaleCountChange(value: String) {
|
||||||
_state.update { it.copy(maleCount = value.filter { it.isDigit() }, error = null) }
|
_state.update { current ->
|
||||||
|
current.copy(maleCount = value.filter { it.isDigit() }, error = null)
|
||||||
|
.withDefaultMemberRelationForFamily()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onFemaleCountChange(value: String) {
|
fun onFemaleCountChange(value: String) {
|
||||||
_state.update { it.copy(femaleCount = value.filter { it.isDigit() }, error = null) }
|
_state.update { current ->
|
||||||
|
current.copy(femaleCount = value.filter { it.isDigit() }, error = null)
|
||||||
|
.withDefaultMemberRelationForFamily()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onExpectedGuestCountChange(value: String) {
|
fun onExpectedGuestCountChange(value: String) {
|
||||||
@@ -292,3 +311,20 @@ class BookingCreateViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun BookingCreateState.withDefaultTransportModeForCheckIn(expectedCheckInAt: String): BookingCreateState {
|
||||||
|
if (!isTransportModeAuto) return this
|
||||||
|
val defaultMode = if (isFutureBookingCheckIn(expectedCheckInAt)) "" else "CAR"
|
||||||
|
if (transportMode == defaultMode) return this
|
||||||
|
return copy(transportMode = defaultMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BookingCreateState.withDefaultMemberRelationForFamily(): BookingCreateState {
|
||||||
|
if (memberRelation.isNotBlank()) return this
|
||||||
|
val child = childCount.toIntOrNull() ?: 0
|
||||||
|
val male = maleCount.toIntOrNull() ?: 0
|
||||||
|
val female = femaleCount.toIntOrNull() ?: 0
|
||||||
|
val shouldDefaultFamily = child >= 1 || (male >= 1 && female >= 1)
|
||||||
|
if (!shouldDefaultFamily) return this
|
||||||
|
return copy(memberRelation = "FAMILY")
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package com.android.trisolarispms.ui.booking
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Remove
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.android.trisolarispms.ui.common.BackTopBarScaffold
|
||||||
|
import com.android.trisolarispms.ui.common.PaddedScreenColumn
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BookingRoomRequestScreen(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
|
fromAt: String,
|
||||||
|
toAt: String,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onDone: () -> Unit,
|
||||||
|
viewModel: BookingRoomRequestViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(propertyId, fromAt, toAt) {
|
||||||
|
viewModel.load(propertyId, fromAt, toAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
BackTopBarScaffold(
|
||||||
|
title = "Select Room Types",
|
||||||
|
onBack = onBack,
|
||||||
|
bottomBar = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
val hasSelection = state.roomTypes.any { it.quantity > 0 }
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
viewModel.submit(propertyId, bookingId, fromAt, toAt, onDone)
|
||||||
|
},
|
||||||
|
enabled = hasSelection && !state.isSubmitting,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(if (state.isSubmitting) "Saving..." else "Proceed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
PaddedScreenColumn(padding = padding, contentPadding = 16.dp) {
|
||||||
|
if (state.isLoading) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
state.error?.let {
|
||||||
|
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
if (!state.isLoading) {
|
||||||
|
if (state.roomTypes.isEmpty()) {
|
||||||
|
Text(text = "No room types found")
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
items(state.roomTypes) { item ->
|
||||||
|
RoomTypeQuantityCard(
|
||||||
|
item = item,
|
||||||
|
onIncrease = { viewModel.increaseQuantity(item.roomTypeCode) },
|
||||||
|
onDecrease = { viewModel.decreaseQuantity(item.roomTypeCode) },
|
||||||
|
onRateChange = { viewModel.updateRate(item.roomTypeCode, it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RoomTypeQuantityCard(
|
||||||
|
item: BookingRoomTypeQuantityItem,
|
||||||
|
onIncrease: () -> Unit,
|
||||||
|
onDecrease: () -> Unit,
|
||||||
|
onRateChange: (String) -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(text = item.roomTypeName, style = MaterialTheme.typography.titleMedium)
|
||||||
|
Text(
|
||||||
|
text = "${item.roomTypeCode} • Available: ${item.maxQuantity}",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
IconButton(onClick = onDecrease, enabled = item.quantity > 0) {
|
||||||
|
Icon(Icons.Default.Remove, contentDescription = "Decrease")
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = item.quantity.toString(),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp)
|
||||||
|
)
|
||||||
|
IconButton(onClick = onIncrease, enabled = item.quantity < item.maxQuantity) {
|
||||||
|
Icon(Icons.Default.Add, contentDescription = "Increase")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutlinedTextField(
|
||||||
|
value = item.rateInput,
|
||||||
|
onValueChange = onRateChange,
|
||||||
|
label = { Text("Rate / night") },
|
||||||
|
placeholder = { Text("Enter rate") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
singleLine = true,
|
||||||
|
prefix = { item.currency?.takeIf { it.isNotBlank() }?.let { Text("$it ") } },
|
||||||
|
supportingText = {
|
||||||
|
item.ratePlanCode?.takeIf { it.isNotBlank() }?.let { Text("Plan: $it") }
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 10.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.android.trisolarispms.ui.booking
|
||||||
|
|
||||||
|
data class BookingRoomTypeQuantityItem(
|
||||||
|
val roomTypeCode: String,
|
||||||
|
val roomTypeName: String,
|
||||||
|
val maxQuantity: Int,
|
||||||
|
val quantity: Int = 0,
|
||||||
|
val rateInput: String = "",
|
||||||
|
val currency: String? = null,
|
||||||
|
val ratePlanCode: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BookingRoomRequestState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val isSubmitting: Boolean = false,
|
||||||
|
val error: String? = null,
|
||||||
|
val roomTypes: List<BookingRoomTypeQuantityItem> = emptyList()
|
||||||
|
)
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package com.android.trisolarispms.ui.booking
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.android.trisolarispms.data.api.core.ApiClient
|
||||||
|
import com.android.trisolarispms.data.api.model.BookingRoomRequestCreateRequest
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
class BookingRoomRequestViewModel : ViewModel() {
|
||||||
|
private val _state = MutableStateFlow(BookingRoomRequestState())
|
||||||
|
val state: StateFlow<BookingRoomRequestState> = _state
|
||||||
|
|
||||||
|
fun load(propertyId: String, fromAt: String, toAt: String) {
|
||||||
|
if (propertyId.isBlank()) return
|
||||||
|
val fromDate = fromAt.toDateOnly() ?: run {
|
||||||
|
_state.update { it.copy(error = "Invalid check-in date") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val toDate = toAt.toDateOnly() ?: run {
|
||||||
|
_state.update { it.copy(error = "Invalid check-out date") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isLoading = true, error = null) }
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val availabilityResponse = api.getRoomAvailabilityRange(propertyId, from = fromDate, to = toDate)
|
||||||
|
if (!availabilityResponse.isSuccessful) {
|
||||||
|
_state.update { it.copy(isLoading = false, error = "Load failed: ${availabilityResponse.code()}") }
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentByType = _state.value.roomTypes.associateBy { it.roomTypeCode }
|
||||||
|
val items = availabilityResponse.body().orEmpty()
|
||||||
|
.mapNotNull { entry ->
|
||||||
|
val maxQuantity = (entry.freeCount ?: entry.freeRoomNumbers.size).coerceAtLeast(0)
|
||||||
|
if (maxQuantity <= 0) return@mapNotNull null
|
||||||
|
val code = entry.roomTypeCode?.trim()?.takeIf { it.isNotBlank() }
|
||||||
|
?: entry.roomTypeName?.trim()?.takeIf { it.isNotBlank() }
|
||||||
|
?: return@mapNotNull null
|
||||||
|
val name = entry.roomTypeName?.trim()?.takeIf { it.isNotBlank() } ?: code
|
||||||
|
val previous = currentByType[code]
|
||||||
|
val defaultRateInput = entry.averageRate?.toLong()?.toString().orEmpty()
|
||||||
|
BookingRoomTypeQuantityItem(
|
||||||
|
roomTypeCode = code,
|
||||||
|
roomTypeName = name,
|
||||||
|
maxQuantity = maxQuantity,
|
||||||
|
quantity = previous?.quantity?.coerceAtMost(maxQuantity) ?: 0,
|
||||||
|
rateInput = previous?.rateInput ?: defaultRateInput,
|
||||||
|
currency = entry.currency,
|
||||||
|
ratePlanCode = entry.ratePlanCode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.sortedBy { it.roomTypeName }
|
||||||
|
|
||||||
|
_state.update { it.copy(isLoading = false, roomTypes = items, error = null) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun increaseQuantity(roomTypeCode: String) {
|
||||||
|
updateQuantity(roomTypeCode, delta = 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decreaseQuantity(roomTypeCode: String) {
|
||||||
|
updateQuantity(roomTypeCode, delta = -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateRate(roomTypeCode: String, value: String) {
|
||||||
|
val digitsOnly = value.filter { it.isDigit() }
|
||||||
|
_state.update { current ->
|
||||||
|
current.copy(
|
||||||
|
roomTypes = current.roomTypes.map { item ->
|
||||||
|
if (item.roomTypeCode == roomTypeCode) {
|
||||||
|
item.copy(rateInput = digitsOnly)
|
||||||
|
} else {
|
||||||
|
item
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun submit(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
|
fromAt: String,
|
||||||
|
toAt: String,
|
||||||
|
onDone: () -> Unit
|
||||||
|
) {
|
||||||
|
if (propertyId.isBlank() || bookingId.isBlank() || fromAt.isBlank() || toAt.isBlank()) {
|
||||||
|
_state.update { it.copy(error = "Booking dates are missing") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val selected = _state.value.roomTypes.filter { it.quantity > 0 }
|
||||||
|
if (selected.isEmpty()) {
|
||||||
|
_state.update { it.copy(error = "Select at least one room type") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isSubmitting = true, error = null) }
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
for (item in selected) {
|
||||||
|
val response = api.createRoomRequest(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
body = BookingRoomRequestCreateRequest(
|
||||||
|
roomTypeCode = item.roomTypeCode,
|
||||||
|
quantity = item.quantity,
|
||||||
|
fromAt = fromAt,
|
||||||
|
toAt = toAt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
_state.update { it.copy(isSubmitting = false, error = "Create failed: ${response.code()}") }
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_state.update { it.copy(isSubmitting = false, error = null) }
|
||||||
|
onDone()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update { it.copy(isSubmitting = false, error = e.localizedMessage ?: "Create failed") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateQuantity(roomTypeCode: String, delta: Int) {
|
||||||
|
_state.update { current ->
|
||||||
|
current.copy(
|
||||||
|
roomTypes = current.roomTypes.map { item ->
|
||||||
|
if (item.roomTypeCode != roomTypeCode) {
|
||||||
|
item
|
||||||
|
} else {
|
||||||
|
val updated = (item.quantity + delta).coerceIn(0, item.maxQuantity)
|
||||||
|
item.copy(quantity = updated)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.toDateOnly(): String? =
|
||||||
|
runCatching {
|
||||||
|
OffsetDateTime.parse(this).toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE)
|
||||||
|
}.getOrNull()
|
||||||
@@ -31,6 +31,13 @@ sealed interface AppRoute {
|
|||||||
val fromAt: String,
|
val fromAt: String,
|
||||||
val toAt: String?
|
val toAt: String?
|
||||||
) : AppRoute
|
) : AppRoute
|
||||||
|
data class BookingRoomRequestFromBooking(
|
||||||
|
val propertyId: String,
|
||||||
|
val bookingId: String,
|
||||||
|
val guestId: String,
|
||||||
|
val fromAt: String,
|
||||||
|
val toAt: String
|
||||||
|
) : AppRoute
|
||||||
data class BookingRoomStays(
|
data class BookingRoomStays(
|
||||||
val propertyId: String,
|
val propertyId: String,
|
||||||
val bookingId: String
|
val bookingId: String
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ internal fun handleBackNavigation(
|
|||||||
null
|
null
|
||||||
)
|
)
|
||||||
is AppRoute.CreateBooking -> refs.openActiveRoomStays(currentRoute.propertyId)
|
is AppRoute.CreateBooking -> refs.openActiveRoomStays(currentRoute.propertyId)
|
||||||
|
is AppRoute.BookingRoomRequestFromBooking -> refs.openActiveRoomStays(currentRoute.propertyId)
|
||||||
is AppRoute.GuestInfo -> refs.route.value = AppRoute.Home
|
is AppRoute.GuestInfo -> refs.route.value = AppRoute.Home
|
||||||
is AppRoute.GuestSignature -> refs.route.value = AppRoute.GuestInfo(
|
is AppRoute.GuestSignature -> refs.route.value = AppRoute.GuestInfo(
|
||||||
currentRoute.propertyId,
|
currentRoute.propertyId,
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package com.android.trisolarispms.ui.navigation
|
|||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import com.android.trisolarispms.core.auth.Role
|
import com.android.trisolarispms.core.auth.Role
|
||||||
|
import com.android.trisolarispms.core.booking.isFutureBookingCheckIn
|
||||||
import com.android.trisolarispms.ui.navigation.AppRoute
|
import com.android.trisolarispms.ui.navigation.AppRoute
|
||||||
import com.android.trisolarispms.ui.auth.AuthUiState
|
import com.android.trisolarispms.ui.auth.AuthUiState
|
||||||
import com.android.trisolarispms.ui.auth.AuthViewModel
|
import com.android.trisolarispms.ui.auth.AuthViewModel
|
||||||
import com.android.trisolarispms.ui.booking.BookingCreateScreen
|
import com.android.trisolarispms.ui.booking.BookingCreateScreen
|
||||||
|
import com.android.trisolarispms.ui.booking.BookingRoomRequestScreen
|
||||||
import com.android.trisolarispms.ui.guest.GuestInfoScreen
|
import com.android.trisolarispms.ui.guest.GuestInfoScreen
|
||||||
import com.android.trisolarispms.ui.guest.GuestSignatureScreen
|
import com.android.trisolarispms.ui.guest.GuestSignatureScreen
|
||||||
import com.android.trisolarispms.ui.home.HomeScreen
|
import com.android.trisolarispms.ui.home.HomeScreen
|
||||||
@@ -70,19 +72,52 @@ internal fun renderHomeGuestRoutes(
|
|||||||
val fromAt = response.checkInAt?.takeIf { it.isNotBlank() }
|
val fromAt = response.checkInAt?.takeIf { it.isNotBlank() }
|
||||||
?: response.expectedCheckInAt.orEmpty()
|
?: response.expectedCheckInAt.orEmpty()
|
||||||
val toAt = response.expectedCheckOutAt?.takeIf { it.isNotBlank() }
|
val toAt = response.expectedCheckOutAt?.takeIf { it.isNotBlank() }
|
||||||
refs.route.value = AppRoute.ManageRoomStaySelectFromBooking(
|
if (isFutureBookingCheckIn(response.expectedCheckInAt)) {
|
||||||
propertyId = currentRoute.propertyId,
|
if (fromAt.isNotBlank() && !toAt.isNullOrBlank()) {
|
||||||
bookingId = bookingId,
|
refs.route.value = AppRoute.BookingRoomRequestFromBooking(
|
||||||
guestId = guestId,
|
propertyId = currentRoute.propertyId,
|
||||||
fromAt = fromAt,
|
bookingId = bookingId,
|
||||||
toAt = toAt
|
guestId = guestId,
|
||||||
)
|
fromAt = fromAt,
|
||||||
|
toAt = toAt
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
refs.route.value = AppRoute.GuestInfo(
|
||||||
|
propertyId = currentRoute.propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
guestId = guestId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
refs.route.value = AppRoute.ManageRoomStaySelectFromBooking(
|
||||||
|
propertyId = currentRoute.propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
guestId = guestId,
|
||||||
|
fromAt = fromAt,
|
||||||
|
toAt = toAt
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
refs.route.value = AppRoute.Home
|
refs.route.value = AppRoute.Home
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is AppRoute.BookingRoomRequestFromBooking -> BookingRoomRequestScreen(
|
||||||
|
propertyId = currentRoute.propertyId,
|
||||||
|
bookingId = currentRoute.bookingId,
|
||||||
|
fromAt = currentRoute.fromAt,
|
||||||
|
toAt = currentRoute.toAt,
|
||||||
|
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) },
|
||||||
|
onDone = {
|
||||||
|
refs.route.value = AppRoute.GuestInfo(
|
||||||
|
propertyId = currentRoute.propertyId,
|
||||||
|
bookingId = currentRoute.bookingId,
|
||||||
|
guestId = currentRoute.guestId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
is AppRoute.GuestInfo -> GuestInfoScreen(
|
is AppRoute.GuestInfo -> GuestInfoScreen(
|
||||||
propertyId = currentRoute.propertyId,
|
propertyId = currentRoute.propertyId,
|
||||||
guestId = currentRoute.guestId,
|
guestId = currentRoute.guestId,
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.android.trisolarispms.data.api.model.RoomAvailableRateResponse
|
|
||||||
import com.android.trisolarispms.ui.common.BackTopBarScaffold
|
import com.android.trisolarispms.ui.common.BackTopBarScaffold
|
||||||
import com.android.trisolarispms.ui.common.LoadingAndError
|
import com.android.trisolarispms.ui.common.LoadingAndError
|
||||||
import com.android.trisolarispms.ui.common.PaddedScreenColumn
|
import com.android.trisolarispms.ui.common.PaddedScreenColumn
|
||||||
@@ -113,16 +112,15 @@ fun ManageRoomStaySelectScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
items(state.rooms) { room ->
|
items(state.rooms) { room ->
|
||||||
val selection = room.toSelection() ?: return@items
|
val isSelected = selectedRooms.any { it.roomId == room.roomId }
|
||||||
val isSelected = selectedRooms.any { it.roomId == selection.roomId }
|
|
||||||
RoomSelectCard(
|
RoomSelectCard(
|
||||||
item = selection,
|
item = room,
|
||||||
isSelected = isSelected,
|
isSelected = isSelected,
|
||||||
onToggle = {
|
onToggle = {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
selectedRooms.removeAll { it.roomId == selection.roomId }
|
selectedRooms.removeAll { it.roomId == room.roomId }
|
||||||
} else {
|
} else {
|
||||||
selectedRooms.add(selection)
|
selectedRooms.add(room)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -187,16 +185,3 @@ private fun String.toDateOnly(): String? {
|
|||||||
OffsetDateTime.parse(this).toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE)
|
OffsetDateTime.parse(this).toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE)
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun RoomAvailableRateResponse.toSelection(): ManageRoomStaySelection? {
|
|
||||||
val id = roomId ?: return null
|
|
||||||
val number = roomNumber ?: return null
|
|
||||||
return ManageRoomStaySelection(
|
|
||||||
roomId = id,
|
|
||||||
roomNumber = number,
|
|
||||||
roomTypeName = roomTypeName ?: roomTypeCode ?: "Room",
|
|
||||||
averageRate = averageRate,
|
|
||||||
currency = currency,
|
|
||||||
ratePlanCode = ratePlanCode
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package com.android.trisolarispms.ui.roomstay
|
package com.android.trisolarispms.ui.roomstay
|
||||||
|
|
||||||
import com.android.trisolarispms.data.api.model.RoomAvailableRateResponse
|
|
||||||
|
|
||||||
data class ManageRoomStaySelectState(
|
data class ManageRoomStaySelectState(
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
val rooms: List<RoomAvailableRateResponse> = emptyList()
|
val rooms: List<ManageRoomStaySelection> = emptyList()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package com.android.trisolarispms.ui.roomstay
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.android.trisolarispms.data.api.core.ApiClient
|
import com.android.trisolarispms.data.api.core.ApiClient
|
||||||
import com.android.trisolarispms.core.viewmodel.launchRequest
|
import com.android.trisolarispms.core.viewmodel.launchRequest
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
@@ -20,17 +23,57 @@ class ManageRoomStaySelectViewModel : ViewModel() {
|
|||||||
defaultError = "Load failed"
|
defaultError = "Load failed"
|
||||||
) {
|
) {
|
||||||
val api = ApiClient.create()
|
val api = ApiClient.create()
|
||||||
val response = api.listAvailableRoomsWithRate(propertyId, from = from, to = to)
|
val response = api.getRoomAvailabilityRange(propertyId, from = from, to = to)
|
||||||
if (response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
rooms = response.body().orEmpty(),
|
|
||||||
error = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
|
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
|
||||||
|
return@launchRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
val rooms = coroutineScope {
|
||||||
|
response.body().orEmpty()
|
||||||
|
.mapNotNull { entry ->
|
||||||
|
val roomTypeCode = entry.roomTypeCode?.trim()?.takeIf { it.isNotBlank() } ?: return@mapNotNull null
|
||||||
|
val hasFreeRooms = (entry.freeCount ?: entry.freeRoomNumbers.size) > 0
|
||||||
|
if (!hasFreeRooms) return@mapNotNull null
|
||||||
|
async {
|
||||||
|
val byTypeResponse = api.listRoomsByType(
|
||||||
|
propertyId = propertyId,
|
||||||
|
roomTypeCode = roomTypeCode,
|
||||||
|
availableOnly = true
|
||||||
|
)
|
||||||
|
if (!byTypeResponse.isSuccessful) {
|
||||||
|
throw IllegalStateException("Load failed: ${byTypeResponse.code()}")
|
||||||
|
}
|
||||||
|
byTypeResponse.body().orEmpty()
|
||||||
|
.mapNotNull { room ->
|
||||||
|
val roomId = room.id ?: return@mapNotNull null
|
||||||
|
val roomNumber = room.roomNumber ?: return@mapNotNull null
|
||||||
|
ManageRoomStaySelection(
|
||||||
|
roomId = roomId,
|
||||||
|
roomNumber = roomNumber,
|
||||||
|
roomTypeName = room.roomTypeName
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?: entry.roomTypeName
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?: roomTypeCode,
|
||||||
|
averageRate = entry.averageRate,
|
||||||
|
currency = entry.currency,
|
||||||
|
ratePlanCode = entry.ratePlanCode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.awaitAll()
|
||||||
|
.flatten()
|
||||||
|
.sortedBy { it.roomNumber }
|
||||||
|
}
|
||||||
|
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
rooms = rooms,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user