diff --git a/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateScreen.kt index 90d48ea..ecd5184 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateScreen.kt @@ -1,7 +1,6 @@ package com.android.trisolarispms.ui.booking import android.app.TimePickerDialog -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -11,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll @@ -20,7 +18,6 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.CalendarMonth import androidx.compose.material.icons.filled.Schedule import androidx.compose.material.icons.filled.Done -import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExposedDropdownMenuBox @@ -33,7 +30,6 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable @@ -50,17 +46,8 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.android.trisolarispms.data.api.model.BookingBillingMode -import com.kizitonwose.calendar.compose.HorizontalCalendar -import com.kizitonwose.calendar.compose.rememberCalendarState -import com.kizitonwose.calendar.core.CalendarDay -import com.kizitonwose.calendar.core.CalendarMonth -import com.kizitonwose.calendar.core.DayPosition -import com.kizitonwose.calendar.core.daysOfWeek import java.time.LocalDate -import java.time.LocalDateTime import java.time.OffsetDateTime -import java.time.YearMonth -import java.time.ZoneId import java.time.format.DateTimeFormatter @Composable @@ -104,7 +91,7 @@ fun BookingCreateScreen( val defaultCheckoutDate = now.toLocalDate().plusDays(1) checkOutDate.value = defaultCheckoutDate checkOutTime.value = "11:00" - viewModel.onExpectedCheckOutAtChange(formatIso(defaultCheckoutDate, checkOutTime.value)) + viewModel.onExpectedCheckOutAtChange(formatBookingIso(defaultCheckoutDate, checkOutTime.value)) } Scaffold( @@ -508,7 +495,7 @@ fun BookingCreateScreen( } if (showCheckInPicker.value) { - DateTimePickerDialog( + BookingDateTimePickerDialog( title = "Select check-in", initialDate = checkInDate.value, initialTime = checkInTime.value, @@ -517,7 +504,7 @@ fun BookingCreateScreen( onConfirm = { date, time -> checkInDate.value = date checkInTime.value = time - val formatted = formatIso(date, time) + val formatted = formatBookingIso(date, time) viewModel.onExpectedCheckInAtChange(formatted) showCheckInPicker.value = false } @@ -525,7 +512,7 @@ fun BookingCreateScreen( } if (showCheckOutPicker.value) { - DateTimePickerDialog( + BookingDateTimePickerDialog( title = "Select check-out", initialDate = checkOutDate.value, initialTime = checkOutTime.value, @@ -534,7 +521,7 @@ fun BookingCreateScreen( onConfirm = { date, time -> checkOutDate.value = date checkOutTime.value = time - val formatted = formatIso(date, time) + val formatted = formatBookingIso(date, time) viewModel.onExpectedCheckOutAtChange(formatted) showCheckOutPicker.value = false } @@ -593,153 +580,3 @@ private fun TimePickerTextField( } ) } - -@Composable -private fun DateTimePickerDialog( - title: String, - initialDate: LocalDate?, - initialTime: String, - minDate: LocalDate, - onDismiss: () -> Unit, - onConfirm: (LocalDate, String) -> Unit -) { - val today = remember { LocalDate.now() } - val currentMonth = remember { YearMonth.from(today) } - val startMonth = remember { currentMonth } - val endMonth = remember { currentMonth.plusMonths(24) } - val daysOfWeek = remember { daysOfWeek() } - val calendarState = rememberCalendarState( - startMonth = startMonth, - endMonth = endMonth, - firstVisibleMonth = currentMonth, - firstDayOfWeek = daysOfWeek.first() - ) - val selectedDate = remember { mutableStateOf(initialDate ?: today) } - val timeValue = remember { mutableStateOf(initialTime) } - val dateFormatter = remember { DateTimeFormatter.ISO_LOCAL_DATE } - - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(title) }, - text = { - Column { - DaysOfWeekHeader(daysOfWeek) - HorizontalCalendar( - state = calendarState, - dayContent = { day -> - val selectable = day.position == DayPosition.MonthDate && !day.date.isBefore(minDate) - DayCell( - day = day, - isSelectedStart = selectedDate.value == day.date, - isSelectedEnd = false, - isInRange = false, - hasRate = false, - isSelectable = selectable, - onClick = { selectedDate.value = day.date } - ) - }, - monthHeader = { month -> - MonthHeader(month) - } - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Selected: ${selectedDate.value.format(dateFormatter)}", - style = MaterialTheme.typography.bodySmall - ) - Spacer(modifier = Modifier.height(8.dp)) - OutlinedTextField( - value = timeValue.value, - onValueChange = { timeValue.value = it }, - label = { Text("Time (HH:MM)") }, - modifier = Modifier.fillMaxWidth() - ) - } - }, - confirmButton = { - TextButton( - onClick = { - val time = timeValue.value.ifBlank { initialTime } - onConfirm(selectedDate.value, time) - } - ) { - Text("OK") - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text("Cancel") - } - } - ) -} - -@Composable -private fun DaysOfWeekHeader(daysOfWeek: List) { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - daysOfWeek.forEach { day -> - Text( - text = day.name.take(3), - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.weight(1f), - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - Spacer(modifier = Modifier.height(4.dp)) -} - -@Composable -private fun MonthHeader(month: CalendarMonth) { - Text( - text = "${month.yearMonth.month.name.lowercase().replaceFirstChar { it.titlecase() }} ${month.yearMonth.year}", - style = MaterialTheme.typography.titleSmall, - modifier = Modifier.padding(vertical = 8.dp) - ) -} - -@Composable -private fun DayCell( - day: CalendarDay, - isSelectedStart: Boolean, - isSelectedEnd: Boolean, - isInRange: Boolean, - hasRate: Boolean, - isSelectable: Boolean, - onClick: () -> Unit -) { - val isInMonth = day.position == DayPosition.MonthDate - val background = when { - isSelectedStart || isSelectedEnd -> MaterialTheme.colorScheme.primary.copy(alpha = 0.35f) - isInRange -> MaterialTheme.colorScheme.primary.copy(alpha = 0.18f) - hasRate -> MaterialTheme.colorScheme.secondary.copy(alpha = 0.2f) - else -> Color.Transparent - } - val textColor = when { - !isInMonth -> MaterialTheme.colorScheme.onSurfaceVariant - !isSelectable -> MaterialTheme.colorScheme.error - else -> MaterialTheme.colorScheme.onSurface - } - Column( - modifier = Modifier - .size(40.dp) - .padding(2.dp) - .background(background, shape = MaterialTheme.shapes.small) - .clickable(enabled = isSelectable) { onClick() }, - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text(text = day.date.dayOfMonth.toString(), color = textColor, style = MaterialTheme.typography.bodySmall) - } -} - -private fun formatIso(date: LocalDate, time: String): String { - val parts = time.split(":") - val hour = parts.getOrNull(0)?.toIntOrNull() ?: 0 - val minute = parts.getOrNull(1)?.toIntOrNull() ?: 0 - val zone = ZoneId.of("Asia/Kolkata") - val localDateTime = LocalDateTime.of(date.year, date.monthValue, date.dayOfMonth, hour, minute) - val offset = zone.rules.getOffset(localDateTime) - return OffsetDateTime.of(localDateTime, offset) - .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) -} diff --git a/app/src/main/java/com/android/trisolarispms/ui/booking/BookingDateTimePicker.kt b/app/src/main/java/com/android/trisolarispms/ui/booking/BookingDateTimePicker.kt new file mode 100644 index 0000000..8fee4fb --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/ui/booking/BookingDateTimePicker.kt @@ -0,0 +1,120 @@ +package com.android.trisolarispms.ui.booking + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.trisolarispms.ui.calendar.CalendarDayCell +import com.android.trisolarispms.ui.calendar.CalendarDaysOfWeekHeader +import com.android.trisolarispms.ui.calendar.CalendarMonthHeader +import com.kizitonwose.calendar.compose.HorizontalCalendar +import com.kizitonwose.calendar.compose.rememberCalendarState +import com.kizitonwose.calendar.core.DayPosition +import com.kizitonwose.calendar.core.daysOfWeek +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.time.YearMonth +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Composable +internal fun BookingDateTimePickerDialog( + title: String, + initialDate: LocalDate?, + initialTime: String, + minDate: LocalDate, + onDismiss: () -> Unit, + onConfirm: (LocalDate, String) -> Unit +) { + val today = remember { LocalDate.now() } + val currentMonth = remember { YearMonth.from(today) } + val startMonth = remember { currentMonth } + val endMonth = remember { currentMonth.plusMonths(24) } + val daysOfWeek = remember { daysOfWeek() } + val calendarState = rememberCalendarState( + startMonth = startMonth, + endMonth = endMonth, + firstVisibleMonth = currentMonth, + firstDayOfWeek = daysOfWeek.first() + ) + val selectedDate = remember { mutableStateOf(initialDate ?: today) } + val timeValue = remember { mutableStateOf(initialTime) } + val dateFormatter = remember { DateTimeFormatter.ISO_LOCAL_DATE } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(title) }, + text = { + Column { + CalendarDaysOfWeekHeader(daysOfWeek) + HorizontalCalendar( + state = calendarState, + dayContent = { day -> + val selectable = day.position == DayPosition.MonthDate && !day.date.isBefore(minDate) + CalendarDayCell( + day = day, + isSelectedStart = selectedDate.value == day.date, + isSelectedEnd = false, + isInRange = false, + hasRate = false, + isSelectable = selectable, + onClick = { selectedDate.value = day.date } + ) + }, + monthHeader = { month -> + CalendarMonthHeader(month) + } + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Selected: ${selectedDate.value.format(dateFormatter)}", + style = MaterialTheme.typography.bodySmall + ) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = timeValue.value, + onValueChange = { timeValue.value = it }, + label = { Text("Time (HH:MM)") }, + modifier = Modifier.fillMaxWidth() + ) + } + }, + confirmButton = { + TextButton( + onClick = { + val time = timeValue.value.ifBlank { initialTime } + onConfirm(selectedDate.value, time) + } + ) { + Text("OK") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + } + ) +} + +internal fun formatBookingIso(date: LocalDate, time: String): String { + val parts = time.split(":") + val hour = parts.getOrNull(0)?.toIntOrNull() ?: 0 + val minute = parts.getOrNull(1)?.toIntOrNull() ?: 0 + val zone = ZoneId.of("Asia/Kolkata") + val localDateTime = LocalDateTime.of(date.year, date.monthValue, date.dayOfMonth, hour, minute) + val offset = zone.rules.getOffset(localDateTime) + return OffsetDateTime.of(localDateTime, offset) + .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) +} diff --git a/app/src/main/java/com/android/trisolarispms/ui/booking/BookingExpectedDatesScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/booking/BookingExpectedDatesScreen.kt index 2f1db3b..a598adf 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/booking/BookingExpectedDatesScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/booking/BookingExpectedDatesScreen.kt @@ -1,21 +1,16 @@ package com.android.trisolarispms.ui.booking -import androidx.compose.foundation.background -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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.CalendarMonth import androidx.compose.material.icons.filled.Done -import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -24,31 +19,20 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults 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.unit.dp -import androidx.compose.ui.Alignment -import androidx.compose.ui.graphics.Color -import kotlinx.coroutines.launch -import androidx.compose.runtime.rememberCoroutineScope import com.android.trisolarispms.data.api.core.ApiClient import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest -import com.kizitonwose.calendar.compose.HorizontalCalendar -import com.kizitonwose.calendar.compose.rememberCalendarState -import com.kizitonwose.calendar.core.CalendarDay -import com.kizitonwose.calendar.core.CalendarMonth -import com.kizitonwose.calendar.core.DayPosition -import com.kizitonwose.calendar.core.daysOfWeek +import kotlinx.coroutines.launch import java.time.LocalDate -import java.time.LocalDateTime import java.time.OffsetDateTime -import java.time.YearMonth import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -115,11 +99,11 @@ fun BookingExpectedDatesScreen( isLoading.value = true error.value = null val inAt = if (editableCheckIn) { - checkInDate.value?.let { formatIso(it, checkInTime.value) } + checkInDate.value?.let { formatBookingIso(it, checkInTime.value) } } else { null } - val outAt = checkOutDate.value?.let { formatIso(it, checkOutTime.value) } + val outAt = checkOutDate.value?.let { formatBookingIso(it, checkOutTime.value) } scope.launch { try { val api = ApiClient.create() @@ -162,7 +146,7 @@ fun BookingExpectedDatesScreen( if (editableCheckIn) { OutlinedTextField( value = checkInDate.value?.let { - formatIso(it, checkInTime.value) + formatBookingIso(it, checkInTime.value) }?.let { iso -> runCatching { OffsetDateTime.parse(iso).atZoneSameInstant(displayZone).format(displayFormatter) @@ -182,7 +166,7 @@ fun BookingExpectedDatesScreen( } OutlinedTextField( value = checkOutDate.value?.let { - formatIso(it, checkOutTime.value) + formatBookingIso(it, checkOutTime.value) }?.let { iso -> runCatching { OffsetDateTime.parse(iso).atZoneSameInstant(displayZone).format(displayFormatter) @@ -210,7 +194,7 @@ fun BookingExpectedDatesScreen( } if (showCheckInPicker.value && editableCheckIn) { - DateTimePickerDialog( + BookingDateTimePickerDialog( title = "Select check-in", initialDate = checkInDate.value, initialTime = checkInTime.value, @@ -225,7 +209,7 @@ fun BookingExpectedDatesScreen( } if (showCheckOutPicker.value) { - DateTimePickerDialog( + BookingDateTimePickerDialog( title = "Select check-out", initialDate = checkOutDate.value, initialTime = checkOutTime.value, @@ -239,153 +223,3 @@ fun BookingExpectedDatesScreen( ) } } - -@Composable -private fun DateTimePickerDialog( - title: String, - initialDate: LocalDate?, - initialTime: String, - minDate: LocalDate, - onDismiss: () -> Unit, - onConfirm: (LocalDate, String) -> Unit -) { - val today = remember { LocalDate.now() } - val currentMonth = remember { YearMonth.from(today) } - val startMonth = remember { currentMonth } - val endMonth = remember { currentMonth.plusMonths(24) } - val daysOfWeek = remember { daysOfWeek() } - val calendarState = rememberCalendarState( - startMonth = startMonth, - endMonth = endMonth, - firstVisibleMonth = currentMonth, - firstDayOfWeek = daysOfWeek.first() - ) - val selectedDate = remember { mutableStateOf(initialDate ?: today) } - val timeValue = remember { mutableStateOf(initialTime) } - val dateFormatter = remember { DateTimeFormatter.ISO_LOCAL_DATE } - - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(title) }, - text = { - Column { - DaysOfWeekHeader(daysOfWeek) - HorizontalCalendar( - state = calendarState, - dayContent = { day -> - val selectable = day.position == DayPosition.MonthDate && !day.date.isBefore(minDate) - DayCell( - day = day, - isSelectedStart = selectedDate.value == day.date, - isSelectedEnd = false, - isInRange = false, - hasRate = false, - isSelectable = selectable, - onClick = { selectedDate.value = day.date } - ) - }, - monthHeader = { month -> - MonthHeader(month) - } - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Selected: ${selectedDate.value.format(dateFormatter)}", - style = MaterialTheme.typography.bodySmall - ) - Spacer(modifier = Modifier.height(8.dp)) - OutlinedTextField( - value = timeValue.value, - onValueChange = { timeValue.value = it }, - label = { Text("Time (HH:MM)") }, - modifier = Modifier.fillMaxWidth() - ) - } - }, - confirmButton = { - TextButton( - onClick = { - val time = timeValue.value.ifBlank { initialTime } - onConfirm(selectedDate.value, time) - } - ) { - Text("OK") - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text("Cancel") - } - } - ) -} - -@Composable -private fun DaysOfWeekHeader(daysOfWeek: List) { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - daysOfWeek.forEach { day -> - Text( - text = day.name.take(3), - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.weight(1f), - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - Spacer(modifier = Modifier.height(4.dp)) -} - -@Composable -private fun MonthHeader(month: CalendarMonth) { - Text( - text = "${month.yearMonth.month.name.lowercase().replaceFirstChar { it.titlecase() }} ${month.yearMonth.year}", - style = MaterialTheme.typography.titleSmall, - modifier = Modifier.padding(vertical = 8.dp) - ) -} - -@Composable -private fun DayCell( - day: CalendarDay, - isSelectedStart: Boolean, - isSelectedEnd: Boolean, - isInRange: Boolean, - hasRate: Boolean, - isSelectable: Boolean, - onClick: () -> Unit -) { - val isInMonth = day.position == DayPosition.MonthDate - val background = when { - isSelectedStart || isSelectedEnd -> MaterialTheme.colorScheme.primary.copy(alpha = 0.35f) - isInRange -> MaterialTheme.colorScheme.primary.copy(alpha = 0.18f) - hasRate -> MaterialTheme.colorScheme.secondary.copy(alpha = 0.2f) - else -> Color.Transparent - } - val textColor = when { - !isInMonth -> MaterialTheme.colorScheme.onSurfaceVariant - !isSelectable -> MaterialTheme.colorScheme.error - else -> MaterialTheme.colorScheme.onSurface - } - Column( - modifier = Modifier - .size(40.dp) - .padding(2.dp) - .background(background, shape = MaterialTheme.shapes.small) - .clickable(enabled = isSelectable) { onClick() }, - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text(text = day.date.dayOfMonth.toString(), color = textColor, style = MaterialTheme.typography.bodySmall) - } -} - -private fun formatIso(date: LocalDate, time: String): String { - val parts = time.split(":") - val hour = parts.getOrNull(0)?.toIntOrNull() ?: 0 - val minute = parts.getOrNull(1)?.toIntOrNull() ?: 0 - val zone = ZoneId.of("Asia/Kolkata") - val localDateTime = LocalDateTime.of(date.year, date.monthValue, date.dayOfMonth, hour, minute) - val offset = zone.rules.getOffset(localDateTime) - return OffsetDateTime.of(localDateTime, offset) - .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) -} diff --git a/app/src/main/java/com/android/trisolarispms/ui/calendar/CalendarComponents.kt b/app/src/main/java/com/android/trisolarispms/ui/calendar/CalendarComponents.kt new file mode 100644 index 0000000..efce045 --- /dev/null +++ b/app/src/main/java/com/android/trisolarispms/ui/calendar/CalendarComponents.kt @@ -0,0 +1,85 @@ +package com.android.trisolarispms.ui.calendar + +import androidx.compose.foundation.background +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.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.kizitonwose.calendar.core.CalendarDay +import com.kizitonwose.calendar.core.CalendarMonth +import com.kizitonwose.calendar.core.DayPosition + +@Composable +fun CalendarDaysOfWeekHeader(daysOfWeek: List) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + daysOfWeek.forEach { day -> + Text( + text = day.name.take(3), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.weight(1f), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + Spacer(modifier = Modifier.height(4.dp)) +} + +@Composable +fun CalendarMonthHeader(month: CalendarMonth) { + Text( + text = "${month.yearMonth.month.name.lowercase().replaceFirstChar { it.titlecase() }} ${month.yearMonth.year}", + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.padding(vertical = 8.dp) + ) +} + +@Composable +fun CalendarDayCell( + day: CalendarDay, + isSelectedStart: Boolean, + isSelectedEnd: Boolean, + isInRange: Boolean, + hasRate: Boolean, + isSelectable: Boolean, + onClick: () -> Unit, + footerContent: (@Composable () -> Unit)? = null +) { + val isInMonth = day.position == DayPosition.MonthDate + val background = when { + isSelectedStart || isSelectedEnd -> MaterialTheme.colorScheme.primary.copy(alpha = 0.35f) + isInRange -> MaterialTheme.colorScheme.primary.copy(alpha = 0.18f) + hasRate -> MaterialTheme.colorScheme.secondary.copy(alpha = 0.2f) + else -> Color.Transparent + } + val textColor = when { + !isInMonth -> MaterialTheme.colorScheme.onSurfaceVariant + !isSelectable -> MaterialTheme.colorScheme.error + else -> MaterialTheme.colorScheme.onSurface + } + Column( + modifier = Modifier + .size(40.dp) + .padding(2.dp) + .background(background, shape = MaterialTheme.shapes.small) + .clickable(enabled = isSelectable) { onClick() }, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text(text = day.date.dayOfMonth.toString(), color = textColor, style = MaterialTheme.typography.bodySmall) + if (footerContent != null) { + footerContent() + } + } +} diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysScreen.kt index 3f0c633..ad9c7f0 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysScreen.kt @@ -246,7 +246,6 @@ private fun CheckedInBookingCard( if (start != null && end != null) { val total = Duration.between(start, end).toMinutes().coerceAtLeast(0) val remaining = Duration.between(now, end).toMinutes().coerceAtLeast(0) - val hoursLeft = (remaining / 60).coerceAtLeast(0) Spacer(modifier = Modifier.height(8.dp)) if (now.isAfter(end)) { Text( @@ -260,7 +259,14 @@ private fun CheckedInBookingCard( } else { 0f } - Text(text = "$hoursLeft hours", style = MaterialTheme.typography.bodySmall) + val remainingHours = remaining / 60 + val remainingMinutes = remaining % 60 + val remainingText = if (remainingHours > 0) { + "${remainingHours}h ${remainingMinutes}m" + } else { + "${remainingMinutes}m" + } + Text(text = remainingText, style = MaterialTheme.typography.bodySmall) Spacer(modifier = Modifier.height(6.dp)) LinearProgressIndicator( progress = { progress }, diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomtype/RatePlanCalendarScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomtype/RatePlanCalendarScreen.kt index ccabe62..4a0bfe8 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomtype/RatePlanCalendarScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomtype/RatePlanCalendarScreen.kt @@ -1,7 +1,5 @@ package com.android.trisolarispms.ui.roomtype -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -10,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -31,16 +28,15 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.android.trisolarispms.ui.calendar.CalendarDayCell +import com.android.trisolarispms.ui.calendar.CalendarDaysOfWeekHeader +import com.android.trisolarispms.ui.calendar.CalendarMonthHeader import com.kizitonwose.calendar.compose.HorizontalCalendar import com.kizitonwose.calendar.compose.rememberCalendarState -import com.kizitonwose.calendar.core.CalendarDay -import com.kizitonwose.calendar.core.CalendarMonth import com.kizitonwose.calendar.core.DayPosition import com.kizitonwose.calendar.core.daysOfWeek import java.time.LocalDate @@ -114,7 +110,7 @@ fun RatePlanCalendarScreen( Text(text = it, color = MaterialTheme.colorScheme.error) Spacer(modifier = Modifier.height(8.dp)) } - DaysOfWeekHeader(daysOfWeek) + CalendarDaysOfWeekHeader(daysOfWeek) Text( text = "Past dates disabled", style = MaterialTheme.typography.bodySmall, @@ -132,7 +128,7 @@ fun RatePlanCalendarScreen( (day.date == start || day.date == end || (day.date.isAfter(start) && day.date.isBefore(end))) val selectable = day.position == DayPosition.MonthDate && !day.date.isBefore(today) - DayCell( + CalendarDayCell( day = day, isSelectedStart = start == day.date, isSelectedEnd = end == day.date, @@ -159,11 +155,21 @@ fun RatePlanCalendarScreen( val rateEntry = rateByDate[day.date.format(dateFormatter)] rateInput.value = rateEntry?.rate?.toString().orEmpty() } + }, + footerContent = { + if (entry != null && day.position == DayPosition.MonthDate) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = "₹", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurface + ) + } } ) }, monthHeader = { month -> - MonthHeader(month) + CalendarMonthHeader(month) } ) Spacer(modifier = Modifier.height(16.dp)) @@ -230,66 +236,3 @@ fun RatePlanCalendarScreen( } } } - -@Composable -private fun DaysOfWeekHeader(daysOfWeek: List) { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - daysOfWeek.forEach { day -> - Text( - text = day.name.take(3), - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.weight(1f), - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - Spacer(modifier = Modifier.height(4.dp)) -} - -@Composable -private fun MonthHeader(month: CalendarMonth) { - Text( - text = "${month.yearMonth.month.name.lowercase().replaceFirstChar { it.titlecase() }} ${month.yearMonth.year}", - style = MaterialTheme.typography.titleSmall, - modifier = Modifier.padding(vertical = 8.dp) - ) -} - -@Composable -private fun DayCell( - day: CalendarDay, - isSelectedStart: Boolean, - isSelectedEnd: Boolean, - isInRange: Boolean, - hasRate: Boolean, - isSelectable: Boolean, - onClick: () -> Unit -) { - val isInMonth = day.position == DayPosition.MonthDate - val background = when { - isSelectedStart || isSelectedEnd -> MaterialTheme.colorScheme.primary.copy(alpha = 0.35f) - isInRange -> MaterialTheme.colorScheme.primary.copy(alpha = 0.18f) - hasRate -> MaterialTheme.colorScheme.secondary.copy(alpha = 0.2f) - else -> Color.Transparent - } - val textColor = when { - !isInMonth -> MaterialTheme.colorScheme.onSurfaceVariant - !isSelectable -> MaterialTheme.colorScheme.error - else -> MaterialTheme.colorScheme.onSurface - } - Column( - modifier = Modifier - .size(40.dp) - .padding(2.dp) - .background(background, shape = MaterialTheme.shapes.small) - .clickable(enabled = isSelectable) { onClick() }, - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text(text = day.date.dayOfMonth.toString(), color = textColor, style = MaterialTheme.typography.bodySmall) - if (hasRate && isInMonth) { - Spacer(modifier = Modifier.height(2.dp)) - Text(text = "₹", style = MaterialTheme.typography.labelSmall, color = textColor) - } - } -}