stay: improve checkin and checkout time editor
This commit is contained in:
@@ -11,15 +11,11 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
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.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.CalendarMonth
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
@@ -51,8 +47,10 @@ fun BookingCreateScreen(
|
|||||||
viewModel: BookingCreateViewModel = viewModel()
|
viewModel: BookingCreateViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val showCheckInPicker = remember { mutableStateOf(false) }
|
val showCheckInDatePicker = remember { mutableStateOf(false) }
|
||||||
val showCheckOutPicker = remember { mutableStateOf(false) }
|
val showCheckInTimePicker = remember { mutableStateOf(false) }
|
||||||
|
val showCheckOutDatePicker = remember { mutableStateOf(false) }
|
||||||
|
val showCheckOutTimePicker = remember { mutableStateOf(false) }
|
||||||
val checkInDate = remember { mutableStateOf<LocalDate?>(null) }
|
val checkInDate = remember { mutableStateOf<LocalDate?>(null) }
|
||||||
val checkOutDate = remember { mutableStateOf<LocalDate?>(null) }
|
val checkOutDate = remember { mutableStateOf<LocalDate?>(null) }
|
||||||
val checkInTime = remember { mutableStateOf("12:00") }
|
val checkInTime = remember { mutableStateOf("12:00") }
|
||||||
@@ -76,6 +74,12 @@ fun BookingCreateScreen(
|
|||||||
checkInTime.value = time
|
checkInTime.value = time
|
||||||
val checkInAt = formatBookingIso(date, time)
|
val checkInAt = formatBookingIso(date, time)
|
||||||
viewModel.onExpectedCheckInAtChange(checkInAt)
|
viewModel.onExpectedCheckInAtChange(checkInAt)
|
||||||
|
val currentCheckOutDate = checkOutDate.value
|
||||||
|
if (currentCheckOutDate != null && currentCheckOutDate.isBefore(date)) {
|
||||||
|
checkOutDate.value = date
|
||||||
|
val adjustedCheckOutAt = formatBookingIso(date, checkOutTime.value)
|
||||||
|
viewModel.onExpectedCheckOutAtChange(adjustedCheckOutAt)
|
||||||
|
}
|
||||||
viewModel.autoSetBillingFromCheckIn(checkInAt)
|
viewModel.autoSetBillingFromCheckIn(checkInAt)
|
||||||
viewModel.refreshExpectedCheckoutPreview(propertyId)
|
viewModel.refreshExpectedCheckoutPreview(propertyId)
|
||||||
}
|
}
|
||||||
@@ -124,43 +128,35 @@ fun BookingCreateScreen(
|
|||||||
if (enabled) {
|
if (enabled) {
|
||||||
val now = OffsetDateTime.now()
|
val now = OffsetDateTime.now()
|
||||||
applyCheckInSelection(now.toLocalDate(), now.format(timeFormatter))
|
applyCheckInSelection(now.toLocalDate(), now.format(timeFormatter))
|
||||||
} else {
|
|
||||||
viewModel.onExpectedCheckInAtChange("")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!checkInNow.value) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.expectedCheckInAt.takeIf { it.isNotBlank() }?.let {
|
|
||||||
runCatching { OffsetDateTime.parse(it).format(displayFormatter) }.getOrDefault(it)
|
|
||||||
}.orEmpty(),
|
|
||||||
onValueChange = {},
|
|
||||||
readOnly = true,
|
|
||||||
label = { Text("Expected Check-in") },
|
|
||||||
trailingIcon = {
|
|
||||||
IconButton(onClick = { showCheckInPicker.value = true }) {
|
|
||||||
Icon(Icons.Default.CalendarMonth, contentDescription = "Pick date")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
OutlinedTextField(
|
val checkInDisplay = state.expectedCheckInAt.takeIf { it.isNotBlank() }?.let {
|
||||||
value = state.expectedCheckOutAt.takeIf { it.isNotBlank() }?.let {
|
runCatching { OffsetDateTime.parse(it) }.getOrNull()
|
||||||
runCatching { OffsetDateTime.parse(it).format(displayFormatter) }.getOrDefault(it)
|
}
|
||||||
}.orEmpty(),
|
val checkOutDisplay = state.expectedCheckOutAt.takeIf { it.isNotBlank() }?.let {
|
||||||
onValueChange = {},
|
runCatching { OffsetDateTime.parse(it) }.getOrNull()
|
||||||
readOnly = true,
|
}
|
||||||
label = { Text("Expected Check-out") },
|
val cardDateFormatter = remember { DateTimeFormatter.ofPattern("dd/MM/yyyy") }
|
||||||
trailingIcon = {
|
val cardTimeFormatter = remember { DateTimeFormatter.ofPattern("hh:mm a") }
|
||||||
IconButton(onClick = { showCheckOutPicker.value = true }) {
|
val totalTimeText = remember(state.expectedCheckInAt, state.expectedCheckOutAt) {
|
||||||
Icon(Icons.Default.CalendarMonth, contentDescription = "Pick date")
|
val start = runCatching { OffsetDateTime.parse(state.expectedCheckInAt) }.getOrNull()
|
||||||
}
|
val end = runCatching { OffsetDateTime.parse(state.expectedCheckOutAt) }.getOrNull()
|
||||||
},
|
formatBookingDurationText(start, end)
|
||||||
modifier = Modifier.fillMaxWidth()
|
}
|
||||||
|
BookingDateTimeQuickEditorCard(
|
||||||
|
checkInDateText = checkInDisplay?.format(cardDateFormatter) ?: "--/--/----",
|
||||||
|
checkInTimeText = checkInDisplay?.format(cardTimeFormatter) ?: "--:--",
|
||||||
|
checkOutDateText = checkOutDisplay?.format(cardDateFormatter) ?: "--/--/----",
|
||||||
|
checkOutTimeText = checkOutDisplay?.format(cardTimeFormatter) ?: "--:--",
|
||||||
|
totalTimeText = totalTimeText,
|
||||||
|
checkInEditable = !checkInNow.value,
|
||||||
|
onCheckInDateClick = { showCheckInDatePicker.value = true },
|
||||||
|
onCheckInTimeClick = { showCheckInTimePicker.value = true },
|
||||||
|
onCheckOutDateClick = { showCheckOutDatePicker.value = true },
|
||||||
|
onCheckOutTimeClick = { showCheckOutTimePicker.value = true }
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
ExposedDropdownMenuBox(
|
ExposedDropdownMenuBox(
|
||||||
@@ -485,33 +481,50 @@ fun BookingCreateScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showCheckInPicker.value) {
|
if (showCheckInDatePicker.value) {
|
||||||
BookingDateTimePickerDialog(
|
BookingDatePickerDialog(
|
||||||
title = "Select check-in",
|
initialDate = checkInDate.value ?: LocalDate.now(),
|
||||||
initialDate = checkInDate.value,
|
|
||||||
initialTime = checkInTime.value,
|
|
||||||
minDate = LocalDate.now(),
|
minDate = LocalDate.now(),
|
||||||
onDismiss = { showCheckInPicker.value = false },
|
onDismiss = { showCheckInDatePicker.value = false },
|
||||||
onConfirm = { date, time ->
|
onDateSelected = { selectedDate ->
|
||||||
applyCheckInSelection(date, time)
|
applyCheckInSelection(selectedDate, checkInTime.value)
|
||||||
showCheckInPicker.value = false
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showCheckOutPicker.value) {
|
if (showCheckInTimePicker.value) {
|
||||||
BookingDateTimePickerDialog(
|
BookingTimePickerDialog(
|
||||||
title = "Select check-out",
|
initialTime = checkInTime.value,
|
||||||
initialDate = checkOutDate.value,
|
onDismiss = { showCheckInTimePicker.value = false },
|
||||||
initialTime = checkOutTime.value,
|
onTimeSelected = { selectedTime ->
|
||||||
|
val selectedDate = checkInDate.value ?: LocalDate.now()
|
||||||
|
applyCheckInSelection(selectedDate, selectedTime)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showCheckOutDatePicker.value) {
|
||||||
|
BookingDatePickerDialog(
|
||||||
|
initialDate = checkOutDate.value ?: (checkInDate.value ?: LocalDate.now()),
|
||||||
minDate = checkInDate.value ?: LocalDate.now(),
|
minDate = checkInDate.value ?: LocalDate.now(),
|
||||||
onDismiss = { showCheckOutPicker.value = false },
|
onDismiss = { showCheckOutDatePicker.value = false },
|
||||||
onConfirm = { date, time ->
|
onDateSelected = { selectedDate ->
|
||||||
checkOutDate.value = date
|
checkOutDate.value = selectedDate
|
||||||
checkOutTime.value = time
|
val formatted = formatBookingIso(selectedDate, checkOutTime.value)
|
||||||
val formatted = formatBookingIso(date, time)
|
viewModel.onExpectedCheckOutAtChange(formatted)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showCheckOutTimePicker.value) {
|
||||||
|
BookingTimePickerDialog(
|
||||||
|
initialTime = checkOutTime.value,
|
||||||
|
onDismiss = { showCheckOutTimePicker.value = false },
|
||||||
|
onTimeSelected = { selectedTime ->
|
||||||
|
checkOutTime.value = selectedTime
|
||||||
|
val selectedDate = checkOutDate.value ?: (checkInDate.value ?: LocalDate.now())
|
||||||
|
val formatted = formatBookingIso(selectedDate, selectedTime)
|
||||||
viewModel.onExpectedCheckOutAtChange(formatted)
|
viewModel.onExpectedCheckOutAtChange(formatted)
|
||||||
showCheckOutPicker.value = false
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
package com.android.trisolarispms.ui.booking
|
||||||
|
|
||||||
|
import android.app.DatePickerDialog
|
||||||
|
import android.app.TimePickerDialog
|
||||||
|
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.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun BookingDateTimeQuickEditorCard(
|
||||||
|
checkInDateText: String,
|
||||||
|
checkInTimeText: String,
|
||||||
|
checkOutDateText: String,
|
||||||
|
checkOutTimeText: String,
|
||||||
|
totalTimeText: String?,
|
||||||
|
checkInEditable: Boolean,
|
||||||
|
checkOutEditable: Boolean = true,
|
||||||
|
onCheckInDateClick: () -> Unit,
|
||||||
|
onCheckInTimeClick: () -> Unit,
|
||||||
|
onCheckOutDateClick: () -> Unit,
|
||||||
|
onCheckOutTimeClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
shape = MaterialTheme.shapes.large
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
BookingDateTimeQuickEditorRow(
|
||||||
|
label = "Check In Time:",
|
||||||
|
dateText = checkInDateText,
|
||||||
|
timeText = checkInTimeText,
|
||||||
|
editable = checkInEditable,
|
||||||
|
onDateClick = onCheckInDateClick,
|
||||||
|
onTimeClick = onCheckInTimeClick
|
||||||
|
)
|
||||||
|
BookingDateTimeQuickEditorRow(
|
||||||
|
label = "Check Out Time:",
|
||||||
|
dateText = checkOutDateText,
|
||||||
|
timeText = checkOutTimeText,
|
||||||
|
editable = checkOutEditable,
|
||||||
|
onDateClick = onCheckOutDateClick,
|
||||||
|
onTimeClick = onCheckOutTimeClick
|
||||||
|
)
|
||||||
|
if (!totalTimeText.isNullOrBlank()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Total Time:",
|
||||||
|
style = MaterialTheme.typography.titleSmall
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = totalTimeText,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
style = MaterialTheme.typography.titleSmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BookingDateTimeQuickEditorRow(
|
||||||
|
label: String,
|
||||||
|
dateText: String,
|
||||||
|
timeText: String,
|
||||||
|
editable: Boolean,
|
||||||
|
onDateClick: () -> Unit,
|
||||||
|
onTimeClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val valueColor = if (editable) {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(text = label, style = MaterialTheme.typography.bodyLarge)
|
||||||
|
Text(
|
||||||
|
text = timeText,
|
||||||
|
color = valueColor,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(enabled = editable, onClick = onTimeClick)
|
||||||
|
.padding(vertical = 2.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(2.dp))
|
||||||
|
Text(
|
||||||
|
text = dateText,
|
||||||
|
color = valueColor,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(enabled = editable, onClick = onDateClick)
|
||||||
|
.padding(vertical = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun BookingDatePickerDialog(
|
||||||
|
initialDate: LocalDate,
|
||||||
|
minDate: LocalDate,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onDateSelected: (LocalDate) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val dismissState = rememberUpdatedState(onDismiss)
|
||||||
|
val selectDateState = rememberUpdatedState(onDateSelected)
|
||||||
|
|
||||||
|
DisposableEffect(context, initialDate, minDate) {
|
||||||
|
val dialog = DatePickerDialog(
|
||||||
|
context,
|
||||||
|
{ _, year, monthOfYear, dayOfMonth ->
|
||||||
|
selectDateState.value(LocalDate.of(year, monthOfYear + 1, dayOfMonth))
|
||||||
|
},
|
||||||
|
initialDate.year,
|
||||||
|
initialDate.monthValue - 1,
|
||||||
|
initialDate.dayOfMonth
|
||||||
|
)
|
||||||
|
val minDateMillis = minDate
|
||||||
|
.atStartOfDay(ZoneId.systemDefault())
|
||||||
|
.toInstant()
|
||||||
|
.toEpochMilli()
|
||||||
|
dialog.datePicker.minDate = minDateMillis
|
||||||
|
dialog.setOnDismissListener { dismissState.value() }
|
||||||
|
dialog.show()
|
||||||
|
onDispose {
|
||||||
|
dialog.setOnDismissListener(null)
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun BookingTimePickerDialog(
|
||||||
|
initialTime: String,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onTimeSelected: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val dismissState = rememberUpdatedState(onDismiss)
|
||||||
|
val selectTimeState = rememberUpdatedState(onTimeSelected)
|
||||||
|
|
||||||
|
val initialHour = initialTime.split(":").getOrNull(0)?.toIntOrNull()?.coerceIn(0, 23) ?: 12
|
||||||
|
val initialMinute = initialTime.split(":").getOrNull(1)?.toIntOrNull()?.coerceIn(0, 59) ?: 0
|
||||||
|
|
||||||
|
DisposableEffect(context, initialHour, initialMinute) {
|
||||||
|
val dialog = TimePickerDialog(
|
||||||
|
context,
|
||||||
|
{ _, hourOfDay, minute ->
|
||||||
|
selectTimeState.value("%02d:%02d".format(hourOfDay, minute))
|
||||||
|
},
|
||||||
|
initialHour,
|
||||||
|
initialMinute,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
dialog.setOnDismissListener { dismissState.value() }
|
||||||
|
dialog.show()
|
||||||
|
onDispose {
|
||||||
|
dialog.setOnDismissListener(null)
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun formatBookingDurationText(
|
||||||
|
start: OffsetDateTime?,
|
||||||
|
end: OffsetDateTime?
|
||||||
|
): String? {
|
||||||
|
if (start == null || end == null || !end.isAfter(start)) return null
|
||||||
|
val totalMinutes = Duration.between(start, end).toMinutes()
|
||||||
|
val totalHours = totalMinutes / 60
|
||||||
|
val minutes = totalMinutes % 60
|
||||||
|
return if (totalHours >= 24) {
|
||||||
|
val days = totalHours / 24
|
||||||
|
val hoursLeft = totalHours % 24
|
||||||
|
"%02dd:%02dh left".format(days, hoursLeft)
|
||||||
|
} else if (totalHours > 0) {
|
||||||
|
"${totalHours}h ${minutes}m"
|
||||||
|
} else {
|
||||||
|
"${minutes}m"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
package com.android.trisolarispms.ui.booking
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.android.trisolarispms.data.api.core.ApiClient
|
|
||||||
import com.android.trisolarispms.data.api.model.BookingBillableNightsRequest
|
|
||||||
import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest
|
|
||||||
import com.android.trisolarispms.ui.common.PaddedScreenColumn
|
|
||||||
import com.android.trisolarispms.ui.common.SaveTopBarScaffold
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.time.LocalDate
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import java.time.ZoneId
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BookingExpectedDatesScreen(
|
|
||||||
propertyId: String,
|
|
||||||
bookingId: String,
|
|
||||||
status: String?,
|
|
||||||
expectedCheckInAt: String?,
|
|
||||||
expectedCheckOutAt: String?,
|
|
||||||
onBack: () -> Unit,
|
|
||||||
onDone: () -> Unit
|
|
||||||
) {
|
|
||||||
val checkInDate = remember { mutableStateOf<LocalDate?>(null) }
|
|
||||||
val checkOutDate = remember { mutableStateOf<LocalDate?>(null) }
|
|
||||||
val checkInTime = remember { mutableStateOf("12:00") }
|
|
||||||
val checkOutTime = remember { mutableStateOf("11:00") }
|
|
||||||
val isLoading = remember { mutableStateOf(false) }
|
|
||||||
val error = remember { mutableStateOf<String?>(null) }
|
|
||||||
val displayFormatter = remember { DateTimeFormatter.ofPattern("dd-MM-yy HH:mm") }
|
|
||||||
val displayZone = remember { ZoneId.of("Asia/Kolkata") }
|
|
||||||
val today = LocalDate.now(displayZone)
|
|
||||||
val bookingStatus = status?.uppercase()
|
|
||||||
val editableCheckIn = bookingStatus == "OPEN"
|
|
||||||
val billableNights = remember { mutableStateOf<Long?>(null) }
|
|
||||||
val isBillableNightsLoading = remember { mutableStateOf(false) }
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
LaunchedEffect(bookingId) {
|
|
||||||
val now = OffsetDateTime.now()
|
|
||||||
expectedCheckInAt?.let {
|
|
||||||
runCatching { OffsetDateTime.parse(it) }.getOrNull()?.let { parsed ->
|
|
||||||
val zoned = parsed.atZoneSameInstant(displayZone)
|
|
||||||
checkInDate.value = zoned.toLocalDate()
|
|
||||||
checkInTime.value = zoned.format(DateTimeFormatter.ofPattern("HH:mm"))
|
|
||||||
}
|
|
||||||
} ?: run {
|
|
||||||
checkInDate.value = now.toLocalDate()
|
|
||||||
checkInTime.value = now.format(DateTimeFormatter.ofPattern("HH:mm"))
|
|
||||||
}
|
|
||||||
expectedCheckOutAt?.let {
|
|
||||||
runCatching { OffsetDateTime.parse(it) }.getOrNull()?.let { parsed ->
|
|
||||||
val zoned = parsed.atZoneSameInstant(displayZone)
|
|
||||||
checkOutDate.value = zoned.toLocalDate()
|
|
||||||
checkOutTime.value = zoned.format(DateTimeFormatter.ofPattern("HH:mm"))
|
|
||||||
}
|
|
||||||
} ?: run {
|
|
||||||
checkOutDate.value = (checkInDate.value ?: now.toLocalDate()).plusDays(1)
|
|
||||||
checkOutTime.value = "11:00"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(
|
|
||||||
propertyId,
|
|
||||||
bookingId,
|
|
||||||
bookingStatus,
|
|
||||||
checkInDate.value,
|
|
||||||
checkInTime.value,
|
|
||||||
checkOutDate.value,
|
|
||||||
checkOutTime.value
|
|
||||||
) {
|
|
||||||
val inAt = if (editableCheckIn) {
|
|
||||||
checkInDate.value?.let { formatBookingIso(it, checkInTime.value) }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val outAt = checkOutDate.value?.let { formatBookingIso(it, checkOutTime.value) }
|
|
||||||
val previewBody = when (bookingStatus) {
|
|
||||||
"OPEN" -> {
|
|
||||||
if (inAt.isNullOrBlank() || outAt.isNullOrBlank()) null
|
|
||||||
else BookingBillableNightsRequest(expectedCheckInAt = inAt, expectedCheckOutAt = outAt)
|
|
||||||
}
|
|
||||||
"CHECKED_IN" -> {
|
|
||||||
if (outAt.isNullOrBlank()) null
|
|
||||||
else BookingBillableNightsRequest(expectedCheckOutAt = outAt)
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
if (previewBody == null) {
|
|
||||||
billableNights.value = null
|
|
||||||
isBillableNightsLoading.value = false
|
|
||||||
return@LaunchedEffect
|
|
||||||
}
|
|
||||||
isBillableNightsLoading.value = true
|
|
||||||
try {
|
|
||||||
val api = ApiClient.create()
|
|
||||||
val response = api.previewBillableNights(
|
|
||||||
propertyId = propertyId,
|
|
||||||
bookingId = bookingId,
|
|
||||||
body = previewBody
|
|
||||||
)
|
|
||||||
billableNights.value = if (response.isSuccessful) response.body()?.billableNights else null
|
|
||||||
} catch (_: Exception) {
|
|
||||||
billableNights.value = null
|
|
||||||
} finally {
|
|
||||||
isBillableNightsLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SaveTopBarScaffold(
|
|
||||||
title = "Update Expected Dates",
|
|
||||||
onBack = onBack,
|
|
||||||
saveEnabled = !isLoading.value,
|
|
||||||
onSave = {
|
|
||||||
val inAt = if (editableCheckIn) {
|
|
||||||
checkInDate.value?.let { formatBookingIso(it, checkInTime.value) }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val outAt = checkOutDate.value?.let { formatBookingIso(it, checkOutTime.value) }
|
|
||||||
val hasCheckInChanged = editableCheckIn && !isSameBookingDateTime(inAt, expectedCheckInAt)
|
|
||||||
val hasCheckOutChanged = !isSameBookingDateTime(outAt, expectedCheckOutAt)
|
|
||||||
if (!hasCheckInChanged && !hasCheckOutChanged) {
|
|
||||||
onDone()
|
|
||||||
return@SaveTopBarScaffold
|
|
||||||
}
|
|
||||||
isLoading.value = true
|
|
||||||
error.value = null
|
|
||||||
scope.launch {
|
|
||||||
try {
|
|
||||||
val api = ApiClient.create()
|
|
||||||
val response = api.updateExpectedDates(
|
|
||||||
propertyId = propertyId,
|
|
||||||
bookingId = bookingId,
|
|
||||||
body = BookingExpectedDatesRequest(
|
|
||||||
expectedCheckInAt = inAt,
|
|
||||||
expectedCheckOutAt = outAt
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (response.isSuccessful) {
|
|
||||||
onDone()
|
|
||||||
} else {
|
|
||||||
error.value = "Update failed: ${response.code()}"
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
error.value = e.localizedMessage ?: "Update failed"
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) { padding ->
|
|
||||||
PaddedScreenColumn(padding = padding) {
|
|
||||||
if (editableCheckIn) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = checkInDate.value?.let {
|
|
||||||
formatBookingIso(it, checkInTime.value)
|
|
||||||
}?.let { iso ->
|
|
||||||
runCatching {
|
|
||||||
OffsetDateTime.parse(iso).atZoneSameInstant(displayZone).format(displayFormatter)
|
|
||||||
}.getOrDefault(iso)
|
|
||||||
}.orEmpty(),
|
|
||||||
onValueChange = {},
|
|
||||||
readOnly = true,
|
|
||||||
label = { Text("Expected Check-in") },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
BookingDateTimePickerInline(
|
|
||||||
title = "Select check-in",
|
|
||||||
initialDate = checkInDate.value,
|
|
||||||
initialTime = checkInTime.value,
|
|
||||||
minDate = today,
|
|
||||||
onValueChange = { date, time ->
|
|
||||||
checkInDate.value = date
|
|
||||||
checkInTime.value = time
|
|
||||||
if (checkOutDate.value?.isBefore(date) == true) {
|
|
||||||
checkOutDate.value = date
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
}
|
|
||||||
OutlinedTextField(
|
|
||||||
value = checkOutDate.value?.let {
|
|
||||||
formatBookingIso(it, checkOutTime.value)
|
|
||||||
}?.let { iso ->
|
|
||||||
runCatching {
|
|
||||||
OffsetDateTime.parse(iso).atZoneSameInstant(displayZone).format(displayFormatter)
|
|
||||||
}.getOrDefault(iso)
|
|
||||||
}.orEmpty(),
|
|
||||||
onValueChange = {},
|
|
||||||
readOnly = true,
|
|
||||||
label = { Text("Expected Check-out") },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.45f),
|
|
||||||
shape = MaterialTheme.shapes.small
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = if (isBillableNightsLoading.value) {
|
|
||||||
"Billable Nights: Calculating..."
|
|
||||||
} else {
|
|
||||||
"Billable Nights: ${billableNights.value?.toString() ?: "-"}"
|
|
||||||
},
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val checkOutMinDate = maxOf(checkInDate.value ?: today, today)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
BookingDateTimePickerInline(
|
|
||||||
title = "Select check-out",
|
|
||||||
initialDate = checkOutDate.value,
|
|
||||||
initialTime = checkOutTime.value,
|
|
||||||
minDate = checkOutMinDate,
|
|
||||||
onValueChange = { date, time ->
|
|
||||||
checkOutDate.value = date
|
|
||||||
checkOutTime.value = time
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (isLoading.value) {
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
error.value?.let {
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isSameBookingDateTime(current: String?, original: String?): Boolean {
|
|
||||||
if (current.isNullOrBlank() && original.isNullOrBlank()) return true
|
|
||||||
if (current.isNullOrBlank() || original.isNullOrBlank()) return false
|
|
||||||
val currentInstant = runCatching { OffsetDateTime.parse(current).toInstant() }.getOrNull()
|
|
||||||
val originalInstant = runCatching { OffsetDateTime.parse(original).toInstant() }.getOrNull()
|
|
||||||
return if (currentInstant != null && originalInstant != null) {
|
|
||||||
currentInstant == originalInstant
|
|
||||||
} else {
|
|
||||||
current.trim() == original.trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,13 +35,6 @@ sealed interface AppRoute {
|
|||||||
val propertyId: String,
|
val propertyId: String,
|
||||||
val bookingId: String
|
val bookingId: String
|
||||||
) : AppRoute
|
) : AppRoute
|
||||||
data class BookingExpectedDates(
|
|
||||||
val propertyId: String,
|
|
||||||
val bookingId: String,
|
|
||||||
val status: String?,
|
|
||||||
val expectedCheckInAt: String?,
|
|
||||||
val expectedCheckOutAt: String?
|
|
||||||
) : AppRoute
|
|
||||||
data class BookingDetailsTabs(
|
data class BookingDetailsTabs(
|
||||||
val propertyId: String,
|
val propertyId: String,
|
||||||
val bookingId: String,
|
val bookingId: String,
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ internal fun handleBackNavigation(
|
|||||||
currentRoute.toAt
|
currentRoute.toAt
|
||||||
)
|
)
|
||||||
is AppRoute.BookingRoomStays -> refs.openActiveRoomStays(currentRoute.propertyId)
|
is AppRoute.BookingRoomStays -> refs.openActiveRoomStays(currentRoute.propertyId)
|
||||||
is AppRoute.BookingExpectedDates -> refs.openActiveRoomStays(currentRoute.propertyId)
|
|
||||||
is AppRoute.BookingDetailsTabs -> refs.openActiveRoomStays(currentRoute.propertyId)
|
is AppRoute.BookingDetailsTabs -> refs.openActiveRoomStays(currentRoute.propertyId)
|
||||||
is AppRoute.BookingPayments -> refs.route.value = AppRoute.BookingDetailsTabs(
|
is AppRoute.BookingPayments -> refs.route.value = AppRoute.BookingDetailsTabs(
|
||||||
currentRoute.propertyId,
|
currentRoute.propertyId,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.android.trisolarispms.ui.navigation
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import com.android.trisolarispms.core.auth.AuthzPolicy
|
import com.android.trisolarispms.core.auth.AuthzPolicy
|
||||||
import com.android.trisolarispms.ui.navigation.AppRoute
|
import com.android.trisolarispms.ui.navigation.AppRoute
|
||||||
import com.android.trisolarispms.ui.booking.BookingExpectedDatesScreen
|
|
||||||
import com.android.trisolarispms.ui.payment.BookingPaymentsScreen
|
import com.android.trisolarispms.ui.payment.BookingPaymentsScreen
|
||||||
import com.android.trisolarispms.ui.roomstay.BookingDetailsTabsScreen
|
import com.android.trisolarispms.ui.roomstay.BookingDetailsTabsScreen
|
||||||
import com.android.trisolarispms.ui.roomstay.BookingRoomStaysScreen
|
import com.android.trisolarispms.ui.roomstay.BookingRoomStaysScreen
|
||||||
@@ -20,30 +19,11 @@ internal fun renderBookingRoutes(
|
|||||||
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) }
|
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) }
|
||||||
)
|
)
|
||||||
|
|
||||||
is AppRoute.BookingExpectedDates -> BookingExpectedDatesScreen(
|
|
||||||
propertyId = currentRoute.propertyId,
|
|
||||||
bookingId = currentRoute.bookingId,
|
|
||||||
status = currentRoute.status,
|
|
||||||
expectedCheckInAt = currentRoute.expectedCheckInAt,
|
|
||||||
expectedCheckOutAt = currentRoute.expectedCheckOutAt,
|
|
||||||
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) },
|
|
||||||
onDone = { refs.openActiveRoomStays(currentRoute.propertyId) }
|
|
||||||
)
|
|
||||||
|
|
||||||
is AppRoute.BookingDetailsTabs -> BookingDetailsTabsScreen(
|
is AppRoute.BookingDetailsTabs -> BookingDetailsTabsScreen(
|
||||||
propertyId = currentRoute.propertyId,
|
propertyId = currentRoute.propertyId,
|
||||||
bookingId = currentRoute.bookingId,
|
bookingId = currentRoute.bookingId,
|
||||||
guestId = currentRoute.guestId,
|
guestId = currentRoute.guestId,
|
||||||
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) },
|
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) },
|
||||||
onEditCheckout = { expectedCheckInAt, expectedCheckOutAt ->
|
|
||||||
refs.route.value = AppRoute.BookingExpectedDates(
|
|
||||||
propertyId = currentRoute.propertyId,
|
|
||||||
bookingId = currentRoute.bookingId,
|
|
||||||
status = "CHECKED_IN",
|
|
||||||
expectedCheckInAt = expectedCheckInAt,
|
|
||||||
expectedCheckOutAt = expectedCheckOutAt
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onEditSignature = { guestId ->
|
onEditSignature = { guestId ->
|
||||||
refs.route.value = AppRoute.GuestSignature(
|
refs.route.value = AppRoute.GuestSignature(
|
||||||
currentRoute.propertyId,
|
currentRoute.propertyId,
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import androidx.compose.foundation.pager.rememberPagerState
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Edit
|
|
||||||
import androidx.compose.material.icons.filled.QrCode
|
import androidx.compose.material.icons.filled.QrCode
|
||||||
import androidx.compose.material.icons.filled.ReceiptLong
|
import androidx.compose.material.icons.filled.ReceiptLong
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
@@ -33,10 +32,11 @@ import androidx.compose.material3.TabRow
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -49,10 +49,17 @@ import coil.ImageLoader
|
|||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil.decode.SvgDecoder
|
import coil.decode.SvgDecoder
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
|
import com.android.trisolarispms.data.api.core.ApiClient
|
||||||
import com.android.trisolarispms.data.api.core.ApiConstants
|
import com.android.trisolarispms.data.api.core.ApiConstants
|
||||||
import com.android.trisolarispms.data.api.core.FirebaseAuthTokenProvider
|
import com.android.trisolarispms.data.api.core.FirebaseAuthTokenProvider
|
||||||
import com.android.trisolarispms.data.api.model.BookingBillingMode
|
import com.android.trisolarispms.data.api.model.BookingBillingMode
|
||||||
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
|
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
|
||||||
|
import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest
|
||||||
|
import com.android.trisolarispms.ui.booking.BookingDatePickerDialog
|
||||||
|
import com.android.trisolarispms.ui.booking.BookingDateTimeQuickEditorCard
|
||||||
|
import com.android.trisolarispms.ui.booking.BookingTimePickerDialog
|
||||||
|
import com.android.trisolarispms.ui.booking.formatBookingIso
|
||||||
|
import com.android.trisolarispms.ui.booking.formatBookingDurationText
|
||||||
import com.android.trisolarispms.ui.common.BackTopBarScaffold
|
import com.android.trisolarispms.ui.common.BackTopBarScaffold
|
||||||
import com.android.trisolarispms.ui.guestdocs.GuestDocumentsTab
|
import com.android.trisolarispms.ui.guestdocs.GuestDocumentsTab
|
||||||
import com.google.firebase.auth.FirebaseAuth
|
import com.google.firebase.auth.FirebaseAuth
|
||||||
@@ -72,7 +79,6 @@ fun BookingDetailsTabsScreen(
|
|||||||
bookingId: String,
|
bookingId: String,
|
||||||
guestId: String?,
|
guestId: String?,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onEditCheckout: (String?, String?) -> Unit,
|
|
||||||
onEditSignature: (String) -> Unit,
|
onEditSignature: (String) -> Unit,
|
||||||
onOpenRazorpayQr: (Long?, String?) -> Unit,
|
onOpenRazorpayQr: (Long?, String?) -> Unit,
|
||||||
onOpenPayments: () -> Unit,
|
onOpenPayments: () -> Unit,
|
||||||
@@ -140,11 +146,11 @@ fun BookingDetailsTabsScreen(
|
|||||||
when (page) {
|
when (page) {
|
||||||
0 -> GuestInfoTabContent(
|
0 -> GuestInfoTabContent(
|
||||||
propertyId = propertyId,
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
details = detailsState.details,
|
details = detailsState.details,
|
||||||
guestId = guestId,
|
guestId = guestId,
|
||||||
isLoading = detailsState.isLoading,
|
isLoading = detailsState.isLoading,
|
||||||
error = detailsState.error,
|
error = detailsState.error,
|
||||||
onEditCheckout = onEditCheckout,
|
|
||||||
onEditSignature = onEditSignature,
|
onEditSignature = onEditSignature,
|
||||||
onOpenRazorpayQr = onOpenRazorpayQr,
|
onOpenRazorpayQr = onOpenRazorpayQr,
|
||||||
onOpenPayments = onOpenPayments
|
onOpenPayments = onOpenPayments
|
||||||
@@ -178,11 +184,11 @@ fun BookingDetailsTabsScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun GuestInfoTabContent(
|
private fun GuestInfoTabContent(
|
||||||
propertyId: String,
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
details: BookingDetailsResponse?,
|
details: BookingDetailsResponse?,
|
||||||
guestId: String?,
|
guestId: String?,
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
error: String?,
|
error: String?,
|
||||||
onEditCheckout: (String?, String?) -> Unit,
|
|
||||||
onEditSignature: (String) -> Unit,
|
onEditSignature: (String) -> Unit,
|
||||||
onOpenRazorpayQr: (Long?, String?) -> Unit,
|
onOpenRazorpayQr: (Long?, String?) -> Unit,
|
||||||
onOpenPayments: () -> Unit
|
onOpenPayments: () -> Unit
|
||||||
@@ -190,6 +196,67 @@ private fun GuestInfoTabContent(
|
|||||||
val displayZone = remember { ZoneId.of("Asia/Kolkata") }
|
val displayZone = remember { ZoneId.of("Asia/Kolkata") }
|
||||||
val dateFormatter = remember { DateTimeFormatter.ofPattern("dd/MM/yyyy") }
|
val dateFormatter = remember { DateTimeFormatter.ofPattern("dd/MM/yyyy") }
|
||||||
val timeFormatter = remember { DateTimeFormatter.ofPattern("hh:mm a") }
|
val timeFormatter = remember { DateTimeFormatter.ofPattern("hh:mm a") }
|
||||||
|
val pickerTimeFormatter = remember { DateTimeFormatter.ofPattern("HH:mm") }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val showCheckInDatePicker = remember { mutableStateOf(false) }
|
||||||
|
val showCheckInTimePicker = remember { mutableStateOf(false) }
|
||||||
|
val showCheckOutDatePicker = remember { mutableStateOf(false) }
|
||||||
|
val showCheckOutTimePicker = remember { mutableStateOf(false) }
|
||||||
|
val isUpdatingDates = remember { mutableStateOf(false) }
|
||||||
|
val updateDatesError = remember { mutableStateOf<String?>(null) }
|
||||||
|
val draftCheckInAt = remember { mutableStateOf<OffsetDateTime?>(null) }
|
||||||
|
val draftCheckOutAt = remember { mutableStateOf<OffsetDateTime?>(null) }
|
||||||
|
val today = remember(displayZone) { LocalDate.now(displayZone) }
|
||||||
|
|
||||||
|
val checkInFromDetails = details?.checkInAt ?: details?.expectedCheckInAt
|
||||||
|
val checkOutFromDetails = details?.expectedCheckOutAt ?: details?.checkOutAt
|
||||||
|
val bookingStatus = details?.status?.uppercase()
|
||||||
|
val canEditCheckIn = bookingStatus == "OPEN"
|
||||||
|
val canEditCheckOut = bookingStatus == "OPEN" || bookingStatus == "CHECKED_IN"
|
||||||
|
|
||||||
|
LaunchedEffect(checkInFromDetails, checkOutFromDetails) {
|
||||||
|
draftCheckInAt.value = checkInFromDetails
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?.let { runCatching { OffsetDateTime.parse(it) }.getOrNull() }
|
||||||
|
draftCheckOutAt.value = checkOutFromDetails
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?.let { runCatching { OffsetDateTime.parse(it) }.getOrNull() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun submitExpectedDatesUpdate(updatedCheckInAt: OffsetDateTime?, updatedCheckOutAt: OffsetDateTime?) {
|
||||||
|
val bookingStatus = details?.status?.uppercase() ?: return
|
||||||
|
if (bookingStatus != "OPEN" && bookingStatus != "CHECKED_IN") return
|
||||||
|
scope.launch {
|
||||||
|
isUpdatingDates.value = true
|
||||||
|
updateDatesError.value = null
|
||||||
|
try {
|
||||||
|
val body = when (bookingStatus) {
|
||||||
|
"OPEN" -> BookingExpectedDatesRequest(
|
||||||
|
expectedCheckInAt = updatedCheckInAt?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
|
||||||
|
expectedCheckOutAt = updatedCheckOutAt?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
|
||||||
|
)
|
||||||
|
else -> BookingExpectedDatesRequest(
|
||||||
|
expectedCheckOutAt = updatedCheckOutAt?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val response = ApiClient.create().updateExpectedDates(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
body = body
|
||||||
|
)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
draftCheckInAt.value = updatedCheckInAt
|
||||||
|
draftCheckOutAt.value = updatedCheckOutAt
|
||||||
|
} else {
|
||||||
|
updateDatesError.value = "Update failed: ${response.code()}"
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
updateDatesError.value = e.localizedMessage ?: "Update failed"
|
||||||
|
} finally {
|
||||||
|
isUpdatingDates.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -221,37 +288,46 @@ private fun GuestInfoTabContent(
|
|||||||
GuestDetailRow(label = "Mode of transport", value = details?.transportMode)
|
GuestDetailRow(label = "Mode of transport", value = details?.transportMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
val checkIn = details?.checkInAt ?: details?.expectedCheckInAt
|
|
||||||
val checkOut = details?.expectedCheckOutAt ?: details?.checkOutAt
|
|
||||||
SectionCard(title = "Stay") {
|
SectionCard(title = "Stay") {
|
||||||
if (!checkIn.isNullOrBlank()) {
|
val parsedCheckIn = draftCheckInAt.value
|
||||||
val parsed = runCatching { OffsetDateTime.parse(checkIn).atZoneSameInstant(displayZone) }.getOrNull()
|
val parsedCheckOut = draftCheckOutAt.value
|
||||||
GuestDetailRow(
|
val zonedCheckIn = parsedCheckIn?.atZoneSameInstant(displayZone)
|
||||||
label = "Check In Time",
|
val zonedCheckOut = parsedCheckOut?.atZoneSameInstant(displayZone)
|
||||||
value = parsed?.let { "${it.format(dateFormatter)} ${it.format(timeFormatter)}" }
|
if (zonedCheckIn != null || zonedCheckOut != null) {
|
||||||
|
BookingDateTimeQuickEditorCard(
|
||||||
|
checkInDateText = zonedCheckIn?.format(dateFormatter) ?: "--/--/----",
|
||||||
|
checkInTimeText = zonedCheckIn?.format(timeFormatter) ?: "--:--",
|
||||||
|
checkOutDateText = zonedCheckOut?.format(dateFormatter) ?: "--/--/----",
|
||||||
|
checkOutTimeText = zonedCheckOut?.format(timeFormatter) ?: "--:--",
|
||||||
|
totalTimeText = formatBookingDurationText(parsedCheckIn, parsedCheckOut),
|
||||||
|
checkInEditable = canEditCheckIn,
|
||||||
|
checkOutEditable = canEditCheckOut,
|
||||||
|
onCheckInDateClick = {
|
||||||
|
if (canEditCheckIn) showCheckInDatePicker.value = true
|
||||||
|
},
|
||||||
|
onCheckInTimeClick = {
|
||||||
|
if (canEditCheckIn) showCheckInTimePicker.value = true
|
||||||
|
},
|
||||||
|
onCheckOutDateClick = {
|
||||||
|
if (canEditCheckOut) showCheckOutDatePicker.value = true
|
||||||
|
},
|
||||||
|
onCheckOutTimeClick = {
|
||||||
|
if (canEditCheckOut) showCheckOutTimePicker.value = true
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
if (isUpdatingDates.value) {
|
||||||
if (!checkOut.isNullOrBlank()) {
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
val parsed = runCatching { OffsetDateTime.parse(checkOut).atZoneSameInstant(displayZone) }.getOrNull()
|
CircularProgressIndicator()
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
GuestDetailRow(
|
|
||||||
label = "Estimated Check Out Time",
|
|
||||||
value = parsed?.let { "${it.format(dateFormatter)} ${it.format(timeFormatter)}" }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
onEditCheckout(details?.expectedCheckInAt, details?.expectedCheckOutAt)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.Edit, contentDescription = "Edit checkout")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
updateDatesError.value?.let { message ->
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
val billingMode = BookingBillingMode.from(details?.billingMode)
|
val billingMode = BookingBillingMode.from(details?.billingMode)
|
||||||
GuestDetailRow(label = "Billing Mode", value = details?.billingMode)
|
GuestDetailRow(label = "Billing Mode", value = details?.billingMode)
|
||||||
@@ -325,6 +401,106 @@ private fun GuestInfoTabContent(
|
|||||||
onEditSignature = onEditSignature
|
onEditSignature = onEditSignature
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showCheckInDatePicker.value && canEditCheckIn) {
|
||||||
|
BookingDatePickerDialog(
|
||||||
|
initialDate = draftCheckInAt.value?.atZoneSameInstant(displayZone)?.toLocalDate() ?: today,
|
||||||
|
minDate = today,
|
||||||
|
onDismiss = { showCheckInDatePicker.value = false },
|
||||||
|
onDateSelected = { selectedDate ->
|
||||||
|
val selectedTime = draftCheckInAt.value
|
||||||
|
?.atZoneSameInstant(displayZone)
|
||||||
|
?.format(pickerTimeFormatter)
|
||||||
|
?: "12:00"
|
||||||
|
val updatedCheckIn = runCatching {
|
||||||
|
OffsetDateTime.parse(formatBookingIso(selectedDate, selectedTime))
|
||||||
|
}.getOrNull() ?: return@BookingDatePickerDialog
|
||||||
|
val currentCheckOut = draftCheckOutAt.value
|
||||||
|
val adjustedCheckOut = if (currentCheckOut != null && !currentCheckOut.isAfter(updatedCheckIn)) {
|
||||||
|
runCatching {
|
||||||
|
OffsetDateTime.parse(formatBookingIso(selectedDate.plusDays(1), selectedTime))
|
||||||
|
}.getOrNull()
|
||||||
|
} else {
|
||||||
|
currentCheckOut
|
||||||
|
}
|
||||||
|
submitExpectedDatesUpdate(updatedCheckIn, adjustedCheckOut)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showCheckInTimePicker.value && canEditCheckIn) {
|
||||||
|
BookingTimePickerDialog(
|
||||||
|
initialTime = draftCheckInAt.value
|
||||||
|
?.atZoneSameInstant(displayZone)
|
||||||
|
?.format(pickerTimeFormatter)
|
||||||
|
?: "12:00",
|
||||||
|
onDismiss = { showCheckInTimePicker.value = false },
|
||||||
|
onTimeSelected = { selectedTime ->
|
||||||
|
val selectedDate = draftCheckInAt.value?.atZoneSameInstant(displayZone)?.toLocalDate() ?: today
|
||||||
|
val updatedCheckIn = runCatching {
|
||||||
|
OffsetDateTime.parse(formatBookingIso(selectedDate, selectedTime))
|
||||||
|
}.getOrNull() ?: return@BookingTimePickerDialog
|
||||||
|
val currentCheckOut = draftCheckOutAt.value
|
||||||
|
val adjustedCheckOut = if (currentCheckOut != null && !currentCheckOut.isAfter(updatedCheckIn)) {
|
||||||
|
runCatching {
|
||||||
|
OffsetDateTime.parse(formatBookingIso(selectedDate.plusDays(1), selectedTime))
|
||||||
|
}.getOrNull()
|
||||||
|
} else {
|
||||||
|
currentCheckOut
|
||||||
|
}
|
||||||
|
submitExpectedDatesUpdate(updatedCheckIn, adjustedCheckOut)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showCheckOutDatePicker.value && canEditCheckOut) {
|
||||||
|
val checkInDate = draftCheckInAt.value?.atZoneSameInstant(displayZone)?.toLocalDate()
|
||||||
|
val checkoutMinDate = maxOf(checkInDate ?: today, today)
|
||||||
|
BookingDatePickerDialog(
|
||||||
|
initialDate = draftCheckOutAt.value?.atZoneSameInstant(displayZone)?.toLocalDate() ?: checkoutMinDate,
|
||||||
|
minDate = checkoutMinDate,
|
||||||
|
onDismiss = { showCheckOutDatePicker.value = false },
|
||||||
|
onDateSelected = { selectedDate ->
|
||||||
|
val selectedTime = draftCheckOutAt.value
|
||||||
|
?.atZoneSameInstant(displayZone)
|
||||||
|
?.format(pickerTimeFormatter)
|
||||||
|
?: "11:00"
|
||||||
|
val updatedCheckOut = runCatching {
|
||||||
|
OffsetDateTime.parse(formatBookingIso(selectedDate, selectedTime))
|
||||||
|
}.getOrNull() ?: return@BookingDatePickerDialog
|
||||||
|
val currentCheckIn = draftCheckInAt.value
|
||||||
|
if (currentCheckIn != null && !updatedCheckOut.isAfter(currentCheckIn)) {
|
||||||
|
updateDatesError.value = "Check-out must be after check-in"
|
||||||
|
return@BookingDatePickerDialog
|
||||||
|
}
|
||||||
|
submitExpectedDatesUpdate(currentCheckIn, updatedCheckOut)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showCheckOutTimePicker.value && canEditCheckOut) {
|
||||||
|
BookingTimePickerDialog(
|
||||||
|
initialTime = draftCheckOutAt.value
|
||||||
|
?.atZoneSameInstant(displayZone)
|
||||||
|
?.format(pickerTimeFormatter)
|
||||||
|
?: "11:00",
|
||||||
|
onDismiss = { showCheckOutTimePicker.value = false },
|
||||||
|
onTimeSelected = { selectedTime ->
|
||||||
|
val selectedDate = draftCheckOutAt.value?.atZoneSameInstant(displayZone)?.toLocalDate()
|
||||||
|
?: draftCheckInAt.value?.atZoneSameInstant(displayZone)?.toLocalDate()
|
||||||
|
?: today
|
||||||
|
val updatedCheckOut = runCatching {
|
||||||
|
OffsetDateTime.parse(formatBookingIso(selectedDate, selectedTime))
|
||||||
|
}.getOrNull() ?: return@BookingTimePickerDialog
|
||||||
|
val currentCheckIn = draftCheckInAt.value
|
||||||
|
if (currentCheckIn != null && !updatedCheckOut.isAfter(currentCheckIn)) {
|
||||||
|
updateDatesError.value = "Check-out must be after check-in"
|
||||||
|
return@BookingTimePickerDialog
|
||||||
|
}
|
||||||
|
submitExpectedDatesUpdate(currentCheckIn, updatedCheckOut)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
Reference in New Issue
Block a user