booking create: manage booking rates flow
This commit is contained in:
@@ -18,6 +18,9 @@ import com.android.trisolarispms.ui.auth.UnauthorizedScreen
|
||||
import com.android.trisolarispms.ui.booking.BookingCreateScreen
|
||||
import com.android.trisolarispms.ui.guest.GuestInfoScreen
|
||||
import com.android.trisolarispms.ui.guest.GuestSignatureScreen
|
||||
import com.android.trisolarispms.ui.roomstay.ManageRoomStayRatesScreen
|
||||
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelectScreen
|
||||
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelection
|
||||
import com.android.trisolarispms.ui.home.HomeScreen
|
||||
import com.android.trisolarispms.ui.property.AddPropertyScreen
|
||||
import com.android.trisolarispms.ui.room.RoomFormScreen
|
||||
@@ -65,6 +68,7 @@ class MainActivity : ComponentActivity() {
|
||||
val selectedGuest = remember { mutableStateOf<com.android.trisolarispms.data.api.model.GuestDto?>(null) }
|
||||
val selectedGuestPhone = remember { mutableStateOf<String?>(null) }
|
||||
val selectedImageTag = remember { mutableStateOf<com.android.trisolarispms.data.api.model.RoomImageTagDto?>(null) }
|
||||
val selectedManageRooms = remember { mutableStateOf<List<ManageRoomStaySelection>>(emptyList()) }
|
||||
val roomFormKey = remember { mutableStateOf(0) }
|
||||
val amenitiesReturnRoute = remember { mutableStateOf<AppRoute>(AppRoute.Home) }
|
||||
val currentRoute = route.value
|
||||
@@ -117,6 +121,27 @@ class MainActivity : ComponentActivity() {
|
||||
currentRoute.bookingId,
|
||||
currentRoute.guestId
|
||||
)
|
||||
is AppRoute.ManageRoomStaySelect -> route.value = AppRoute.ActiveRoomStays(
|
||||
currentRoute.propertyId,
|
||||
selectedPropertyName.value ?: "Property"
|
||||
)
|
||||
is AppRoute.ManageRoomStayRates -> route.value = AppRoute.ManageRoomStaySelect(
|
||||
currentRoute.propertyId,
|
||||
currentRoute.bookingId,
|
||||
currentRoute.fromAt,
|
||||
currentRoute.toAt
|
||||
)
|
||||
is AppRoute.ManageRoomStaySelectFromBooking -> route.value = AppRoute.ActiveRoomStays(
|
||||
currentRoute.propertyId,
|
||||
selectedPropertyName.value ?: "Property"
|
||||
)
|
||||
is AppRoute.ManageRoomStayRatesFromBooking -> route.value = AppRoute.ManageRoomStaySelectFromBooking(
|
||||
currentRoute.propertyId,
|
||||
currentRoute.bookingId,
|
||||
currentRoute.guestId,
|
||||
currentRoute.fromAt,
|
||||
currentRoute.toAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,8 +185,17 @@ class MainActivity : ComponentActivity() {
|
||||
val guestId = (guest?.id ?: response.guestId).orEmpty()
|
||||
selectedGuest.value = guest
|
||||
selectedGuestPhone.value = phone
|
||||
if (bookingId.isNotBlank()) {
|
||||
route.value = AppRoute.GuestInfo(currentRoute.propertyId, bookingId, guestId)
|
||||
if (bookingId.isNotBlank() && guestId.isNotBlank()) {
|
||||
val fromAt = response.checkInAt?.takeIf { it.isNotBlank() }
|
||||
?: response.expectedCheckInAt.orEmpty()
|
||||
val toAt = response.expectedCheckOutAt?.takeIf { it.isNotBlank() }
|
||||
route.value = AppRoute.ManageRoomStaySelectFromBooking(
|
||||
propertyId = currentRoute.propertyId,
|
||||
bookingId = bookingId,
|
||||
guestId = guestId,
|
||||
fromAt = fromAt,
|
||||
toAt = toAt
|
||||
)
|
||||
} else {
|
||||
route.value = AppRoute.Home
|
||||
}
|
||||
@@ -203,7 +237,106 @@ class MainActivity : ComponentActivity() {
|
||||
propertyName = currentRoute.propertyName,
|
||||
onBack = { route.value = AppRoute.Home },
|
||||
onViewRooms = { route.value = AppRoute.Rooms(currentRoute.propertyId) },
|
||||
onCreateBooking = { route.value = AppRoute.CreateBooking(currentRoute.propertyId) }
|
||||
onCreateBooking = { route.value = AppRoute.CreateBooking(currentRoute.propertyId) },
|
||||
onManageRoomStay = { booking ->
|
||||
val fromAt = booking.checkInAt?.takeIf { it.isNotBlank() }
|
||||
?: booking.expectedCheckInAt.orEmpty()
|
||||
val toAt = booking.expectedCheckOutAt?.takeIf { it.isNotBlank() }
|
||||
?: booking.checkOutAt?.takeIf { it.isNotBlank() }
|
||||
if (fromAt.isNotBlank()) {
|
||||
route.value = AppRoute.ManageRoomStaySelect(
|
||||
propertyId = currentRoute.propertyId,
|
||||
bookingId = booking.id.orEmpty(),
|
||||
fromAt = fromAt,
|
||||
toAt = toAt
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
is AppRoute.ManageRoomStaySelect -> ManageRoomStaySelectScreen(
|
||||
propertyId = currentRoute.propertyId,
|
||||
bookingFromAt = currentRoute.fromAt,
|
||||
bookingToAt = currentRoute.toAt,
|
||||
onBack = {
|
||||
route.value = AppRoute.ActiveRoomStays(
|
||||
currentRoute.propertyId,
|
||||
selectedPropertyName.value ?: "Property"
|
||||
)
|
||||
},
|
||||
onNext = { rooms ->
|
||||
selectedManageRooms.value = rooms
|
||||
route.value = AppRoute.ManageRoomStayRates(
|
||||
propertyId = currentRoute.propertyId,
|
||||
bookingId = currentRoute.bookingId,
|
||||
fromAt = currentRoute.fromAt,
|
||||
toAt = currentRoute.toAt
|
||||
)
|
||||
}
|
||||
)
|
||||
is AppRoute.ManageRoomStaySelectFromBooking -> ManageRoomStaySelectScreen(
|
||||
propertyId = currentRoute.propertyId,
|
||||
bookingFromAt = currentRoute.fromAt,
|
||||
bookingToAt = currentRoute.toAt,
|
||||
onBack = {
|
||||
route.value = AppRoute.ActiveRoomStays(
|
||||
currentRoute.propertyId,
|
||||
selectedPropertyName.value ?: "Property"
|
||||
)
|
||||
},
|
||||
onNext = { rooms ->
|
||||
selectedManageRooms.value = rooms
|
||||
route.value = AppRoute.ManageRoomStayRatesFromBooking(
|
||||
propertyId = currentRoute.propertyId,
|
||||
bookingId = currentRoute.bookingId,
|
||||
guestId = currentRoute.guestId,
|
||||
fromAt = currentRoute.fromAt,
|
||||
toAt = currentRoute.toAt
|
||||
)
|
||||
}
|
||||
)
|
||||
is AppRoute.ManageRoomStayRates -> ManageRoomStayRatesScreen(
|
||||
propertyId = currentRoute.propertyId,
|
||||
bookingId = currentRoute.bookingId,
|
||||
checkInAt = currentRoute.fromAt,
|
||||
checkOutAt = currentRoute.toAt,
|
||||
selectedRooms = selectedManageRooms.value,
|
||||
onBack = {
|
||||
route.value = AppRoute.ManageRoomStaySelect(
|
||||
currentRoute.propertyId,
|
||||
currentRoute.bookingId,
|
||||
currentRoute.fromAt,
|
||||
currentRoute.toAt
|
||||
)
|
||||
},
|
||||
onDone = {
|
||||
route.value = AppRoute.ActiveRoomStays(
|
||||
currentRoute.propertyId,
|
||||
selectedPropertyName.value ?: "Property"
|
||||
)
|
||||
}
|
||||
)
|
||||
is AppRoute.ManageRoomStayRatesFromBooking -> ManageRoomStayRatesScreen(
|
||||
propertyId = currentRoute.propertyId,
|
||||
bookingId = currentRoute.bookingId,
|
||||
checkInAt = currentRoute.fromAt,
|
||||
checkOutAt = currentRoute.toAt,
|
||||
selectedRooms = selectedManageRooms.value,
|
||||
onBack = {
|
||||
route.value = AppRoute.ManageRoomStaySelectFromBooking(
|
||||
currentRoute.propertyId,
|
||||
currentRoute.bookingId,
|
||||
currentRoute.guestId,
|
||||
currentRoute.fromAt,
|
||||
currentRoute.toAt
|
||||
)
|
||||
},
|
||||
onDone = {
|
||||
route.value = AppRoute.GuestInfo(
|
||||
currentRoute.propertyId,
|
||||
currentRoute.bookingId,
|
||||
currentRoute.guestId
|
||||
)
|
||||
}
|
||||
)
|
||||
is AppRoute.Rooms -> RoomsScreen(
|
||||
propertyId = currentRoute.propertyId,
|
||||
|
||||
@@ -9,11 +9,15 @@ import com.android.trisolarispms.data.api.model.BookingCreateResponse
|
||||
import com.android.trisolarispms.data.api.model.BookingLinkGuestRequest
|
||||
import com.android.trisolarispms.data.api.model.BookingNoShowRequest
|
||||
import com.android.trisolarispms.data.api.model.BookingRoomStayCreateRequest
|
||||
import com.android.trisolarispms.data.api.model.BookingListItem
|
||||
import com.android.trisolarispms.data.api.model.BookingBulkCheckInRequest
|
||||
import com.android.trisolarispms.data.api.model.RoomStayDto
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface BookingApi {
|
||||
@POST("properties/{propertyId}/bookings")
|
||||
@@ -22,6 +26,19 @@ interface BookingApi {
|
||||
@Body body: BookingCreateRequest
|
||||
): Response<BookingCreateResponse>
|
||||
|
||||
@GET("properties/{propertyId}/bookings")
|
||||
suspend fun listBookings(
|
||||
@Path("propertyId") propertyId: String,
|
||||
@Query("status") status: String? = null
|
||||
): Response<List<BookingListItem>>
|
||||
|
||||
@POST("properties/{propertyId}/bookings/{bookingId}/check-in/bulk")
|
||||
suspend fun bulkCheckIn(
|
||||
@Path("propertyId") propertyId: String,
|
||||
@Path("bookingId") bookingId: String,
|
||||
@Body body: BookingBulkCheckInRequest
|
||||
): Response<Unit>
|
||||
|
||||
@POST("properties/{propertyId}/bookings/{bookingId}/link-guest")
|
||||
suspend fun linkGuest(
|
||||
@Path("propertyId") propertyId: String,
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.android.trisolarispms.data.api
|
||||
|
||||
import com.android.trisolarispms.data.api.model.RoomAvailabilityRangeResponse
|
||||
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.RoomCreateRequest
|
||||
import com.android.trisolarispms.data.api.model.RoomDto
|
||||
@@ -62,6 +63,14 @@ interface RoomApi {
|
||||
@Path("propertyId") propertyId: String
|
||||
): 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}")
|
||||
suspend fun listRoomsByType(
|
||||
@Path("propertyId") propertyId: String,
|
||||
|
||||
@@ -30,6 +30,38 @@ data class BookingCreateResponse(
|
||||
val expectedCheckOutAt: String? = null
|
||||
)
|
||||
|
||||
data class BookingListItem(
|
||||
val id: String? = null,
|
||||
val status: String? = null,
|
||||
val guestId: String? = null,
|
||||
val source: String? = null,
|
||||
val checkInAt: String? = null,
|
||||
val checkOutAt: String? = null,
|
||||
val expectedCheckInAt: String? = null,
|
||||
val expectedCheckOutAt: String? = null,
|
||||
val adultCount: Int? = null,
|
||||
val childCount: Int? = null,
|
||||
val maleCount: Int? = null,
|
||||
val femaleCount: Int? = null,
|
||||
val totalGuestCount: Int? = null,
|
||||
val expectedGuestCount: Int? = null,
|
||||
val notes: String? = null
|
||||
)
|
||||
|
||||
data class BookingBulkCheckInRequest(
|
||||
val stays: List<BookingBulkCheckInStayRequest>
|
||||
)
|
||||
|
||||
data class BookingBulkCheckInStayRequest(
|
||||
val roomId: String,
|
||||
val checkInAt: String,
|
||||
val checkOutAt: String? = null,
|
||||
val nightlyRate: Long? = null,
|
||||
val rateSource: String? = null,
|
||||
val ratePlanCode: String? = null,
|
||||
val currency: String? = null
|
||||
)
|
||||
|
||||
data class BookingLinkGuestRequest(
|
||||
val guestId: String
|
||||
)
|
||||
|
||||
@@ -52,6 +52,16 @@ data class RoomAvailabilityRangeResponse(
|
||||
val freeCount: Int? = null
|
||||
)
|
||||
|
||||
data class RoomAvailableRateResponse(
|
||||
val roomId: String? = null,
|
||||
val roomNumber: Int? = null,
|
||||
val roomTypeCode: String? = null,
|
||||
val roomTypeName: String? = null,
|
||||
val averageRate: Double? = null,
|
||||
val currency: String? = null,
|
||||
val ratePlanCode: String? = null
|
||||
)
|
||||
|
||||
// Images
|
||||
|
||||
data class ImageDto(
|
||||
|
||||
@@ -5,6 +5,32 @@ sealed interface AppRoute {
|
||||
data class CreateBooking(val propertyId: String) : AppRoute
|
||||
data class GuestInfo(val propertyId: String, val bookingId: String, val guestId: String) : AppRoute
|
||||
data class GuestSignature(val propertyId: String, val bookingId: String, val guestId: String) : AppRoute
|
||||
data class ManageRoomStaySelect(
|
||||
val propertyId: String,
|
||||
val bookingId: String,
|
||||
val fromAt: String,
|
||||
val toAt: String?
|
||||
) : AppRoute
|
||||
data class ManageRoomStayRates(
|
||||
val propertyId: String,
|
||||
val bookingId: String,
|
||||
val fromAt: String,
|
||||
val toAt: String?
|
||||
) : AppRoute
|
||||
data class ManageRoomStaySelectFromBooking(
|
||||
val propertyId: String,
|
||||
val bookingId: String,
|
||||
val guestId: String,
|
||||
val fromAt: String,
|
||||
val toAt: String?
|
||||
) : AppRoute
|
||||
data class ManageRoomStayRatesFromBooking(
|
||||
val propertyId: String,
|
||||
val bookingId: String,
|
||||
val guestId: String,
|
||||
val fromAt: String,
|
||||
val toAt: String?
|
||||
) : AppRoute
|
||||
data object AddProperty : AppRoute
|
||||
data class ActiveRoomStays(val propertyId: String, val propertyName: String) : AppRoute
|
||||
data class Rooms(val propertyId: String) : AppRoute
|
||||
|
||||
@@ -7,10 +7,17 @@ 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.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.MeetingRoom
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -19,15 +26,19 @@ import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.android.trisolarispms.data.api.model.BookingListItem
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -37,9 +48,11 @@ fun ActiveRoomStaysScreen(
|
||||
onBack: () -> Unit,
|
||||
onViewRooms: () -> Unit,
|
||||
onCreateBooking: () -> Unit,
|
||||
onManageRoomStay: (BookingListItem) -> Unit,
|
||||
viewModel: ActiveRoomStaysViewModel = viewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val selectedBooking = remember { mutableStateOf<BookingListItem?>(null) }
|
||||
|
||||
LaunchedEffect(propertyId) {
|
||||
viewModel.load(propertyId)
|
||||
@@ -88,6 +101,25 @@ fun ActiveRoomStaysScreen(
|
||||
}
|
||||
|
||||
if (!state.isLoading && state.error == null) {
|
||||
if (state.checkedInBookings.isNotEmpty()) {
|
||||
Text(text = "Checked-in bookings", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(state.checkedInBookings) { booking ->
|
||||
CheckedInBookingCard(
|
||||
booking = booking,
|
||||
onClick = { selectedBooking.value = booking }
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
if (state.items.isEmpty()) {
|
||||
Text(text = "No active room stays")
|
||||
} else {
|
||||
@@ -108,4 +140,70 @@ fun ActiveRoomStaysScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectedBooking.value?.let { booking ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { selectedBooking.value = null },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
selectedBooking.value = null
|
||||
onManageRoomStay(booking)
|
||||
}
|
||||
) {
|
||||
Text("Manage room stay")
|
||||
}
|
||||
TextButton(onClick = { selectedBooking.value = null }) {
|
||||
Text("Balance")
|
||||
}
|
||||
TextButton(onClick = { selectedBooking.value = null }) {
|
||||
Text("Add photos")
|
||||
}
|
||||
TextButton(onClick = { selectedBooking.value = null }) {
|
||||
Text("Checkout")
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {},
|
||||
dismissButton = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CheckedInBookingCard(
|
||||
booking: BookingListItem,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||
modifier = Modifier.clickable(onClick = onClick)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Text(
|
||||
text = booking.id?.take(8)?.let { "Booking #$it" } ?: "Booking",
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
val source = booking.source?.takeIf { it.isNotBlank() }
|
||||
if (source != null) {
|
||||
Text(text = source, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
val expectedCount = booking.expectedGuestCount
|
||||
val totalCount = booking.totalGuestCount
|
||||
val countLine = when {
|
||||
expectedCount != null -> "Expected guests: $expectedCount"
|
||||
totalCount != null -> "Guests: $totalCount"
|
||||
else -> null
|
||||
}
|
||||
if (countLine != null) {
|
||||
Text(text = countLine, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
val notes = booking.notes?.takeIf { it.isNotBlank() }
|
||||
if (notes != null) {
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Text(text = notes, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.android.trisolarispms.ui.roomstay
|
||||
|
||||
import com.android.trisolarispms.data.api.model.ActiveRoomStayDto
|
||||
import com.android.trisolarispms.data.api.model.BookingListItem
|
||||
|
||||
data class ActiveRoomStaysState(
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val items: List<ActiveRoomStayDto> = emptyList()
|
||||
val items: List<ActiveRoomStayDto> = emptyList(),
|
||||
val checkedInBookings: List<BookingListItem> = emptyList()
|
||||
)
|
||||
|
||||
@@ -18,17 +18,19 @@ class ActiveRoomStaysViewModel : ViewModel() {
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
try {
|
||||
val api = ApiClient.create()
|
||||
val response = api.listActiveRoomStays(propertyId)
|
||||
if (response.isSuccessful) {
|
||||
val activeResponse = api.listActiveRoomStays(propertyId)
|
||||
val bookingsResponse = api.listBookings(propertyId, status = "CHECKED_IN")
|
||||
if (activeResponse.isSuccessful) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
items = response.body().orEmpty(),
|
||||
items = activeResponse.body().orEmpty(),
|
||||
checkedInBookings = bookingsResponse.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: ${activeResponse.code()}") }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") }
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.android.trisolarispms.ui.roomstay
|
||||
|
||||
data class ManageRoomStaySelection(
|
||||
val roomId: String,
|
||||
val roomNumber: Int,
|
||||
val roomTypeName: String,
|
||||
val averageRate: Double?,
|
||||
val currency: String?,
|
||||
val ratePlanCode: String?
|
||||
)
|
||||
|
||||
data class ManageRoomStayRateItem(
|
||||
val roomId: String,
|
||||
val roomNumber: Int,
|
||||
val roomTypeName: String,
|
||||
val nightlyRate: Long,
|
||||
val currency: String?,
|
||||
val ratePlanCode: String?
|
||||
)
|
||||
@@ -0,0 +1,172 @@
|
||||
package com.android.trisolarispms.ui.roomstay
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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 androidx.compose.foundation.text.KeyboardOptions
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun ManageRoomStayRatesScreen(
|
||||
propertyId: String,
|
||||
bookingId: String,
|
||||
checkInAt: String,
|
||||
checkOutAt: String?,
|
||||
selectedRooms: List<ManageRoomStaySelection>,
|
||||
onBack: () -> Unit,
|
||||
onDone: () -> Unit,
|
||||
viewModel: ManageRoomStayRatesViewModel = viewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val showTotalDialog = remember { mutableStateOf(false) }
|
||||
val totalInput = remember { mutableStateOf("") }
|
||||
|
||||
LaunchedEffect(selectedRooms) {
|
||||
viewModel.setItems(selectedRooms)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Selected Rooms") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors()
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val total = state.items.sumOf { it.nightlyRate }
|
||||
Text(
|
||||
text = "${total}₹",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.clickable {
|
||||
totalInput.value = total.takeIf { it > 0 }?.toString().orEmpty()
|
||||
showTotalDialog.value = true
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.submit(propertyId, bookingId, checkInAt, checkOutAt, onDone)
|
||||
},
|
||||
enabled = state.items.isNotEmpty() && !state.isLoading,
|
||||
modifier = Modifier.fillMaxWidth(0.5f)
|
||||
) {
|
||||
if (state.isLoading) {
|
||||
CircularProgressIndicator(modifier = Modifier.height(16.dp), strokeWidth = 2.dp)
|
||||
} else {
|
||||
Text("Update")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
if (state.items.isEmpty()) {
|
||||
Text(text = "No rooms selected.")
|
||||
return@Column
|
||||
}
|
||||
state.items.forEach { item ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = "${item.roomNumber}", style = MaterialTheme.typography.titleMedium)
|
||||
OutlinedTextField(
|
||||
value = item.nightlyRate.toString(),
|
||||
onValueChange = { viewModel.updateRate(item.roomId, it) },
|
||||
label = { Text("Rate") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth(0.5f)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
state.error?.let {
|
||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showTotalDialog.value) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showTotalDialog.value = false },
|
||||
title = { Text("Amount") },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = totalInput.value,
|
||||
onValueChange = { totalInput.value = it.filter { ch -> ch.isDigit() } },
|
||||
label = { Text("Amount") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val total = totalInput.value.toLongOrNull()
|
||||
if (total != null) {
|
||||
viewModel.applyTotal(total)
|
||||
}
|
||||
showTotalDialog.value = false
|
||||
}
|
||||
) {
|
||||
Text("Update")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showTotalDialog.value = false }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.android.trisolarispms.ui.roomstay
|
||||
|
||||
data class ManageRoomStayRatesState(
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val items: List<ManageRoomStayRateItem> = emptyList(),
|
||||
val lastTotal: Long? = null
|
||||
)
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.android.trisolarispms.ui.roomstay
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.android.trisolarispms.data.api.ApiClient
|
||||
import com.android.trisolarispms.data.api.model.BookingBulkCheckInRequest
|
||||
import com.android.trisolarispms.data.api.model.BookingBulkCheckInStayRequest
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ManageRoomStayRatesViewModel : ViewModel() {
|
||||
private val _state = MutableStateFlow(ManageRoomStayRatesState())
|
||||
val state: StateFlow<ManageRoomStayRatesState> = _state
|
||||
|
||||
fun setItems(items: List<ManageRoomStaySelection>) {
|
||||
val mapped = items.map { item ->
|
||||
ManageRoomStayRateItem(
|
||||
roomId = item.roomId,
|
||||
roomNumber = item.roomNumber,
|
||||
roomTypeName = item.roomTypeName,
|
||||
nightlyRate = item.averageRate?.toLong() ?: 0L,
|
||||
currency = item.currency,
|
||||
ratePlanCode = item.ratePlanCode
|
||||
)
|
||||
}
|
||||
_state.update { it.copy(items = mapped, error = null) }
|
||||
}
|
||||
|
||||
fun updateRate(roomId: String, value: String) {
|
||||
val rate = value.filter { it.isDigit() }.toLongOrNull() ?: 0L
|
||||
_state.update { current ->
|
||||
val updated = current.items.map {
|
||||
if (it.roomId == roomId) it.copy(nightlyRate = rate) else it
|
||||
}
|
||||
current.copy(items = updated)
|
||||
}
|
||||
}
|
||||
|
||||
fun applyTotal(total: Long) {
|
||||
if (total <= 0) return
|
||||
_state.update { current ->
|
||||
val items = current.items
|
||||
if (items.isEmpty()) return@update current
|
||||
val currentTotal = items.sumOf { it.nightlyRate }
|
||||
val updated = if (currentTotal <= 0L) {
|
||||
val base = total / items.size
|
||||
val remainder = total - base * items.size
|
||||
items.mapIndexed { index, item ->
|
||||
val extra = if (index == items.lastIndex) remainder else 0L
|
||||
item.copy(nightlyRate = base + extra)
|
||||
}
|
||||
} else {
|
||||
var remaining = total
|
||||
items.mapIndexed { index, item ->
|
||||
val share = if (index == items.lastIndex) {
|
||||
remaining
|
||||
} else {
|
||||
val portion = (item.nightlyRate.toDouble() / currentTotal.toDouble()) * total.toDouble()
|
||||
val rounded = portion.toLong()
|
||||
remaining -= rounded
|
||||
rounded
|
||||
}
|
||||
item.copy(nightlyRate = share)
|
||||
}
|
||||
}
|
||||
current.copy(items = updated, lastTotal = total)
|
||||
}
|
||||
}
|
||||
|
||||
fun submit(
|
||||
propertyId: String,
|
||||
bookingId: String,
|
||||
checkInAt: String,
|
||||
checkOutAt: String?,
|
||||
onDone: () -> Unit
|
||||
) {
|
||||
if (propertyId.isBlank() || bookingId.isBlank() || checkInAt.isBlank()) return
|
||||
val items = _state.value.items
|
||||
if (items.isEmpty()) return
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
try {
|
||||
val api = ApiClient.create()
|
||||
val stays = items.map { item ->
|
||||
BookingBulkCheckInStayRequest(
|
||||
roomId = item.roomId,
|
||||
checkInAt = checkInAt,
|
||||
checkOutAt = checkOutAt,
|
||||
nightlyRate = item.nightlyRate,
|
||||
rateSource = "MANUAL",
|
||||
ratePlanCode = item.ratePlanCode,
|
||||
currency = item.currency
|
||||
)
|
||||
}
|
||||
val response = api.bulkCheckIn(
|
||||
propertyId = propertyId,
|
||||
bookingId = bookingId,
|
||||
body = BookingBulkCheckInRequest(stays = stays)
|
||||
)
|
||||
if (response.isSuccessful) {
|
||||
_state.update { it.copy(isLoading = false, error = null) }
|
||||
onDone()
|
||||
} else {
|
||||
_state.update { it.copy(isLoading = false, error = "Update failed: ${response.code()}") }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Update failed") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package com.android.trisolarispms.ui.roomstay
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
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.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.android.trisolarispms.data.api.model.RoomAvailableRateResponse
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun ManageRoomStaySelectScreen(
|
||||
propertyId: String,
|
||||
bookingFromAt: String,
|
||||
bookingToAt: String?,
|
||||
onBack: () -> Unit,
|
||||
onNext: (List<ManageRoomStaySelection>) -> Unit,
|
||||
viewModel: ManageRoomStaySelectViewModel = viewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val selectedRooms = remember { mutableStateListOf<ManageRoomStaySelection>() }
|
||||
val fromDate = remember(bookingFromAt) { bookingFromAt.toDateOnly() }
|
||||
val toDate = remember(bookingToAt) { bookingToAt?.toDateOnly() }
|
||||
val fallbackToDate = remember(fromDate, toDate) {
|
||||
if (toDate != null || fromDate == null) {
|
||||
toDate
|
||||
} else {
|
||||
runCatching {
|
||||
java.time.LocalDate.parse(fromDate)
|
||||
.plusDays(1)
|
||||
.format(DateTimeFormatter.ISO_LOCAL_DATE)
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(propertyId, fromDate, fallbackToDate) {
|
||||
if (fromDate != null && fallbackToDate != null) {
|
||||
viewModel.load(propertyId, from = fromDate, to = fallbackToDate)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Select Rooms") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors()
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
if (selectedRooms.isNotEmpty()) {
|
||||
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
items(selectedRooms) { item ->
|
||||
RoomChip(text = item.roomNumber.toString())
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
Button(
|
||||
onClick = { onNext(selectedRooms.toList()) },
|
||||
enabled = selectedRooms.isNotEmpty(),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Proceed")
|
||||
}
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(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 (fromDate == null || fallbackToDate == null) {
|
||||
Text(text = "Booking dates not available.", color = MaterialTheme.colorScheme.error)
|
||||
return@Column
|
||||
}
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(state.rooms) { room ->
|
||||
val selection = room.toSelection() ?: return@items
|
||||
val isSelected = selectedRooms.any { it.roomId == selection.roomId }
|
||||
RoomSelectCard(
|
||||
item = selection,
|
||||
isSelected = isSelected,
|
||||
onToggle = {
|
||||
if (isSelected) {
|
||||
selectedRooms.removeAll { it.roomId == selection.roomId }
|
||||
} else {
|
||||
selectedRooms.add(selection)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomSelectCard(
|
||||
item: ManageRoomStaySelection,
|
||||
isSelected: Boolean,
|
||||
onToggle: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
)
|
||||
.background(
|
||||
color = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.08f) else Color.Transparent,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
)
|
||||
.clickable(onClick = onToggle)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = item.roomNumber.toString(),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Text(
|
||||
text = item.roomTypeName,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = item.averageRate?.toLong()?.let { "${item.currency ?: ""} $it" } ?: "--",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomChip(text: String) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.border(1.dp, MaterialTheme.colorScheme.outline, MaterialTheme.shapes.large)
|
||||
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = text, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toDateOnly(): String? {
|
||||
return runCatching {
|
||||
OffsetDateTime.parse(this).toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE)
|
||||
}.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
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.android.trisolarispms.ui.roomstay
|
||||
|
||||
import com.android.trisolarispms.data.api.model.RoomAvailableRateResponse
|
||||
|
||||
data class ManageRoomStaySelectState(
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val rooms: List<RoomAvailableRateResponse> = emptyList()
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.android.trisolarispms.ui.roomstay
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.android.trisolarispms.data.api.ApiClient
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ManageRoomStaySelectViewModel : ViewModel() {
|
||||
private val _state = MutableStateFlow(ManageRoomStaySelectState())
|
||||
val state: StateFlow<ManageRoomStaySelectState> = _state
|
||||
|
||||
fun load(propertyId: String, from: String, to: String) {
|
||||
if (propertyId.isBlank() || from.isBlank() || to.isBlank()) return
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
try {
|
||||
val api = ApiClient.create()
|
||||
val response = api.listAvailableRoomsWithRate(propertyId, from = from, to = to)
|
||||
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()}") }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user