Compare commits

...

4 Commits

Author SHA1 Message Date
androidlover5842
d69ed60a6e agents -_- 2026-02-04 15:32:44 +05:30
androidlover5842
56f13f5e79 ability to see open bookings list 2026-02-04 15:20:17 +05:30
androidlover5842
9555ae2e40 activeScreen:improve menu 2026-02-04 15:07:27 +05:30
androidlover5842
9d942d6411 createBooking: change checkout date based on property policy while editing checking date 2026-02-04 14:58:16 +05:30
8 changed files with 206 additions and 40 deletions

View File

@@ -56,6 +56,7 @@ data class BookingListItem(
val guestId: String? = null,
val guestName: String? = null,
val guestPhone: String? = null,
val vehicleNumbers: List<String> = emptyList(),
val roomNumbers: List<Int> = emptyList(),
val source: String? = null,
val fromCity: String? = null,
@@ -112,6 +113,22 @@ data class BookingBillableNightsResponse(
val billableNights: Long? = null
)
data class BookingExpectedCheckoutPreviewRequest(
val checkInAt: String,
val billableNights: Int? = null,
val billingMode: BookingBillingMode? = null,
val billingCheckinTime: String? = null,
val billingCheckoutTime: String? = null
)
data class BookingExpectedCheckoutPreviewResponse(
val expectedCheckOutAt: String? = null,
val billableNights: Int? = null,
val billingMode: BookingBillingMode? = null,
val billingCheckinTime: String? = null,
val billingCheckoutTime: String? = null
)
data class BookingDetailsResponse(
val id: String? = null,
val status: String? = null,

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ data class BookingCreateState(
val expectedCheckOutAt: String = "",
val billingMode: BookingBillingMode = BookingBillingMode.PROPERTY_POLICY,
val billingCheckoutTime: String = "",
val source: String = "WALKIN",
val source: String = "DIRECT",
val fromCity: String = "",
val toCity: String = "",
val memberRelation: String = "",

View File

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

View File

@@ -1,5 +1,6 @@
package com.android.trisolarispms.ui.roomstay
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -12,6 +13,8 @@ import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.combinedClickable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.CalendarMonth
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.People
import androidx.compose.material.icons.filled.MeetingRoom
import androidx.compose.material.icons.filled.Payment
@@ -21,6 +24,8 @@ import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -63,6 +68,11 @@ fun ActiveRoomStaysScreen(
) {
val state by viewModel.state.collectAsState()
val selectedBooking = remember { mutableStateOf<BookingListItem?>(null) }
val menuExpanded = remember { mutableStateOf(false) }
BackHandler(enabled = state.showOpenBookings) {
viewModel.hideOpenBookings()
}
LaunchedEffect(propertyId) {
viewModel.load(propertyId)
@@ -70,23 +80,63 @@ fun ActiveRoomStaysScreen(
BackTopBarScaffold(
title = propertyName,
onBack = onBack,
onBack = {
if (state.showOpenBookings) {
viewModel.hideOpenBookings()
} else {
onBack()
}
},
showBack = showBack,
actions = {
IconButton(onClick = onViewRooms) {
Icon(Icons.Default.MeetingRoom, contentDescription = "Available Rooms")
}
IconButton(onClick = onOpenSettings) {
Icon(Icons.Default.Settings, contentDescription = "Settings")
IconButton(onClick = viewModel::toggleShowOpenBookings) {
Icon(
Icons.Default.CalendarMonth,
contentDescription = "Show Open Bookings",
tint = if (state.showOpenBookings) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
)
}
if (showRazorpaySettings) {
IconButton(onClick = onRazorpaySettings) {
Icon(Icons.Default.Payment, contentDescription = "Razorpay Settings")
IconButton(onClick = { menuExpanded.value = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "Menu")
}
DropdownMenu(
expanded = menuExpanded.value,
onDismissRequest = { menuExpanded.value = false }
) {
DropdownMenuItem(
text = { Text("Settings") },
leadingIcon = { Icon(Icons.Default.Settings, contentDescription = null) },
onClick = {
menuExpanded.value = false
onOpenSettings()
}
)
if (showRazorpaySettings) {
DropdownMenuItem(
text = { Text("Razorpay Settings") },
leadingIcon = { Icon(Icons.Default.Payment, contentDescription = null) },
onClick = {
menuExpanded.value = false
onRazorpaySettings()
}
)
}
}
if (showUserAdmin) {
IconButton(onClick = onUserAdmin) {
Icon(Icons.Default.People, contentDescription = "Property Users")
if (showUserAdmin) {
DropdownMenuItem(
text = { Text("Property Users") },
leadingIcon = { Icon(Icons.Default.People, contentDescription = null) },
onClick = {
menuExpanded.value = false
onUserAdmin()
}
)
}
}
},
@@ -106,14 +156,24 @@ fun ActiveRoomStaysScreen(
Spacer(modifier = Modifier.height(8.dp))
}
state.error?.let {
Text(text = it, color = MaterialTheme.colorScheme.error)
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 && state.error == null) {
if (state.checkedInBookings.isNotEmpty()) {
Text(text = "Checked-in bookings", style = MaterialTheme.typography.titleMedium)
val shownBookings = if (state.showOpenBookings) {
state.openBookings
} else {
state.checkedInBookings
}
if (shownBookings.isNotEmpty()) {
val sectionTitle = if (state.showOpenBookings) {
"Open bookings"
} else {
"Checked-in bookings"
}
Text(text = sectionTitle, style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp))
LazyVerticalGrid(
columns = GridCells.Fixed(2),
@@ -121,7 +181,7 @@ fun ActiveRoomStaysScreen(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(state.checkedInBookings) { booking ->
items(shownBookings) { booking ->
CheckedInBookingCard(
booking = booking,
onClick = { onOpenBookingDetails(booking) })
@@ -129,7 +189,12 @@ fun ActiveRoomStaysScreen(
}
Spacer(modifier = Modifier.height(16.dp))
} else {
Text(text = "No checked-in bookings")
val emptyLabel = if (state.showOpenBookings) {
"No open bookings"
} else {
"No checked-in bookings"
}
Text(text = emptyLabel)
}
}
}
@@ -188,10 +253,6 @@ private fun CheckedInBookingCard(
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
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 {
@@ -216,6 +277,14 @@ private fun CheckedInBookingCard(
color = MaterialTheme.colorScheme.error
)
}
val vehicleNumbers = booking.vehicleNumbers.filter { it.isNotBlank() }
if (vehicleNumbers.isNotEmpty()) {
Spacer(modifier = Modifier.height(6.dp))
Text(
text = vehicleNumbers.joinToString(", "),
style = MaterialTheme.typography.bodySmall
)
}
val checkInAt = booking.checkInAt?.takeIf { it.isNotBlank() }
val checkOutAt = booking.expectedCheckOutAt?.takeIf { it.isNotBlank() }
if (checkInAt != null && checkOutAt != null) {

View File

@@ -7,5 +7,7 @@ data class ActiveRoomStaysState(
val isLoading: Boolean = false,
val error: String? = null,
val items: List<ActiveRoomStayDto> = emptyList(),
val checkedInBookings: List<BookingListItem> = emptyList()
val checkedInBookings: List<BookingListItem> = emptyList(),
val openBookings: List<BookingListItem> = emptyList(),
val showOpenBookings: Boolean = false
)

View File

@@ -11,6 +11,14 @@ class ActiveRoomStaysViewModel : ViewModel() {
private val _state = MutableStateFlow(ActiveRoomStaysState())
val state: StateFlow<ActiveRoomStaysState> = _state
fun toggleShowOpenBookings() {
_state.update { it.copy(showOpenBookings = !it.showOpenBookings) }
}
fun hideOpenBookings() {
_state.update { it.copy(showOpenBookings = false) }
}
fun load(propertyId: String) {
if (propertyId.isBlank()) return
launchRequest(
@@ -22,12 +30,14 @@ class ActiveRoomStaysViewModel : ViewModel() {
val api = ApiClient.create()
val activeResponse = api.listActiveRoomStays(propertyId)
val bookingsResponse = api.listBookings(propertyId, status = "CHECKED_IN")
val openBookingsResponse = api.listBookings(propertyId, status = "OPEN")
if (activeResponse.isSuccessful) {
_state.update {
it.copy(
isLoading = false,
items = activeResponse.body().orEmpty(),
checkedInBookings = bookingsResponse.body().orEmpty(),
openBookings = openBookingsResponse.body().orEmpty(),
error = null
)
}