From 5f522ca3abd15c0772460cfa0f49f4f1e7b8c3ec Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Thu, 29 Jan 2026 12:09:43 +0530 Subject: [PATCH] booking create: manage booking rates flow --- AGENTS.md | 121 +++++++++- .../com/android/trisolarispms/MainActivity.kt | 139 ++++++++++- .../trisolarispms/data/api/BookingApi.kt | 17 ++ .../android/trisolarispms/data/api/RoomApi.kt | 9 + .../data/api/model/BookingModels.kt | 32 +++ .../data/api/model/RoomModels.kt | 10 + .../com/android/trisolarispms/ui/AppRoute.kt | 26 ++ .../ui/roomstay/ActiveRoomStaysScreen.kt | 98 ++++++++ .../ui/roomstay/ActiveRoomStaysState.kt | 4 +- .../ui/roomstay/ActiveRoomStaysViewModel.kt | 10 +- .../ui/roomstay/ManageRoomStayModels.kt | 19 ++ .../ui/roomstay/ManageRoomStayRatesScreen.kt | 172 +++++++++++++ .../ui/roomstay/ManageRoomStayRatesState.kt | 8 + .../roomstay/ManageRoomStayRatesViewModel.kt | 113 +++++++++ .../ui/roomstay/ManageRoomStaySelectScreen.kt | 226 ++++++++++++++++++ .../ui/roomstay/ManageRoomStaySelectState.kt | 9 + .../roomstay/ManageRoomStaySelectViewModel.kt | 38 +++ 17 files changed, 1033 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayModels.kt create mode 100644 app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayRatesScreen.kt create mode 100644 app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayRatesState.kt create mode 100644 app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayRatesViewModel.kt create mode 100644 app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStaySelectScreen.kt create mode 100644 app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStaySelectState.kt create mode 100644 app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStaySelectViewModel.kt diff --git a/AGENTS.md b/AGENTS.md index 2d7fa8c..cd93be8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,7 +63,7 @@ Response: List of BookingListItem with id, status, guestId, source, times, count ### Check-in (creates RoomStay) POST /properties/{propertyId}/bookings/{bookingId}/check-in -Auth: ADMIN/MANAGER/STAFF +Auth: ADMIN/MANAGER Body Required: @@ -75,7 +75,7 @@ Optional: - checkInAt (String) - transportMode (String enum) - nightlyRate (Long) -- rateSource (PRESET|NEGOTIATED|OTA) +- rateSource (MANUAL|RATE_PLAN|OTA) - ratePlanCode (String) - currency (String) - notes (String) @@ -84,8 +84,8 @@ Optional: "roomIds": ["uuid1","uuid2"], "checkInAt": "2026-01-28T12:00:00+05:30", "nightlyRate": 2500, - "rateSource": "NEGOTIATED", - "ratePlanCode": "WEEKEND", + "rateSource": "MANUAL", + "ratePlanCode": "EP", "currency": "INR", "notes": "Late arrival" } @@ -95,7 +95,7 @@ Optional: ### Pre-assign room stay POST /properties/{propertyId}/bookings/{bookingId}/room-stays -Auth: ADMIN/MANAGER/STAFF +Auth: ADMIN/MANAGER Body Required: @@ -107,7 +107,7 @@ Required: Optional: - nightlyRate (Long) -- rateSource (PRESET|NEGOTIATED|OTA) +- rateSource (MANUAL|RATE_PLAN|OTA) - ratePlanCode (String) - currency (String) - notes (String) @@ -117,13 +117,63 @@ Optional: "fromAt": "2026-01-29T12:00:00+05:30", "toAt": "2026-01-30T10:00:00+05:30", "nightlyRate": 2800, - "rateSource": "PRESET", - "ratePlanCode": "WEEKEND", + "rateSource": "RATE_PLAN", + "ratePlanCode": "EP", "currency": "INR" } --- +### Active room stays + +GET /properties/{propertyId}/room-stays/active +Auth: any member except AGENT-only + +Response: list of ActiveRoomStayResponse + +[ + { + "roomStayId":"uuid", + "bookingId":"uuid", + "guestId":"uuid-or-null", + "guestName":"Name", + "guestPhone":"+9111...", + "roomId":"uuid", + "roomNumber":"101", + "roomTypeName":"DELUXE", + "fromAt":"2026-01-29T12:00:00+05:30", + "checkinAt":"2026-01-29T12:05:00+05:30", + "expectedCheckoutAt":"2026-01-30T10:00:00+05:30" + } +] + +--- + +### Change room (move guest) + +POST /properties/{propertyId}/room-stays/{roomStayId}/change-room +Auth: ADMIN/MANAGER/STAFF + +Body + +{ + "newRoomId":"uuid", + "movedAt":"2026-01-30T15:00:00+05:30", + "idempotencyKey":"any-unique-string" +} + +Response + +{ + "oldRoomStayId":"uuid", + "newRoomStayId":"uuid", + "oldRoomId":"uuid", + "newRoomId":"uuid", + "movedAt":"2026-01-30T15:00:00+05:30" +} + +--- + ## 2) Guests ### Create guest + link to booking @@ -310,7 +360,7 @@ Required: - effectiveAt (String, ISO-8601) - nightlyRate (Long) -- rateSource (PRESET|NEGOTIATED|OTA) +- rateSource (MANUAL|RATE_PLAN|OTA) Optional: @@ -320,7 +370,7 @@ Optional: { "effectiveAt": "2026-01-30T12:00:00+05:30", "nightlyRate": 2000, - "rateSource": "NEGOTIATED", + "rateSource": "MANUAL", "currency": "INR" } @@ -330,6 +380,57 @@ Response --- +### Check-out (closes all active stays on booking) + +POST /properties/{propertyId}/bookings/{bookingId}/check-out +Auth: ADMIN/MANAGER + +Body + +{ "checkOutAt":"2026-01-30T10:00:00+05:30", "notes":"optional" } + +Response: 204 No Content + +--- + +### Bulk check-in (creates multiple room stays) + +POST /properties/{propertyId}/bookings/{bookingId}/check-in/bulk + +Body: + +{ + "stays": [ + { + "roomId": "uuid", + "checkInAt": "2026-01-29T12:00:00+05:30", + "checkOutAt": "2026-01-30T10:00:00+05:30", + "nightlyRate": 6000, + "rateSource": "MANUAL", + "ratePlanCode": "EP", + "currency": "INR" + }, + { + "roomId": "uuid", + "checkInAt": "2026-01-29T12:00:00+05:30", + "checkOutAt": "2026-01-30T10:00:00+05:30", + "nightlyRate": 8000, + "rateSource": "MANUAL", + "ratePlanCode": "EP", + "currency": "INR" + } + ] +} + +Behavior + +- Creates one RoomStay per stay with its own rate. +- Sets booking CHECKED_IN, checkinAt = earliest stay check-in. +- If any checkOutAt provided, booking expectedCheckoutAt = latest of those. +- Rejects duplicate room IDs. +- Rejects invalid stay date range (checkOutAt <= checkInAt). +- Blocks occupied rooms. + ## 6) Payments + Balance ### Add payment diff --git a/app/src/main/java/com/android/trisolarispms/MainActivity.kt b/app/src/main/java/com/android/trisolarispms/MainActivity.kt index c36c113..d0ddb90 100644 --- a/app/src/main/java/com/android/trisolarispms/MainActivity.kt +++ b/app/src/main/java/com/android/trisolarispms/MainActivity.kt @@ -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(null) } val selectedGuestPhone = remember { mutableStateOf(null) } val selectedImageTag = remember { mutableStateOf(null) } + val selectedManageRooms = remember { mutableStateOf>(emptyList()) } val roomFormKey = remember { mutableStateOf(0) } val amenitiesReturnRoute = remember { mutableStateOf(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, diff --git a/app/src/main/java/com/android/trisolarispms/data/api/BookingApi.kt b/app/src/main/java/com/android/trisolarispms/data/api/BookingApi.kt index c1e3712..d157bfa 100644 --- a/app/src/main/java/com/android/trisolarispms/data/api/BookingApi.kt +++ b/app/src/main/java/com/android/trisolarispms/data/api/BookingApi.kt @@ -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 + @GET("properties/{propertyId}/bookings") + suspend fun listBookings( + @Path("propertyId") propertyId: String, + @Query("status") status: String? = null + ): Response> + + @POST("properties/{propertyId}/bookings/{bookingId}/check-in/bulk") + suspend fun bulkCheckIn( + @Path("propertyId") propertyId: String, + @Path("bookingId") bookingId: String, + @Body body: BookingBulkCheckInRequest + ): Response + @POST("properties/{propertyId}/bookings/{bookingId}/link-guest") suspend fun linkGuest( @Path("propertyId") propertyId: String, diff --git a/app/src/main/java/com/android/trisolarispms/data/api/RoomApi.kt b/app/src/main/java/com/android/trisolarispms/data/api/RoomApi.kt index 8c005fb..34afc26 100644 --- a/app/src/main/java/com/android/trisolarispms/data/api/RoomApi.kt +++ b/app/src/main/java/com/android/trisolarispms/data/api/RoomApi.kt @@ -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> + @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> + @GET("properties/{propertyId}/rooms/by-type/{roomTypeCode}") suspend fun listRoomsByType( @Path("propertyId") propertyId: String, diff --git a/app/src/main/java/com/android/trisolarispms/data/api/model/BookingModels.kt b/app/src/main/java/com/android/trisolarispms/data/api/model/BookingModels.kt index 1bcc1fa..f520a94 100644 --- a/app/src/main/java/com/android/trisolarispms/data/api/model/BookingModels.kt +++ b/app/src/main/java/com/android/trisolarispms/data/api/model/BookingModels.kt @@ -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 +) + +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 ) diff --git a/app/src/main/java/com/android/trisolarispms/data/api/model/RoomModels.kt b/app/src/main/java/com/android/trisolarispms/data/api/model/RoomModels.kt index 86da905..be9bb09 100644 --- a/app/src/main/java/com/android/trisolarispms/data/api/model/RoomModels.kt +++ b/app/src/main/java/com/android/trisolarispms/data/api/model/RoomModels.kt @@ -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( diff --git a/app/src/main/java/com/android/trisolarispms/ui/AppRoute.kt b/app/src/main/java/com/android/trisolarispms/ui/AppRoute.kt index c182a7f..4062811 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/AppRoute.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/AppRoute.kt @@ -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 diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysScreen.kt index 0b3b7fb..d78d7af 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysScreen.kt @@ -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(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) + } + } + } } diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysState.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysState.kt index a7b7e8a..06b294f 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysState.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysState.kt @@ -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 = emptyList() + val items: List = emptyList(), + val checkedInBookings: List = emptyList() ) diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysViewModel.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysViewModel.kt index 03dd054..d6959aa 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysViewModel.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysViewModel.kt @@ -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") } diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayModels.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayModels.kt new file mode 100644 index 0000000..e021b87 --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayModels.kt @@ -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? +) diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayRatesScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayRatesScreen.kt new file mode 100644 index 0000000..5484a48 --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayRatesScreen.kt @@ -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, + 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") + } + } + ) + } +} diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayRatesState.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayRatesState.kt new file mode 100644 index 0000000..c884866 --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayRatesState.kt @@ -0,0 +1,8 @@ +package com.android.trisolarispms.ui.roomstay + +data class ManageRoomStayRatesState( + val isLoading: Boolean = false, + val error: String? = null, + val items: List = emptyList(), + val lastTotal: Long? = null +) diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayRatesViewModel.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayRatesViewModel.kt new file mode 100644 index 0000000..6046bab --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayRatesViewModel.kt @@ -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 = _state + + fun setItems(items: List) { + 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") } + } + } + } +} diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStaySelectScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStaySelectScreen.kt new file mode 100644 index 0000000..f8fa113 --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStaySelectScreen.kt @@ -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) -> Unit, + viewModel: ManageRoomStaySelectViewModel = viewModel() +) { + val state by viewModel.state.collectAsState() + val selectedRooms = remember { mutableStateListOf() } + 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 + ) +} diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStaySelectState.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStaySelectState.kt new file mode 100644 index 0000000..3f6787d --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStaySelectState.kt @@ -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 = emptyList() +) diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStaySelectViewModel.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStaySelectViewModel.kt new file mode 100644 index 0000000..0eb8455 --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStaySelectViewModel.kt @@ -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 = _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") } + } + } + } +}