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.ManageRoomStaySelectScreen
|
||||
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.property.AddPropertyScreen
|
||||
import com.android.trisolarispms.ui.room.RoomFormScreen
|
||||
@@ -142,6 +143,10 @@ class MainActivity : ComponentActivity() {
|
||||
currentRoute.fromAt,
|
||||
currentRoute.toAt
|
||||
)
|
||||
is AppRoute.BookingRoomStays -> route.value = AppRoute.ActiveRoomStays(
|
||||
currentRoute.propertyId,
|
||||
selectedPropertyName.value ?: "Property"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +256,12 @@ class MainActivity : ComponentActivity() {
|
||||
toAt = toAt
|
||||
)
|
||||
}
|
||||
},
|
||||
onViewBookingStays = { booking ->
|
||||
route.value = AppRoute.BookingRoomStays(
|
||||
propertyId = currentRoute.propertyId,
|
||||
bookingId = booking.id.orEmpty()
|
||||
)
|
||||
}
|
||||
)
|
||||
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(
|
||||
propertyId = currentRoute.propertyId,
|
||||
onBack = {
|
||||
|
||||
@@ -13,6 +13,9 @@ data class BookingCreateRequest(
|
||||
val expectedCheckOutAt: String,
|
||||
val source: String? = null,
|
||||
val guestPhoneE164: String? = null,
|
||||
val fromCity: String? = null,
|
||||
val toCity: String? = null,
|
||||
val memberRelation: String? = null,
|
||||
val transportMode: String? = null,
|
||||
val childCount: Int? = null,
|
||||
val maleCount: Int? = null,
|
||||
|
||||
@@ -31,6 +31,10 @@ sealed interface AppRoute {
|
||||
val fromAt: String,
|
||||
val toAt: String?
|
||||
) : AppRoute
|
||||
data class BookingRoomStays(
|
||||
val propertyId: String,
|
||||
val bookingId: String
|
||||
) : AppRoute
|
||||
data object AddProperty : AppRoute
|
||||
data class ActiveRoomStays(val propertyId: String, val propertyName: 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.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.CalendarMonth
|
||||
@@ -75,6 +77,8 @@ fun BookingCreateScreen(
|
||||
val checkInNow = remember { mutableStateOf(true) }
|
||||
val sourceMenuExpanded = remember { mutableStateOf(false) }
|
||||
val sourceOptions = listOf("WALKIN", "OTA", "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 displayFormatter = remember { DateTimeFormatter.ofPattern("dd-MM-yy HH:mm") }
|
||||
@@ -117,7 +121,8 @@ fun BookingCreateScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(24.dp),
|
||||
.padding(24.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
Row(
|
||||
@@ -316,6 +321,52 @@ fun BookingCreateScreen(
|
||||
}
|
||||
}
|
||||
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(
|
||||
expanded = transportMenuExpanded.value,
|
||||
onExpandedChange = { transportMenuExpanded.value = !transportMenuExpanded.value }
|
||||
|
||||
@@ -9,6 +9,9 @@ data class BookingCreateState(
|
||||
val expectedCheckInAt: String = "",
|
||||
val expectedCheckOutAt: String = "",
|
||||
val source: String = "WALKIN",
|
||||
val fromCity: String = "",
|
||||
val toCity: String = "",
|
||||
val memberRelation: String = "",
|
||||
val transportMode: String = "CAR",
|
||||
val childCount: String = "",
|
||||
val maleCount: String = "",
|
||||
|
||||
@@ -88,6 +88,18 @@ class BookingCreateViewModel : ViewModel() {
|
||||
_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) {
|
||||
_state.update { it.copy(transportMode = value, error = null) }
|
||||
}
|
||||
@@ -142,6 +154,9 @@ class BookingCreateViewModel : ViewModel() {
|
||||
expectedCheckOutAt = checkOut,
|
||||
source = current.source.trim().ifBlank { null },
|
||||
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 },
|
||||
childCount = childCount,
|
||||
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.LazyVerticalGrid
|
||||
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.filled.Add
|
||||
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.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -39,6 +40,8 @@ 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
|
||||
import java.time.Duration
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -49,6 +52,7 @@ fun ActiveRoomStaysScreen(
|
||||
onViewRooms: () -> Unit,
|
||||
onCreateBooking: () -> Unit,
|
||||
onManageRoomStay: (BookingListItem) -> Unit,
|
||||
onViewBookingStays: (BookingListItem) -> Unit,
|
||||
viewModel: ActiveRoomStaysViewModel = viewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
@@ -113,29 +117,14 @@ fun ActiveRoomStaysScreen(
|
||||
items(state.checkedInBookings) { booking ->
|
||||
CheckedInBookingCard(
|
||||
booking = booking,
|
||||
onClick = { selectedBooking.value = booking }
|
||||
onClick = { selectedBooking.value = booking },
|
||||
onLongClick = { onViewBookingStays(booking) }
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
if (state.items.isEmpty()) {
|
||||
Text(text = "No active room stays")
|
||||
} else {
|
||||
state.items.forEach { item ->
|
||||
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))
|
||||
}
|
||||
Text(text = "No checked-in bookings")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,11 +163,15 @@ fun ActiveRoomStaysScreen(
|
||||
@Composable
|
||||
private fun CheckedInBookingCard(
|
||||
booking: BookingListItem,
|
||||
onClick: () -> Unit
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
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)) {
|
||||
Text(
|
||||
@@ -204,6 +197,30 @@ private fun CheckedInBookingCard(
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
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