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.text.KeyboardOptions
|
||||
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.DropdownMenuItem
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Switch
|
||||
@@ -51,8 +47,10 @@ fun BookingCreateScreen(
|
||||
viewModel: BookingCreateViewModel = viewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val showCheckInPicker = remember { mutableStateOf(false) }
|
||||
val showCheckOutPicker = remember { mutableStateOf(false) }
|
||||
val showCheckInDatePicker = 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 checkOutDate = remember { mutableStateOf<LocalDate?>(null) }
|
||||
val checkInTime = remember { mutableStateOf("12:00") }
|
||||
@@ -76,6 +74,12 @@ fun BookingCreateScreen(
|
||||
checkInTime.value = time
|
||||
val checkInAt = formatBookingIso(date, time)
|
||||
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.refreshExpectedCheckoutPreview(propertyId)
|
||||
}
|
||||
@@ -124,43 +128,35 @@ fun BookingCreateScreen(
|
||||
if (enabled) {
|
||||
val now = OffsetDateTime.now()
|
||||
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))
|
||||
OutlinedTextField(
|
||||
value = state.expectedCheckOutAt.takeIf { it.isNotBlank() }?.let {
|
||||
runCatching { OffsetDateTime.parse(it).format(displayFormatter) }.getOrDefault(it)
|
||||
}.orEmpty(),
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Expected Check-out") },
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { showCheckOutPicker.value = true }) {
|
||||
Icon(Icons.Default.CalendarMonth, contentDescription = "Pick date")
|
||||
val checkInDisplay = state.expectedCheckInAt.takeIf { it.isNotBlank() }?.let {
|
||||
runCatching { OffsetDateTime.parse(it) }.getOrNull()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
val checkOutDisplay = state.expectedCheckOutAt.takeIf { it.isNotBlank() }?.let {
|
||||
runCatching { OffsetDateTime.parse(it) }.getOrNull()
|
||||
}
|
||||
val cardDateFormatter = remember { DateTimeFormatter.ofPattern("dd/MM/yyyy") }
|
||||
val cardTimeFormatter = remember { DateTimeFormatter.ofPattern("hh:mm a") }
|
||||
val totalTimeText = remember(state.expectedCheckInAt, state.expectedCheckOutAt) {
|
||||
val start = runCatching { OffsetDateTime.parse(state.expectedCheckInAt) }.getOrNull()
|
||||
val end = runCatching { OffsetDateTime.parse(state.expectedCheckOutAt) }.getOrNull()
|
||||
formatBookingDurationText(start, end)
|
||||
}
|
||||
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))
|
||||
ExposedDropdownMenuBox(
|
||||
@@ -485,33 +481,50 @@ fun BookingCreateScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (showCheckInPicker.value) {
|
||||
BookingDateTimePickerDialog(
|
||||
title = "Select check-in",
|
||||
initialDate = checkInDate.value,
|
||||
initialTime = checkInTime.value,
|
||||
if (showCheckInDatePicker.value) {
|
||||
BookingDatePickerDialog(
|
||||
initialDate = checkInDate.value ?: LocalDate.now(),
|
||||
minDate = LocalDate.now(),
|
||||
onDismiss = { showCheckInPicker.value = false },
|
||||
onConfirm = { date, time ->
|
||||
applyCheckInSelection(date, time)
|
||||
showCheckInPicker.value = false
|
||||
onDismiss = { showCheckInDatePicker.value = false },
|
||||
onDateSelected = { selectedDate ->
|
||||
applyCheckInSelection(selectedDate, checkInTime.value)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showCheckOutPicker.value) {
|
||||
BookingDateTimePickerDialog(
|
||||
title = "Select check-out",
|
||||
initialDate = checkOutDate.value,
|
||||
initialTime = checkOutTime.value,
|
||||
if (showCheckInTimePicker.value) {
|
||||
BookingTimePickerDialog(
|
||||
initialTime = checkInTime.value,
|
||||
onDismiss = { showCheckInTimePicker.value = false },
|
||||
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(),
|
||||
onDismiss = { showCheckOutPicker.value = false },
|
||||
onConfirm = { date, time ->
|
||||
checkOutDate.value = date
|
||||
checkOutTime.value = time
|
||||
val formatted = formatBookingIso(date, time)
|
||||
onDismiss = { showCheckOutDatePicker.value = false },
|
||||
onDateSelected = { selectedDate ->
|
||||
checkOutDate.value = selectedDate
|
||||
val formatted = formatBookingIso(selectedDate, checkOutTime.value)
|
||||
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)
|
||||
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 bookingId: String
|
||||
) : AppRoute
|
||||
data class BookingExpectedDates(
|
||||
val propertyId: String,
|
||||
val bookingId: String,
|
||||
val status: String?,
|
||||
val expectedCheckInAt: String?,
|
||||
val expectedCheckOutAt: String?
|
||||
) : AppRoute
|
||||
data class BookingDetailsTabs(
|
||||
val propertyId: String,
|
||||
val bookingId: String,
|
||||
|
||||
@@ -72,7 +72,6 @@ internal fun handleBackNavigation(
|
||||
currentRoute.toAt
|
||||
)
|
||||
is AppRoute.BookingRoomStays -> refs.openActiveRoomStays(currentRoute.propertyId)
|
||||
is AppRoute.BookingExpectedDates -> refs.openActiveRoomStays(currentRoute.propertyId)
|
||||
is AppRoute.BookingDetailsTabs -> refs.openActiveRoomStays(currentRoute.propertyId)
|
||||
is AppRoute.BookingPayments -> refs.route.value = AppRoute.BookingDetailsTabs(
|
||||
currentRoute.propertyId,
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.android.trisolarispms.ui.navigation
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.android.trisolarispms.core.auth.AuthzPolicy
|
||||
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.roomstay.BookingDetailsTabsScreen
|
||||
import com.android.trisolarispms.ui.roomstay.BookingRoomStaysScreen
|
||||
@@ -20,30 +19,11 @@ internal fun renderBookingRoutes(
|
||||
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(
|
||||
propertyId = currentRoute.propertyId,
|
||||
bookingId = currentRoute.bookingId,
|
||||
guestId = currentRoute.guestId,
|
||||
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 ->
|
||||
refs.route.value = AppRoute.GuestSignature(
|
||||
currentRoute.propertyId,
|
||||
|
||||
@@ -18,7 +18,6 @@ import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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.ReceiptLong
|
||||
import androidx.compose.material3.Card
|
||||
@@ -33,10 +32,11 @@ import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -49,10 +49,17 @@ import coil.ImageLoader
|
||||
import coil.compose.AsyncImage
|
||||
import coil.decode.SvgDecoder
|
||||
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.FirebaseAuthTokenProvider
|
||||
import com.android.trisolarispms.data.api.model.BookingBillingMode
|
||||
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.guestdocs.GuestDocumentsTab
|
||||
import com.google.firebase.auth.FirebaseAuth
|
||||
@@ -72,7 +79,6 @@ fun BookingDetailsTabsScreen(
|
||||
bookingId: String,
|
||||
guestId: String?,
|
||||
onBack: () -> Unit,
|
||||
onEditCheckout: (String?, String?) -> Unit,
|
||||
onEditSignature: (String) -> Unit,
|
||||
onOpenRazorpayQr: (Long?, String?) -> Unit,
|
||||
onOpenPayments: () -> Unit,
|
||||
@@ -140,11 +146,11 @@ fun BookingDetailsTabsScreen(
|
||||
when (page) {
|
||||
0 -> GuestInfoTabContent(
|
||||
propertyId = propertyId,
|
||||
bookingId = bookingId,
|
||||
details = detailsState.details,
|
||||
guestId = guestId,
|
||||
isLoading = detailsState.isLoading,
|
||||
error = detailsState.error,
|
||||
onEditCheckout = onEditCheckout,
|
||||
onEditSignature = onEditSignature,
|
||||
onOpenRazorpayQr = onOpenRazorpayQr,
|
||||
onOpenPayments = onOpenPayments
|
||||
@@ -178,11 +184,11 @@ fun BookingDetailsTabsScreen(
|
||||
@Composable
|
||||
private fun GuestInfoTabContent(
|
||||
propertyId: String,
|
||||
bookingId: String,
|
||||
details: BookingDetailsResponse?,
|
||||
guestId: String?,
|
||||
isLoading: Boolean,
|
||||
error: String?,
|
||||
onEditCheckout: (String?, String?) -> Unit,
|
||||
onEditSignature: (String) -> Unit,
|
||||
onOpenRazorpayQr: (Long?, String?) -> Unit,
|
||||
onOpenPayments: () -> Unit
|
||||
@@ -190,6 +196,67 @@ private fun GuestInfoTabContent(
|
||||
val displayZone = remember { ZoneId.of("Asia/Kolkata") }
|
||||
val dateFormatter = remember { DateTimeFormatter.ofPattern("dd/MM/yyyy") }
|
||||
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(
|
||||
modifier = Modifier
|
||||
@@ -221,37 +288,46 @@ private fun GuestInfoTabContent(
|
||||
GuestDetailRow(label = "Mode of transport", value = details?.transportMode)
|
||||
}
|
||||
|
||||
val checkIn = details?.checkInAt ?: details?.expectedCheckInAt
|
||||
val checkOut = details?.expectedCheckOutAt ?: details?.checkOutAt
|
||||
SectionCard(title = "Stay") {
|
||||
if (!checkIn.isNullOrBlank()) {
|
||||
val parsed = runCatching { OffsetDateTime.parse(checkIn).atZoneSameInstant(displayZone) }.getOrNull()
|
||||
GuestDetailRow(
|
||||
label = "Check In Time",
|
||||
value = parsed?.let { "${it.format(dateFormatter)} ${it.format(timeFormatter)}" }
|
||||
val parsedCheckIn = draftCheckInAt.value
|
||||
val parsedCheckOut = draftCheckOutAt.value
|
||||
val zonedCheckIn = parsedCheckIn?.atZoneSameInstant(displayZone)
|
||||
val zonedCheckOut = parsedCheckOut?.atZoneSameInstant(displayZone)
|
||||
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) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
updateDatesError.value?.let { message ->
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
if (!checkOut.isNullOrBlank()) {
|
||||
val parsed = runCatching { OffsetDateTime.parse(checkOut).atZoneSameInstant(displayZone) }.getOrNull()
|
||||
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")
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
val billingMode = BookingBillingMode.from(details?.billingMode)
|
||||
GuestDetailRow(label = "Billing Mode", value = details?.billingMode)
|
||||
@@ -325,6 +401,106 @@ private fun GuestInfoTabContent(
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user