Не подтверждена Коммит fb0d66d3 создал по автору Vitaly.Smirnov's avatar Vitaly.Smirnov Зафиксировано автором GitHub
Просмотр файлов

fix: Prevent past time bookings in BookingEditor (#340)



Co-authored-by: default avatarRadch-enko <stanislav.radchenko@effective.band>
владелец 960d50d8
...@@ -6,4 +6,7 @@ ...@@ -6,4 +6,7 @@
<string name="update_button">Изменить</string> <string name="update_button">Изменить</string>
<string name="delete_button">Удалить бронь</string> <string name="delete_button">Удалить бронь</string>
<string name="error">Произошла ошибка</string> <string name="error">Произошла ошибка</string>
<string name="is_time_in_past_error">Ошибка, выбрано прошедшее время</string>
<string name="error_creating_event">Error creating event</string>
<string name="error_deleting_event">Error deleting event"</string>
</resources> </resources>
\ No newline at end of file
...@@ -11,10 +11,17 @@ import androidx.compose.foundation.rememberScrollState ...@@ -11,10 +11,17 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
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.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
...@@ -40,6 +47,9 @@ import band.effective.office.tablet.feature.bookingEditor.booking_view_title ...@@ -40,6 +47,9 @@ import band.effective.office.tablet.feature.bookingEditor.booking_view_title
import band.effective.office.tablet.feature.bookingEditor.create_view_title import band.effective.office.tablet.feature.bookingEditor.create_view_title
import band.effective.office.tablet.feature.bookingEditor.delete_button import band.effective.office.tablet.feature.bookingEditor.delete_button
import band.effective.office.tablet.feature.bookingEditor.error import band.effective.office.tablet.feature.bookingEditor.error
import band.effective.office.tablet.feature.bookingEditor.error_creating_event
import band.effective.office.tablet.feature.bookingEditor.error_deleting_event
import band.effective.office.tablet.feature.bookingEditor.is_time_in_past_error
import band.effective.office.tablet.feature.bookingEditor.presentation.datetimepicker.DateTimePickerModalView import band.effective.office.tablet.feature.bookingEditor.presentation.datetimepicker.DateTimePickerModalView
import band.effective.office.tablet.feature.bookingEditor.update_button import band.effective.office.tablet.feature.bookingEditor.update_button
import com.arkivanov.decompose.extensions.compose.stack.Children import com.arkivanov.decompose.extensions.compose.stack.Children
...@@ -109,6 +119,7 @@ fun BookingEditor( ...@@ -109,6 +119,7 @@ fun BookingEditor(
start = state.event.startTime.format(timeFormatter), start = state.event.startTime.format(timeFormatter),
finish = state.event.finishTime.format(timeFormatter), finish = state.event.finishTime.format(timeFormatter),
room = component.roomName, room = component.roomName,
isTimeInPastError = state.isTimeInPastError
) )
} }
} }
...@@ -149,9 +160,20 @@ private fun BookingEditor( ...@@ -149,9 +160,20 @@ private fun BookingEditor(
isNewEvent: Boolean, isNewEvent: Boolean,
start: String, start: String,
finish: String, finish: String,
room: String room: String,
isTimeInPastError: Boolean
) { ) {
val snackbarHostState = remember { SnackbarHostState() }
val timeInPastErrorMessage = stringResource(Res.string.is_time_in_past_error)
LaunchedEffect(isTimeInPastError) {
if (isTimeInPastError) {
snackbarHostState.showSnackbar(
message = timeInPastErrorMessage,
duration = SnackbarDuration.Short
)
}
}
Box { Box {
Column( Column(
modifier = Modifier modifier = Modifier
...@@ -212,7 +234,7 @@ private fun BookingEditor( ...@@ -212,7 +234,7 @@ private fun BookingEditor(
when { when {
isCreateLoad -> Loader() isCreateLoad -> Loader()
isCreateError -> Text( isCreateError -> Text(
text = "Error creating event", // Ideally, this should be a string resource text = stringResource(Res.string.error_creating_event),
style = MaterialTheme.typography.h6 style = MaterialTheme.typography.h6
) )
else -> Text( else -> Text(
...@@ -247,7 +269,7 @@ private fun BookingEditor( ...@@ -247,7 +269,7 @@ private fun BookingEditor(
when { when {
isDeleteLoad -> Loader() isDeleteLoad -> Loader()
isDeleteError -> Text( isDeleteError -> Text(
text = "Error deleting event", // Ideally, this should be a string resource text = stringResource(Res.string.error_deleting_event),
style = MaterialTheme.typography.h6 style = MaterialTheme.typography.h6
) )
...@@ -266,5 +288,22 @@ private fun BookingEditor( ...@@ -266,5 +288,22 @@ private fun BookingEditor(
.fillMaxWidth().padding(35.dp), .fillMaxWidth().padding(35.dp),
onDismissRequest = onDismissRequest onDismissRequest = onDismissRequest
) )
SnackbarHost(
hostState = snackbarHostState,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 32.dp)
) { snackbarData ->
Snackbar(
modifier = Modifier.padding(16.dp),
containerColor = MaterialTheme.colorScheme.errorContainer,
contentColor = MaterialTheme.colorScheme.onErrorContainer
) {
Text(
text = snackbarData.visuals.message,
style = MaterialTheme.typography.bodyMedium
)
}
}
} }
} }
...@@ -11,8 +11,6 @@ import band.effective.office.tablet.core.domain.useCase.OrganizersInfoUseCase ...@@ -11,8 +11,6 @@ import band.effective.office.tablet.core.domain.useCase.OrganizersInfoUseCase
import band.effective.office.tablet.core.domain.useCase.UpdateBookingUseCase import band.effective.office.tablet.core.domain.useCase.UpdateBookingUseCase
import band.effective.office.tablet.core.domain.util.asInstant import band.effective.office.tablet.core.domain.util.asInstant
import band.effective.office.tablet.core.domain.util.asLocalDateTime import band.effective.office.tablet.core.domain.util.asLocalDateTime
import band.effective.office.tablet.core.domain.util.currentLocalDateTime
import band.effective.office.tablet.core.domain.util.defaultTimeZone
import band.effective.office.tablet.core.ui.common.ModalWindow import band.effective.office.tablet.core.ui.common.ModalWindow
import band.effective.office.tablet.core.ui.utils.componentCoroutineScope import band.effective.office.tablet.core.ui.utils.componentCoroutineScope
import band.effective.office.tablet.feature.bookingEditor.presentation.datetimepicker.DateTimePickerComponent import band.effective.office.tablet.feature.bookingEditor.presentation.datetimepicker.DateTimePickerComponent
...@@ -31,8 +29,10 @@ import kotlinx.coroutines.flow.asStateFlow ...@@ -31,8 +29,10 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.atStartOfDayIn import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
...@@ -216,7 +216,7 @@ class BookingEditorComponent( ...@@ -216,7 +216,7 @@ class BookingEditorComponent(
inputError: Boolean, inputError: Boolean,
busyEvent: Boolean busyEvent: Boolean
) = mutableState.update { ) = mutableState.update {
it.copy(enableUpdateButton = !inputError && !busyEvent) it.copy(enableUpdateButton = !inputError && !busyEvent && !it.isTimeInPastError)
} }
/** /**
...@@ -229,12 +229,14 @@ class BookingEditorComponent( ...@@ -229,12 +229,14 @@ class BookingEditorComponent(
duration = duration, duration = duration,
organizer = selectOrganizer organizer = selectOrganizer
) )
val isTimeInPast = newDate <= getCurrentTime()
updateStateWithNewEventDetails( updateStateWithNewEventDetails(
newDate = newDate, newDate = newDate,
newDuration = duration, newDuration = duration,
newOrganizer = selectOrganizer, newOrganizer = selectOrganizer,
busyEvents = busyEvents busyEvents = busyEvents,
isTimeInPast = isTimeInPast
) )
if (selectOrganizer != Organizer.default) { if (selectOrganizer != Organizer.default) {
...@@ -260,45 +262,33 @@ class BookingEditorComponent( ...@@ -260,45 +262,33 @@ class BookingEditorComponent(
val resolvedOrganizer = organizers.firstOrNull { val resolvedOrganizer = organizers.firstOrNull {
it.fullName == newOrganizer.fullName it.fullName == newOrganizer.fullName
} ?: event.organizer } ?: event.organizer
val isTimeInPast = newDate <= getCurrentTime()
val busyEvents = checkForBusyEvents( val busyEvents = checkForBusyEvents(
date = newDate, date = newDate,
duration = newDuration, duration = newDuration,
organizer = resolvedOrganizer organizer = resolvedOrganizer
) )
if (isValidEventTime(newDate, newDuration)) { updateStateWithNewEventDetails(
updateStateWithNewEventDetails( newDate = newDate,
newDate = newDate, newDuration = newDuration,
newDuration = newDuration, newOrganizer = resolvedOrganizer,
newOrganizer = resolvedOrganizer, busyEvents = busyEvents,
busyEvents = busyEvents isTimeInPast = isTimeInPast
) )
updateButtonState( updateButtonState(
inputError = !organizers.contains(resolvedOrganizer), inputError = !organizers.contains(resolvedOrganizer),
busyEvent = busyEvents.isNotEmpty() busyEvent = busyEvents.isNotEmpty()
) )
}
} }
} }
/** /**
* Checks if the event time is valid * Gets the current time
*/
private fun isValidEventTime(date: LocalDateTime, duration: Int): Boolean {
val today = getTodayStartTime()
val officeEndTime = OfficeTime.finishWorkTime(date.date)
val eventEndTime = date.asInstant.plus(duration.minutes).asLocalDateTime
return duration > 0 && date > today && eventEndTime < officeEndTime
}
/**
* Gets the start time of today
*/ */
private fun getTodayStartTime(): LocalDateTime = private fun getCurrentTime(): LocalDateTime =
currentLocalDateTime.date.atStartOfDayIn(defaultTimeZone).asLocalDateTime Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
/** /**
* Checks for busy events that conflict with the given parameters * Checks for busy events that conflict with the given parameters
...@@ -343,7 +333,8 @@ class BookingEditorComponent( ...@@ -343,7 +333,8 @@ class BookingEditorComponent(
newDate: LocalDateTime, newDate: LocalDateTime,
newDuration: Int, newDuration: Int,
newOrganizer: Organizer, newOrganizer: Organizer,
busyEvents: List<EventInfo> busyEvents: List<EventInfo>,
isTimeInPast: Boolean
) { ) {
val updatedEvent = createEventInfo( val updatedEvent = createEventInfo(
id = state.value.event.id, id = state.value.event.id,
...@@ -358,7 +349,8 @@ class BookingEditorComponent( ...@@ -358,7 +349,8 @@ class BookingEditorComponent(
duration = newDuration, duration = newDuration,
selectOrganizer = newOrganizer, selectOrganizer = newOrganizer,
event = updatedEvent, event = updatedEvent,
isBusyEvent = busyEvents.isNotEmpty() isBusyEvent = busyEvents.isNotEmpty(),
isTimeInPastError = isTimeInPast
) )
} }
} }
......
...@@ -23,7 +23,8 @@ data class State( ...@@ -23,7 +23,8 @@ data class State(
val isErrorCreate: Boolean, val isErrorCreate: Boolean,
val showSelectDate: Boolean, val showSelectDate: Boolean,
val enableUpdateButton: Boolean, val enableUpdateButton: Boolean,
val isBusyEvent: Boolean val isBusyEvent: Boolean,
val isTimeInPastError: Boolean
) { ) {
companion object { companion object {
val defaultValue = State( val defaultValue = State(
...@@ -44,7 +45,8 @@ data class State( ...@@ -44,7 +45,8 @@ data class State(
isErrorCreate = false, isErrorCreate = false,
showSelectDate = false, showSelectDate = false,
enableUpdateButton = false, enableUpdateButton = false,
isBusyEvent = false isBusyEvent = false,
isTimeInPastError = false
) )
} }
......
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать