From be820391bc6d233fd746618a44cbd11354594cc9 Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Thu, 29 Jan 2026 22:03:06 +0530 Subject: [PATCH] booking details: add ability to see details of guest --- AGENTS.md | 17 + app/build.gradle.kts | 1 + .../com/android/trisolarispms/MainActivity.kt | 23 ++ .../trisolarispms/data/api/BookingApi.kt | 7 + .../data/api/model/BookingModels.kt | 36 ++ .../com/android/trisolarispms/ui/AppRoute.kt | 5 + .../trisolarispms/ui/guest/GuestInfoState.kt | 1 + .../ui/guest/GuestInfoViewModel.kt | 2 + .../ui/roomstay/ActiveRoomStaysScreen.kt | 4 +- .../ui/roomstay/BookingDetailsState.kt | 9 + .../ui/roomstay/BookingDetailsTabsScreen.kt | 370 ++++++++++++++++++ .../ui/roomstay/BookingDetailsViewModel.kt | 38 ++ gradle/libs.versions.toml | 2 + 13 files changed, 514 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsState.kt create mode 100644 app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt create mode 100644 app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsViewModel.kt diff --git a/AGENTS.md b/AGENTS.md index 378263e..6a47324 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,6 +63,23 @@ Notes: --- +### Booking details + +GET /properties/{propertyId}/bookings/{bookingId} + +Includes: + +- Guest info (name/phone/nationality/address/signatureUrl) +- Room numbers (active stays if present; otherwise all stays) +- Travel fields (fromCity/toCity/memberRelation) +- Transport mode, expected/actual times +- Counts (male/female/child/total/expected) +- Registered by (createdBy name/phone) +- totalNightlyRate (sum of nightlyRate across shown rooms) +- balance: expectedPay, amountCollected, pending + +--- + ### Check-in (creates RoomStay) POST /properties/{propertyId}/bookings/{bookingId}/check-in diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 553e734..9e1ddd6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(libs.okhttp) implementation(libs.okhttp.logging) implementation(libs.coil.compose) + implementation(libs.coil.svg) implementation(libs.lottie.compose) implementation(libs.calendar.compose) implementation(libs.libphonenumber) diff --git a/app/src/main/java/com/android/trisolarispms/MainActivity.kt b/app/src/main/java/com/android/trisolarispms/MainActivity.kt index 98243ff..5e7a4b1 100644 --- a/app/src/main/java/com/android/trisolarispms/MainActivity.kt +++ b/app/src/main/java/com/android/trisolarispms/MainActivity.kt @@ -23,6 +23,7 @@ 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.roomstay.BookingRoomStaysScreen +import com.android.trisolarispms.ui.roomstay.BookingDetailsTabsScreen import com.android.trisolarispms.ui.home.HomeScreen import com.android.trisolarispms.ui.property.AddPropertyScreen import com.android.trisolarispms.ui.room.RoomFormScreen @@ -152,6 +153,10 @@ class MainActivity : ComponentActivity() { currentRoute.propertyId, selectedPropertyName.value ?: "Property" ) + is AppRoute.BookingDetailsTabs -> route.value = AppRoute.ActiveRoomStays( + currentRoute.propertyId, + selectedPropertyName.value ?: "Property" + ) } } @@ -276,6 +281,13 @@ class MainActivity : ComponentActivity() { expectedCheckInAt = booking.expectedCheckInAt, expectedCheckOutAt = booking.expectedCheckOutAt ) + }, + onOpenBookingDetails = { booking -> + route.value = AppRoute.BookingDetailsTabs( + propertyId = currentRoute.propertyId, + bookingId = booking.id.orEmpty(), + guestId = booking.guestId + ) } ) is AppRoute.ManageRoomStaySelect -> ManageRoomStaySelectScreen( @@ -392,6 +404,17 @@ class MainActivity : ComponentActivity() { ) } ) + is AppRoute.BookingDetailsTabs -> BookingDetailsTabsScreen( + propertyId = currentRoute.propertyId, + bookingId = currentRoute.bookingId, + guestId = currentRoute.guestId, + onBack = { + route.value = AppRoute.ActiveRoomStays( + currentRoute.propertyId, + selectedPropertyName.value ?: "Property" + ) + } + ) is AppRoute.Rooms -> RoomsScreen( propertyId = currentRoute.propertyId, onBack = { 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 3fec178..db1e71c 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 @@ -12,6 +12,7 @@ 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.BookingExpectedDatesRequest +import com.android.trisolarispms.data.api.model.BookingDetailsResponse import com.android.trisolarispms.data.api.model.RoomStayDto import retrofit2.Response import retrofit2.http.Body @@ -47,6 +48,12 @@ interface BookingApi { @Body body: BookingExpectedDatesRequest ): Response + @GET("properties/{propertyId}/bookings/{bookingId}") + suspend fun getBookingDetails( + @Path("propertyId") propertyId: String, + @Path("bookingId") bookingId: String + ): 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/model/BookingModels.kt b/app/src/main/java/com/android/trisolarispms/data/api/model/BookingModels.kt index c7fcf22..e5336d0 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 @@ -41,6 +41,10 @@ data class BookingListItem( val guestPhone: String? = null, val roomNumbers: List = emptyList(), val source: String? = null, + val fromCity: String? = null, + val toCity: String? = null, + val memberRelation: String? = null, + val transportMode: String? = null, val checkInAt: String? = null, val checkOutAt: String? = null, val expectedCheckInAt: String? = null, @@ -73,6 +77,38 @@ data class BookingExpectedDatesRequest( val expectedCheckOutAt: String? = null ) +data class BookingDetailsResponse( + val id: String? = null, + val status: String? = null, + val guestId: String? = null, + val guestName: String? = null, + val guestPhone: String? = null, + val guestNationality: String? = null, + val guestAddressText: String? = null, + val guestSignatureUrl: String? = null, + val roomNumbers: List = emptyList(), + val fromCity: String? = null, + val toCity: String? = null, + val memberRelation: String? = null, + val transportMode: String? = null, + val expectedCheckInAt: String? = null, + val expectedCheckOutAt: String? = null, + val checkInAt: String? = null, + val checkOutAt: String? = null, + val adultCount: Int? = null, + val maleCount: Int? = null, + val femaleCount: Int? = null, + val childCount: Int? = null, + val totalGuestCount: Int? = null, + val expectedGuestCount: Int? = null, + val totalNightlyRate: Long? = null, + val registeredByName: String? = null, + val registeredByPhone: String? = null, + val expectedPay: Long? = null, + val amountCollected: Long? = null, + val pending: Long? = null +) + data class BookingLinkGuestRequest( val guestId: String ) 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 f666293..3d18151 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/AppRoute.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/AppRoute.kt @@ -42,6 +42,11 @@ sealed interface AppRoute { val expectedCheckInAt: String?, val expectedCheckOutAt: String? ) : AppRoute + data class BookingDetailsTabs( + val propertyId: String, + val bookingId: String, + val guestId: 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/guest/GuestInfoState.kt b/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoState.kt index 34ba7e2..a79cbf5 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoState.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoState.kt @@ -5,6 +5,7 @@ data class GuestInfoState( val name: String = "", val nationality: String = "", val addressText: String = "", + val vehicleNumbers: List = emptyList(), val isLoading: Boolean = false, val error: String? = null ) diff --git a/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoViewModel.kt b/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoViewModel.kt index 286c702..b94f631 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoViewModel.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoViewModel.kt @@ -41,6 +41,7 @@ class GuestInfoViewModel : ViewModel() { name = guest?.name.orEmpty(), nationality = guest?.nationality.orEmpty(), addressText = guest?.addressText.orEmpty(), + vehicleNumbers = guest?.vehicleNumbers ?: emptyList(), error = null ) } @@ -61,6 +62,7 @@ class GuestInfoViewModel : ViewModel() { name = guest.name.orEmpty(), nationality = guest.nationality.orEmpty(), addressText = guest.addressText.orEmpty(), + vehicleNumbers = guest.vehicleNumbers ?: emptyList(), isLoading = false, error = null ) 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 bbf498b..2a6dcb9 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 @@ -54,6 +54,7 @@ fun ActiveRoomStaysScreen( onManageRoomStay: (BookingListItem) -> Unit, onViewBookingStays: (BookingListItem) -> Unit, onExtendBooking: (BookingListItem) -> Unit, + onOpenBookingDetails: (BookingListItem) -> Unit, viewModel: ActiveRoomStaysViewModel = viewModel() ) { val state by viewModel.state.collectAsState() @@ -119,7 +120,7 @@ fun ActiveRoomStaysScreen( CheckedInBookingCard( booking = booking, onClick = { selectedBooking.value = booking }, - onLongClick = { onViewBookingStays(booking) } + onLongClick = { onOpenBookingDetails(booking) } ) } } @@ -167,6 +168,7 @@ fun ActiveRoomStaysScreen( dismissButton = {} ) } + } @Composable diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsState.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsState.kt new file mode 100644 index 0000000..db58cdc --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsState.kt @@ -0,0 +1,9 @@ +package com.android.trisolarispms.ui.roomstay + +import com.android.trisolarispms.data.api.model.BookingDetailsResponse + +data class BookingDetailsState( + val isLoading: Boolean = false, + val error: String? = null, + val details: BookingDetailsResponse? = null +) diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt new file mode 100644 index 0000000..40294ae --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt @@ -0,0 +1,370 @@ +package com.android.trisolarispms.ui.roomstay + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +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.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +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.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.compose.foundation.layout.ColumnScope +import kotlinx.coroutines.launch +import com.android.trisolarispms.data.api.model.BookingDetailsResponse +import com.android.trisolarispms.data.api.FirebaseAuthTokenProvider +import com.android.trisolarispms.data.api.ApiConstants +import com.google.firebase.auth.FirebaseAuth +import coil.ImageLoader +import coil.compose.AsyncImage +import coil.decode.SvgDecoder +import coil.request.ImageRequest +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import java.time.OffsetDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import androidx.compose.ui.platform.LocalContext + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun BookingDetailsTabsScreen( + propertyId: String, + bookingId: String, + guestId: String?, + onBack: () -> Unit, + staysViewModel: BookingRoomStaysViewModel = viewModel(), + detailsViewModel: BookingDetailsViewModel = viewModel() +) { + val pagerState = rememberPagerState(initialPage = 0, pageCount = { 2 }) + val scope = rememberCoroutineScope() + val staysState by staysViewModel.state.collectAsState() + val detailsState by detailsViewModel.state.collectAsState() + + LaunchedEffect(propertyId, bookingId, guestId) { + staysViewModel.load(propertyId, bookingId) + detailsViewModel.load(propertyId, bookingId) + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Details") }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back") + } + }, + colors = TopAppBarDefaults.topAppBarColors() + ) + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + TabRow(selectedTabIndex = pagerState.currentPage) { + Tab( + selected = pagerState.currentPage == 0, + onClick = { + scope.launch { pagerState.animateScrollToPage(0) } + }, + text = { Text("Guest Info") } + ) + Tab( + selected = pagerState.currentPage == 1, + onClick = { + scope.launch { pagerState.animateScrollToPage(1) } + }, + text = { Text("Room Stays") } + ) + } + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxSize() + ) { page -> + when (page) { + 0 -> GuestInfoTabContent( + propertyId = propertyId, + details = detailsState.details, + guestId = guestId, + isLoading = detailsState.isLoading, + error = detailsState.error + ) + 1 -> BookingRoomStaysTabContent(staysState, staysViewModel) + } + } + } + } +} + +@Composable +private fun GuestInfoTabContent( + propertyId: String, + details: BookingDetailsResponse?, + guestId: String?, + isLoading: Boolean, + error: String? +) { + val displayZone = remember { ZoneId.of("Asia/Kolkata") } + val dateFormatter = remember { DateTimeFormatter.ofPattern("dd/MM/yyyy") } + val timeFormatter = remember { DateTimeFormatter.ofPattern("hh:mm a") } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .verticalScroll(rememberScrollState()) + ) { + if (isLoading) { + CircularProgressIndicator() + Spacer(modifier = Modifier.height(8.dp)) + } + error?.let { + Text(text = it, color = MaterialTheme.colorScheme.error) + Spacer(modifier = Modifier.height(8.dp)) + } + SectionCard(title = "Details") { + GuestDetailRow(label = "Name", value = details?.guestName.orEmpty()) + GuestDetailRow(label = "Address", value = details?.guestAddressText.orEmpty()) + GuestDetailRow(label = "Phone number", value = details?.guestPhone.orEmpty()) + GuestDetailRow(label = "Coming From", value = details?.fromCity.orEmpty()) + GuestDetailRow(label = "Going To", value = details?.toCity.orEmpty()) + GuestDetailRow(label = "Relation", value = details?.memberRelation.orEmpty()) + GuestDetailRow(label = "Mode of transport", value = details?.transportMode.orEmpty()) + GuestDetailRow(label = "Vehicle numbers", value = "") + } + + val checkIn = details?.checkInAt ?: details?.expectedCheckInAt + val checkOut = details?.expectedCheckOutAt ?: details?.checkOutAt + SectionCard(title = "Stay") { + if (!checkIn.isNullOrBlank()) { + val parsed = runCatching { OffsetDateTime.parse(checkIn).atZoneSameInstant(displayZone) }.getOrNull() + GuestDetailRow( + label = "Check In Time", + value = parsed?.let { "${it.format(dateFormatter)} ${it.format(timeFormatter)}" }.orEmpty() + ) + } else { + GuestDetailRow(label = "Check In Time", value = "") + } + if (!checkOut.isNullOrBlank()) { + val parsed = runCatching { OffsetDateTime.parse(checkOut).atZoneSameInstant(displayZone) }.getOrNull() + GuestDetailRow( + label = "Estimated Check Out Time", + value = parsed?.let { "${it.format(dateFormatter)} ${it.format(timeFormatter)}" }.orEmpty() + ) + } else { + GuestDetailRow(label = "Estimated Check Out Time", value = "") + } + GuestDetailRow( + label = "Rooms Booked", + value = details?.roomNumbers?.takeIf { it.isNotEmpty() }?.joinToString(", ").orEmpty() + ) + GuestDetailRow(label = "Total Adults", value = details?.adultCount?.toString().orEmpty()) + GuestDetailRow(label = "Total Males", value = details?.maleCount?.toString().orEmpty()) + GuestDetailRow(label = "Total Females", value = details?.femaleCount?.toString().orEmpty()) + GuestDetailRow(label = "Total Children", value = details?.childCount?.toString().orEmpty()) + GuestDetailRow(label = "Total Guests", value = details?.totalGuestCount?.toString().orEmpty()) + GuestDetailRow(label = "Expected Guests", value = details?.expectedGuestCount?.toString().orEmpty()) + } + + SectionCard(title = "Calculations") { + GuestDetailRow(label = "Amount Collected", value = details?.amountCollected?.toString().orEmpty()) + GuestDetailRow(label = "Rent per day", value = details?.totalNightlyRate?.toString().orEmpty()) + GuestDetailRow(label = "Expected pay", value = details?.expectedPay?.toString().orEmpty()) + GuestDetailRow(label = "Pending", value = details?.pending?.toString().orEmpty()) + } + + SectionCard(title = "Registered By") { + GuestDetailRow(label = "Name", value = details?.registeredByName.orEmpty()) + GuestDetailRow(label = "Phone number", value = details?.registeredByPhone.orEmpty()) + } + + Spacer(modifier = Modifier.height(16.dp)) + SignaturePreview( + propertyId = propertyId, + guestId = details?.guestId ?: guestId, + signatureUrl = details?.guestSignatureUrl + ) + } +} + +@Composable +private fun GuestDetailRow(label: String, value: String) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 6.dp) + ) { + Text( + text = label, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = value.ifBlank { "-" }, + style = MaterialTheme.typography.bodyLarge + ) + } +} + +@Composable +private fun SectionCard(title: String, content: @Composable ColumnScope.() -> Unit) { + Card( + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text(text = title, style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(8.dp)) + content() + } + } + Spacer(modifier = Modifier.height(12.dp)) +} + +@Composable +private fun SignaturePreview(propertyId: String, guestId: String?, signatureUrl: String?) { + val resolvedGuestId = guestId + if (resolvedGuestId.isNullOrBlank() && signatureUrl.isNullOrBlank()) { + Text(text = "Signature", style = MaterialTheme.typography.titleSmall) + Spacer(modifier = Modifier.height(8.dp)) + Text(text = "Not available", style = MaterialTheme.typography.bodySmall) + return + } + val auth = remember { FirebaseAuth.getInstance() } + val tokenProvider = remember { FirebaseAuthTokenProvider(auth) } + val context = LocalContext.current + val imageLoader = remember { + ImageLoader.Builder(context) + .components { add(SvgDecoder.Factory()) } + .okHttpClient( + OkHttpClient.Builder() + .addInterceptor(Interceptor { chain -> + val original = chain.request() + val token = runCatching { + kotlinx.coroutines.runBlocking { tokenProvider.token(forceRefresh = false) } + }.getOrNull() + if (token.isNullOrBlank()) { + chain.proceed(original) + } else { + val request = original.newBuilder() + .addHeader("Authorization", "Bearer $token") + .build() + chain.proceed(request) + } + }) + .build() + ) + .build() + } + val url = signatureUrl?.let { if (it.startsWith("http")) it else "${ApiConstants.BASE_URL.trimEnd('/')}$it" } + ?: "${ApiConstants.BASE_URL}properties/$propertyId/guests/$resolvedGuestId/signature/file" + Column { + Text(text = "Signature", style = MaterialTheme.typography.titleSmall) + Spacer(modifier = Modifier.height(8.dp)) + Box( + modifier = Modifier + .fillMaxWidth() + .height(120.dp) + ) { + AsyncImage( + model = ImageRequest.Builder(context) + .data(url) + .build(), + imageLoader = imageLoader, + contentDescription = "Signature", + contentScale = ContentScale.Fit, + modifier = Modifier + .fillMaxWidth() + .height(120.dp) + .padding(8.dp) + ) + } + } +} + +@Composable +private fun BookingRoomStaysTabContent( + state: BookingRoomStaysState, + viewModel: BookingRoomStaysViewModel +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically + ) { + Text(text = "Show all (including checkout)") + androidx.compose.material3.Switch( + checked = state.showAll, + onCheckedChange = viewModel::toggleShowAll + ) + } + Spacer(modifier = Modifier.height(12.dp)) + if (state.isLoading) { + CircularProgressIndicator() + Spacer(modifier = Modifier.height(8.dp)) + } + state.error?.let { + Text(text = it, color = MaterialTheme.colorScheme.error) + Spacer(modifier = Modifier.height(8.dp)) + } + if (!state.isLoading && state.error == null) { + if (state.stays.isEmpty()) { + Text(text = "No stays found") + } else { + state.stays.forEach { stay -> + val roomLine = "Room ${stay.roomNumber ?: "-"} • ${stay.roomTypeName ?: ""}".trim() + Text(text = roomLine, style = MaterialTheme.typography.titleMedium) + val guestLine = listOfNotNull(stay.guestName, stay.guestPhone).joinToString(" • ") + if (guestLine.isNotBlank()) { + Text(text = guestLine, style = MaterialTheme.typography.bodyMedium) + } + val timeLine = listOfNotNull(stay.fromAt, stay.expectedCheckoutAt).joinToString(" → ") + if (timeLine.isNotBlank()) { + Text(text = timeLine, style = MaterialTheme.typography.bodySmall) + } + Spacer(modifier = Modifier.height(12.dp)) + } + } + } + } +} diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsViewModel.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsViewModel.kt new file mode 100644 index 0000000..92018cb --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsViewModel.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 BookingDetailsViewModel : ViewModel() { + private val _state = MutableStateFlow(BookingDetailsState()) + val state: StateFlow = _state + + fun load(propertyId: String, bookingId: String) { + if (propertyId.isBlank() || bookingId.isBlank()) return + viewModelScope.launch { + _state.update { it.copy(isLoading = true, error = null) } + try { + val api = ApiClient.create() + val response = api.getBookingDetails(propertyId, bookingId) + if (response.isSuccessful) { + _state.update { + it.copy( + isLoading = false, + details = response.body(), + 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") } + } + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dee6fd5..38a5cdf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ lifecycleViewModelCompose = "2.10.0" firebaseAuthKtx = "24.0.1" vectordrawable = "1.2.0" coilCompose = "2.7.0" +coilSvg = "2.7.0" lottieCompose = "6.7.1" calendarCompose = "2.6.0" libphonenumber = "8.13.34" @@ -49,6 +50,7 @@ kotlinx-coroutines-play-services = { group = "org.jetbrains.kotlinx", name = "ko androidx-vectordrawable = { group = "androidx.vectordrawable", name = "vectordrawable", version.ref = "vectordrawable" } androidx-vectordrawable-animated = { group = "androidx.vectordrawable", name = "vectordrawable-animated", version.ref = "vectordrawable" } coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coilCompose" } +coil-svg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coilSvg" } lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "lottieCompose" } calendar-compose = { group = "com.kizitonwose.calendar", name = "compose", version.ref = "calendarCompose" } libphonenumber = { group = "com.googlecode.libphonenumber", name = "libphonenumber", version.ref = "libphonenumber" }