ability to see stays from booking id on click
This commit is contained in:
@@ -21,6 +21,7 @@ import com.android.trisolarispms.ui.guest.GuestSignatureScreen
|
|||||||
import com.android.trisolarispms.ui.roomstay.ManageRoomStayRatesScreen
|
import com.android.trisolarispms.ui.roomstay.ManageRoomStayRatesScreen
|
||||||
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelectScreen
|
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelectScreen
|
||||||
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelection
|
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelection
|
||||||
|
import com.android.trisolarispms.ui.roomstay.BookingRoomStaysScreen
|
||||||
import com.android.trisolarispms.ui.home.HomeScreen
|
import com.android.trisolarispms.ui.home.HomeScreen
|
||||||
import com.android.trisolarispms.ui.property.AddPropertyScreen
|
import com.android.trisolarispms.ui.property.AddPropertyScreen
|
||||||
import com.android.trisolarispms.ui.room.RoomFormScreen
|
import com.android.trisolarispms.ui.room.RoomFormScreen
|
||||||
@@ -142,6 +143,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
currentRoute.fromAt,
|
currentRoute.fromAt,
|
||||||
currentRoute.toAt
|
currentRoute.toAt
|
||||||
)
|
)
|
||||||
|
is AppRoute.BookingRoomStays -> route.value = AppRoute.ActiveRoomStays(
|
||||||
|
currentRoute.propertyId,
|
||||||
|
selectedPropertyName.value ?: "Property"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +256,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
toAt = toAt
|
toAt = toAt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onViewBookingStays = { booking ->
|
||||||
|
route.value = AppRoute.BookingRoomStays(
|
||||||
|
propertyId = currentRoute.propertyId,
|
||||||
|
bookingId = booking.id.orEmpty()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
is AppRoute.ManageRoomStaySelect -> ManageRoomStaySelectScreen(
|
is AppRoute.ManageRoomStaySelect -> ManageRoomStaySelectScreen(
|
||||||
@@ -338,6 +349,16 @@ class MainActivity : ComponentActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
is AppRoute.BookingRoomStays -> BookingRoomStaysScreen(
|
||||||
|
propertyId = currentRoute.propertyId,
|
||||||
|
bookingId = currentRoute.bookingId,
|
||||||
|
onBack = {
|
||||||
|
route.value = AppRoute.ActiveRoomStays(
|
||||||
|
currentRoute.propertyId,
|
||||||
|
selectedPropertyName.value ?: "Property"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
is AppRoute.Rooms -> RoomsScreen(
|
is AppRoute.Rooms -> RoomsScreen(
|
||||||
propertyId = currentRoute.propertyId,
|
propertyId = currentRoute.propertyId,
|
||||||
onBack = {
|
onBack = {
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ data class BookingCreateRequest(
|
|||||||
val expectedCheckOutAt: String,
|
val expectedCheckOutAt: String,
|
||||||
val source: String? = null,
|
val source: String? = null,
|
||||||
val guestPhoneE164: String? = null,
|
val guestPhoneE164: String? = null,
|
||||||
|
val fromCity: String? = null,
|
||||||
|
val toCity: String? = null,
|
||||||
|
val memberRelation: String? = null,
|
||||||
val transportMode: String? = null,
|
val transportMode: String? = null,
|
||||||
val childCount: Int? = null,
|
val childCount: Int? = null,
|
||||||
val maleCount: Int? = null,
|
val maleCount: Int? = null,
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ sealed interface AppRoute {
|
|||||||
val fromAt: String,
|
val fromAt: String,
|
||||||
val toAt: String?
|
val toAt: String?
|
||||||
) : AppRoute
|
) : AppRoute
|
||||||
|
data class BookingRoomStays(
|
||||||
|
val propertyId: String,
|
||||||
|
val bookingId: String
|
||||||
|
) : AppRoute
|
||||||
data object AddProperty : AppRoute
|
data object AddProperty : AppRoute
|
||||||
data class ActiveRoomStays(val propertyId: String, val propertyName: String) : AppRoute
|
data class ActiveRoomStays(val propertyId: String, val propertyName: String) : AppRoute
|
||||||
data class Rooms(val propertyId: String) : AppRoute
|
data class Rooms(val propertyId: String) : AppRoute
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.CalendarMonth
|
import androidx.compose.material.icons.filled.CalendarMonth
|
||||||
@@ -75,6 +77,8 @@ fun BookingCreateScreen(
|
|||||||
val checkInNow = remember { mutableStateOf(true) }
|
val checkInNow = remember { mutableStateOf(true) }
|
||||||
val sourceMenuExpanded = remember { mutableStateOf(false) }
|
val sourceMenuExpanded = remember { mutableStateOf(false) }
|
||||||
val sourceOptions = listOf("WALKIN", "OTA", "AGENT")
|
val sourceOptions = listOf("WALKIN", "OTA", "AGENT")
|
||||||
|
val relationMenuExpanded = remember { mutableStateOf(false) }
|
||||||
|
val relationOptions = listOf("FRIENDS", "FAMILY", "GROUP", "ALONE")
|
||||||
val transportMenuExpanded = remember { mutableStateOf(false) }
|
val transportMenuExpanded = remember { mutableStateOf(false) }
|
||||||
val transportOptions = listOf("CAR", "BIKE", "TRAIN", "PLANE", "BUS", "FOOT", "CYCLE", "OTHER")
|
val transportOptions = listOf("CAR", "BIKE", "TRAIN", "PLANE", "BUS", "FOOT", "CYCLE", "OTHER")
|
||||||
val displayFormatter = remember { DateTimeFormatter.ofPattern("dd-MM-yy HH:mm") }
|
val displayFormatter = remember { DateTimeFormatter.ofPattern("dd-MM-yy HH:mm") }
|
||||||
@@ -117,7 +121,8 @@ fun BookingCreateScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.padding(24.dp),
|
.padding(24.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
verticalArrangement = Arrangement.Top
|
verticalArrangement = Arrangement.Top
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -316,6 +321,52 @@ fun BookingCreateScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.fromCity,
|
||||||
|
onValueChange = viewModel::onFromCityChange,
|
||||||
|
label = { Text("From City (optional)") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.toCity,
|
||||||
|
onValueChange = viewModel::onToCityChange,
|
||||||
|
label = { Text("To City (optional)") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = relationMenuExpanded.value,
|
||||||
|
onExpandedChange = { relationMenuExpanded.value = !relationMenuExpanded.value }
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.memberRelation,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text("Member Relation (optional)") },
|
||||||
|
trailingIcon = {
|
||||||
|
ExposedDropdownMenuDefaults.TrailingIcon(expanded = relationMenuExpanded.value)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor()
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = relationMenuExpanded.value,
|
||||||
|
onDismissRequest = { relationMenuExpanded.value = false }
|
||||||
|
) {
|
||||||
|
relationOptions.forEach { option ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(option) },
|
||||||
|
onClick = {
|
||||||
|
relationMenuExpanded.value = false
|
||||||
|
viewModel.onMemberRelationChange(option)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
ExposedDropdownMenuBox(
|
ExposedDropdownMenuBox(
|
||||||
expanded = transportMenuExpanded.value,
|
expanded = transportMenuExpanded.value,
|
||||||
onExpandedChange = { transportMenuExpanded.value = !transportMenuExpanded.value }
|
onExpandedChange = { transportMenuExpanded.value = !transportMenuExpanded.value }
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ data class BookingCreateState(
|
|||||||
val expectedCheckInAt: String = "",
|
val expectedCheckInAt: String = "",
|
||||||
val expectedCheckOutAt: String = "",
|
val expectedCheckOutAt: String = "",
|
||||||
val source: String = "WALKIN",
|
val source: String = "WALKIN",
|
||||||
|
val fromCity: String = "",
|
||||||
|
val toCity: String = "",
|
||||||
|
val memberRelation: String = "",
|
||||||
val transportMode: String = "CAR",
|
val transportMode: String = "CAR",
|
||||||
val childCount: String = "",
|
val childCount: String = "",
|
||||||
val maleCount: String = "",
|
val maleCount: String = "",
|
||||||
|
|||||||
@@ -88,6 +88,18 @@ class BookingCreateViewModel : ViewModel() {
|
|||||||
_state.update { it.copy(source = value, error = null) }
|
_state.update { it.copy(source = value, error = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onFromCityChange(value: String) {
|
||||||
|
_state.update { it.copy(fromCity = value, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onToCityChange(value: String) {
|
||||||
|
_state.update { it.copy(toCity = value, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onMemberRelationChange(value: String) {
|
||||||
|
_state.update { it.copy(memberRelation = value, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
fun onTransportModeChange(value: String) {
|
fun onTransportModeChange(value: String) {
|
||||||
_state.update { it.copy(transportMode = value, error = null) }
|
_state.update { it.copy(transportMode = value, error = null) }
|
||||||
}
|
}
|
||||||
@@ -142,6 +154,9 @@ class BookingCreateViewModel : ViewModel() {
|
|||||||
expectedCheckOutAt = checkOut,
|
expectedCheckOutAt = checkOut,
|
||||||
source = current.source.trim().ifBlank { null },
|
source = current.source.trim().ifBlank { null },
|
||||||
guestPhoneE164 = phone,
|
guestPhoneE164 = phone,
|
||||||
|
fromCity = current.fromCity.trim().ifBlank { null },
|
||||||
|
toCity = current.toCity.trim().ifBlank { null },
|
||||||
|
memberRelation = current.memberRelation.trim().ifBlank { null },
|
||||||
transportMode = current.transportMode.trim().ifBlank { null },
|
transportMode = current.transportMode.trim().ifBlank { null },
|
||||||
childCount = childCount,
|
childCount = childCount,
|
||||||
maleCount = maleCount,
|
maleCount = maleCount,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
@@ -29,6 +29,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -39,6 +40,8 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.android.trisolarispms.data.api.model.BookingListItem
|
import com.android.trisolarispms.data.api.model.BookingListItem
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -49,6 +52,7 @@ fun ActiveRoomStaysScreen(
|
|||||||
onViewRooms: () -> Unit,
|
onViewRooms: () -> Unit,
|
||||||
onCreateBooking: () -> Unit,
|
onCreateBooking: () -> Unit,
|
||||||
onManageRoomStay: (BookingListItem) -> Unit,
|
onManageRoomStay: (BookingListItem) -> Unit,
|
||||||
|
onViewBookingStays: (BookingListItem) -> Unit,
|
||||||
viewModel: ActiveRoomStaysViewModel = viewModel()
|
viewModel: ActiveRoomStaysViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
@@ -113,29 +117,14 @@ fun ActiveRoomStaysScreen(
|
|||||||
items(state.checkedInBookings) { booking ->
|
items(state.checkedInBookings) { booking ->
|
||||||
CheckedInBookingCard(
|
CheckedInBookingCard(
|
||||||
booking = booking,
|
booking = booking,
|
||||||
onClick = { selectedBooking.value = booking }
|
onClick = { selectedBooking.value = booking },
|
||||||
|
onLongClick = { onViewBookingStays(booking) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
}
|
|
||||||
|
|
||||||
if (state.items.isEmpty()) {
|
|
||||||
Text(text = "No active room stays")
|
|
||||||
} else {
|
} else {
|
||||||
state.items.forEach { item ->
|
Text(text = "No checked-in bookings")
|
||||||
val roomLine = "Room ${item.roomNumber ?: "-"} • ${item.roomTypeName ?: ""}".trim()
|
|
||||||
Text(text = roomLine, style = MaterialTheme.typography.titleMedium)
|
|
||||||
val guestLine = listOfNotNull(item.guestName, item.guestPhone).joinToString(" • ")
|
|
||||||
if (guestLine.isNotBlank()) {
|
|
||||||
Text(text = guestLine, style = MaterialTheme.typography.bodyMedium)
|
|
||||||
}
|
|
||||||
val timeLine = listOfNotNull(item.fromAt, item.expectedCheckoutAt).joinToString(" → ")
|
|
||||||
if (timeLine.isNotBlank()) {
|
|
||||||
Text(text = timeLine, style = MaterialTheme.typography.bodySmall)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,11 +163,15 @@ fun ActiveRoomStaysScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun CheckedInBookingCard(
|
private fun CheckedInBookingCard(
|
||||||
booking: BookingListItem,
|
booking: BookingListItem,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||||
modifier = Modifier.clickable(onClick = onClick)
|
modifier = Modifier.combinedClickable(
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(12.dp)) {
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
Text(
|
Text(
|
||||||
@@ -204,6 +197,30 @@ private fun CheckedInBookingCard(
|
|||||||
Spacer(modifier = Modifier.height(6.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
Text(text = notes, style = MaterialTheme.typography.bodySmall)
|
Text(text = notes, style = MaterialTheme.typography.bodySmall)
|
||||||
}
|
}
|
||||||
|
val checkInAt = booking.checkInAt?.takeIf { it.isNotBlank() }
|
||||||
|
val checkOutAt = booking.expectedCheckOutAt?.takeIf { it.isNotBlank() }
|
||||||
|
if (checkInAt != null && checkOutAt != null) {
|
||||||
|
val now = OffsetDateTime.now()
|
||||||
|
val start = runCatching { OffsetDateTime.parse(checkInAt) }.getOrNull()
|
||||||
|
val end = runCatching { OffsetDateTime.parse(checkOutAt) }.getOrNull()
|
||||||
|
if (start != null && end != null) {
|
||||||
|
val total = Duration.between(start, end).toMinutes().coerceAtLeast(0)
|
||||||
|
val remaining = Duration.between(now, end).toMinutes().coerceAtLeast(0)
|
||||||
|
val hoursLeft = (remaining / 60).coerceAtLeast(0)
|
||||||
|
val progress = if (total > 0) {
|
||||||
|
((total - remaining).toFloat() / total.toFloat()).coerceIn(0f, 1f)
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(text = "$hoursLeft hours", style = MaterialTheme.typography.bodySmall)
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { progress },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package com.android.trisolarispms.ui.roomstay
|
||||||
|
|
||||||
|
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.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.Switch
|
||||||
|
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.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
fun BookingRoomStaysScreen(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
viewModel: BookingRoomStaysViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(propertyId, bookingId) {
|
||||||
|
viewModel.load(propertyId, bookingId)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Room Stays") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(text = "Show all (including checkout)")
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.android.trisolarispms.ui.roomstay
|
||||||
|
|
||||||
|
import com.android.trisolarispms.data.api.model.ActiveRoomStayDto
|
||||||
|
|
||||||
|
data class BookingRoomStaysState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val error: String? = null,
|
||||||
|
val stays: List<ActiveRoomStayDto> = emptyList(),
|
||||||
|
val showAll: Boolean = false
|
||||||
|
)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
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 BookingRoomStaysViewModel : ViewModel() {
|
||||||
|
private val _state = MutableStateFlow(BookingRoomStaysState())
|
||||||
|
val state: StateFlow<BookingRoomStaysState> = _state
|
||||||
|
|
||||||
|
fun toggleShowAll(value: Boolean) {
|
||||||
|
_state.update { it.copy(showAll = value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
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.listActiveRoomStays(propertyId)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val filtered = response.body().orEmpty().filter { it.bookingId == bookingId }
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
stays = filtered,
|
||||||
|
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