Коммит 874784e8 создал по автору Radch-enko's avatar Radch-enko
Просмотр файлов

Enhance error handling and loading states in FastBooking and FreeSelectRoom features

- Added `isError` state tracking in `State` to improve error identification and handling.
- Introduced `ErrorView` and updated `FastBooking` composables to display error UI when necessary.
- Refactored `LoadingView` for better clarity and modularity.
- Improved `FastBookingComponent` logic for room availability checks, error handling, and state management.
- Updated `FreeSelectRoomComponent` to use `FreeUpRoomUseCase` directly and removed redundant event handling logic.
- Added error string resource for better localization handling.
владелец d0591192
......@@ -32,4 +32,5 @@
<string name="event_slot">Занято %1$s</string>
<string name="multislot">%1$s брони</string>
<string name="loading_slot">Слот загружается</string>
<string name="error">Error</string>
</resources>
\ Нет новой строки в конце файла
......@@ -21,18 +21,29 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import band.effective.office.tablet.core.ui.Res
import band.effective.office.tablet.core.ui.common.CrossButtonView
import band.effective.office.tablet.core.ui.common.FailureFastSelectRoomView
import band.effective.office.tablet.core.ui.common.Loader
import band.effective.office.tablet.core.ui.common.SuccessFastSelectRoomView
import band.effective.office.tablet.core.ui.date.timeFormatter
import band.effective.office.tablet.core.ui.error
import band.effective.office.tablet.core.ui.theme.LocalCustomColorsPalette
import band.effective.office.tablet.core.ui.theme.h2
import band.effective.office.tablet.core.ui.common.FailureFastSelectRoomView
import band.effective.office.tablet.core.ui.common.SuccessFastSelectRoomView
import band.effective.office.tablet.core.ui.theme.h4
import com.arkivanov.decompose.extensions.compose.stack.Children
import org.jetbrains.compose.resources.stringResource
/**
* Main composable for the Fast Booking feature.
* Displays different views based on the current state of the component.
*
* @param component The FastBookingComponent that manages the state and logic
*/
@Composable
fun FastBooking(component: FastBookingComponent) {
val state by component.state.collectAsState()
......@@ -61,23 +72,43 @@ fun FastBooking(component: FastBookingComponent) {
verticalAlignment = Alignment.CenterVertically
) {
when (val modalInstance = modal.instance) {
FastBookingComponent.ModalConfig.LoadingModal -> FastBooking(
FastBookingComponent.ModalConfig.LoadingModal -> LoadingView(
onDismissRequest = { component.sendIntent(Intent.OnCloseWindowRequest) }
)
is FastBookingComponent.ModalConfig.FailureModal -> FailureFastSelectRoomView(
onDismissRequest = { component.sendIntent(Intent.OnCloseWindowRequest) },
minutes = state.minutesLeft,
room = modalInstance.room
)
is FastBookingComponent.ModalConfig.FailureModal -> {
// Show failure view - either due to no available rooms or an error
if (state.isError) {
ErrorView(onDismissRequest = { component.sendIntent(Intent.OnCloseWindowRequest) })
} else {
FailureFastSelectRoomView(
onDismissRequest = { component.sendIntent(Intent.OnCloseWindowRequest) },
minutes = state.minutesLeft,
room = modalInstance.room
)
}
}
is FastBookingComponent.ModalConfig.SuccessModal -> SuccessFastSelectRoomView(
roomName = modalInstance.room,
finishTime = modalInstance.eventInfo.finishTime,
close = { component.sendIntent(Intent.OnCloseWindowRequest) },
onFreeRoomRequest = { component.sendIntent(Intent.OnFreeSelectRequest(it)) },
isLoading = state.isLoad
)
is FastBookingComponent.ModalConfig.SuccessModal -> {
// Only show success view if there's no error
if (state.isError) {
ErrorView(onDismissRequest = { component.sendIntent(Intent.OnCloseWindowRequest) })
} else {
SuccessFastSelectRoomView(
roomName = modalInstance.room,
finishTime = modalInstance.eventInfo.finishTime,
close = { component.sendIntent(Intent.OnCloseWindowRequest) },
onFreeRoomRequest = {
component.sendIntent(
Intent.OnFreeSelectRequest(
it
)
)
},
isLoading = state.isLoad
)
}
}
}
}
}
......@@ -85,8 +116,13 @@ fun FastBooking(component: FastBookingComponent) {
}
}
/**
* Displays a loading view with a spinner.
*
* @param onDismissRequest Callback to dismiss the dialog
*/
@Composable
private fun FastBooking(
private fun LoadingView(
onDismissRequest: () -> Unit
) {
Box(contentAlignment = Alignment.Center) {
......@@ -100,11 +136,45 @@ private fun FastBooking(
horizontalAlignment = Alignment.CenterHorizontally
) {
CrossButtonView(
Modifier.fillMaxWidth(),
onDismissRequest = { onDismissRequest() }
modifier = Modifier.fillMaxWidth(),
onDismissRequest = onDismissRequest
)
Spacer(modifier = Modifier.height(40.dp))
Loader()
}
}
}
/**
* Displays an error view when an operation fails.
*
* @param onDismissRequest Callback to dismiss the dialog
*/
@Composable
private fun ErrorView(onDismissRequest: () -> Unit) {
Box(contentAlignment = Alignment.Center) {
Column(
modifier = Modifier
.fillMaxWidth(0.75f)
.fillMaxHeight(0.4f)
.clip(RoundedCornerShape(3))
.background(LocalCustomColorsPalette.current.elevationBackground)
.padding(35.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
CrossButtonView(
modifier = Modifier.fillMaxWidth(),
onDismissRequest = onDismissRequest
)
Spacer(modifier = Modifier.height(40.dp))
Text(
text = stringResource(Res.string.error),
style = MaterialTheme.typography.h4,
minLines = 2,
textAlign = TextAlign.Center,
color = LocalCustomColorsPalette.current.primaryTextAndIcon
)
Spacer(Modifier.height(30.dp))
}
}
}
......@@ -18,7 +18,7 @@ import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.push
import kotlin.time.Duration.Companion.minutes
import io.github.aakira.napier.Napier
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
......@@ -28,7 +28,12 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.time.Duration.Companion.minutes
/**
* Component responsible for fast booking of rooms.
* Handles finding available rooms and creating quick bookings.
*/
class FastBookingComponent(
private val componentContext: ComponentContext,
val minEventDuration: Int,
......@@ -37,19 +42,23 @@ class FastBookingComponent(
private val onCloseRequest: () -> Unit
) : ComponentContext by componentContext, KoinComponent, ModalWindow {
private val scope = componentCoroutineScope()
private val coroutineScope = componentCoroutineScope()
val selectRoomUseCase: SelectRoomUseCase by inject()
// Use cases
private val selectRoomUseCase: SelectRoomUseCase by inject()
private val createFastBookingUseCase: CreateBookingUseCase by inject()
private val deleteBookingUseCase: DeleteBookingUseCase by inject()
private val timerUseCase: TimerUseCase by inject()
private val currentTimeTimer = BootstrapperTimer(timerUseCase, scope)
// Timers
private val currentTimeTimer = BootstrapperTimer(timerUseCase, coroutineScope)
// State management
private val mutableState = MutableStateFlow(State.defaultState)
val state = mutableState.asStateFlow()
// Navigation
private val navigation = StackNavigation<ModalConfig>()
val childStack = childStack(
source = navigation,
initialConfiguration = ModalConfig.LoadingModal,
......@@ -58,24 +67,21 @@ class FastBookingComponent(
)
init {
scope.launch {
val selectRoom: RoomInfo? = selectRoomUseCase.getRoom(
currentRoom = selectedRoom,
rooms = rooms,
minEventDuration = minEventDuration
)
if (selectRoom != null) {
createEvent(selectRoom.name, minEventDuration)
return@launch
}
initializeComponent()
}
mutableState.update { it.copy(isLoad = false, isSuccess = false) }
val nearestFreeRoom = selectRoomUseCase.getNearestFreeRoom(rooms, minEventDuration)
/**
* Initializes the component, finding available rooms and setting up timers.
*/
private fun initializeComponent() {
findAvailableRoom()
setupTimeUpdates()
}
mutableState.update { it.copy(minutesLeft = nearestFreeRoom.second.inWholeMinutes.toInt()) }
navigation.push(ModalConfig.FailureModal(nearestFreeRoom.first.name))
}
/**
* Sets up periodic time updates.
*/
private fun setupTimeUpdates() {
mutableState.update { it.copy(currentTime = currentLocalDateTime) }
currentTimeTimer.start(1.minutes) {
......@@ -85,53 +91,176 @@ class FastBookingComponent(
}
}
/**
* Finds an available room for booking.
*/
private fun findAvailableRoom() = coroutineScope.launch {
try {
val availableRoom = findRoomForBooking()
if (availableRoom != null) {
createEvent(availableRoom.name, minEventDuration)
} else {
handleNoAvailableRooms()
}
} catch (e: Exception) {
Napier.e("Error finding available room", e)
mutableState.update { it.copy(isLoad = false, isSuccess = false, isError = true) }
navigation.push(ModalConfig.FailureModal(""))
}
}
/**
* Finds a room that can be booked for the specified duration.
*/
private fun findRoomForBooking(): RoomInfo? {
return selectRoomUseCase.getRoom(
currentRoom = selectedRoom,
rooms = rooms,
minEventDuration = minEventDuration
)
}
/**
* Handles the case when no rooms are available for immediate booking.
*/
private fun handleNoAvailableRooms() {
mutableState.update { it.copy(isLoad = false, isSuccess = false) }
val nearestFreeRoom = selectRoomUseCase.getNearestFreeRoom(rooms, minEventDuration)
val minutesUntilAvailable = nearestFreeRoom.second.inWholeMinutes.toInt()
mutableState.update { it.copy(minutesLeft = minutesUntilAvailable) }
navigation.push(ModalConfig.FailureModal(nearestFreeRoom.first.name))
}
/**
* Handles intents from the UI.
*/
fun sendIntent(intent: Intent) {
when (intent) {
is Intent.OnFreeSelectRequest -> freeRoom(intent.room)
Intent.OnCloseWindowRequest -> onCloseRequest()
}
}
private fun createEvent(room: String, minDuration: Int) = scope.launch {
val eventInfo = EventInfo.emptyEvent.copy(
/**
* Creates a new event in the specified room.
*/
private fun createEvent(room: String, minDuration: Int) = coroutineScope.launch {
try {
val eventInfo = createEventInfo(minDuration)
when (val result = createFastBookingUseCase(room, eventInfo)) {
is Either.Success -> {
handleSuccessfulEventCreation(room, eventInfo, result.data.id)
}
is Either.Error -> {
Napier.e("Failed to create event: ${result.error}")
handleFailedEventCreation(room)
}
}
} catch (e: Exception) {
Napier.e("Error creating event", e)
handleFailedEventCreation(room)
}
}
/**
* Creates an EventInfo object with the given duration.
*/
private fun createEventInfo(minDuration: Int): EventInfo {
return EventInfo.emptyEvent.copy(
startTime = currentLocalDateTime.cropSeconds(),
finishTime = currentInstant.plus(minDuration.minutes).asLocalDateTime.cropSeconds()
)
when (val result = createFastBookingUseCase(room, eventInfo)) {
is Either.Success -> {
mutableState.update {
it.copy(
event = eventInfo.copy(id = result.data.id),
isLoad = false,
isSuccess = true,
)
}
navigation.push(ModalConfig.SuccessModal(room, eventInfo))
}
}
is Either.Error -> {
mutableState.update { it.copy(isSuccess = false) }
navigation.push(ModalConfig.FailureModal(room))
}
/**
* Handles successful event creation.
*/
private fun handleSuccessfulEventCreation(room: String, eventInfo: EventInfo, eventId: String) {
mutableState.update {
it.copy(
event = eventInfo.copy(id = eventId),
isLoad = false,
isSuccess = true,
isError = false
)
}
navigation.push(ModalConfig.SuccessModal(room, eventInfo))
}
private fun freeRoom(room: String) = scope.launch {
mutableState.update { it.copy(isLoad = true) }
deleteBookingUseCase(room, state.value.event)
onCloseRequest()
/**
* Handles failed event creation.
*/
private fun handleFailedEventCreation(room: String) {
mutableState.update {
it.copy(
isLoad = false,
isSuccess = false,
isError = true
)
}
navigation.push(ModalConfig.FailureModal(room))
}
/**
* Frees up a room by deleting the current event.
*/
private fun freeRoom(room: String) = coroutineScope.launch {
try {
mutableState.update { it.copy(isLoad = true) }
when (val result = deleteBookingUseCase(room, state.value.event)) {
is Either.Success -> {
mutableState.update { it.copy(isLoad = false) }
onCloseRequest()
}
is Either.Error -> {
Napier.e("Failed to free room: ${result.error}")
mutableState.update {
it.copy(
isLoad = false,
isError = true
)
}
}
}
} catch (e: Exception) {
Napier.e("Error freeing room", e)
mutableState.update {
it.copy(
isLoad = false,
isError = true
)
}
}
}
/**
* Modal window configurations.
*/
@Serializable
sealed interface ModalConfig {
/**
* Shown when a booking is successfully created.
*/
@Serializable
data class SuccessModal(val room: String, val eventInfo: EventInfo) : ModalConfig
/**
* Shown when a booking cannot be created.
*/
@Serializable
data class FailureModal(val room: String) : ModalConfig
/**
* Shown while loading.
*/
@Serializable
object LoadingModal : ModalConfig
}
}
\ Нет новой строки в конце файла
}
......@@ -4,9 +4,13 @@ import band.effective.office.tablet.core.domain.model.EventInfo
import band.effective.office.tablet.core.domain.util.currentLocalDateTime
import kotlinx.datetime.LocalDateTime
/**
* State for the FastBookingComponent.
*/
data class State(
val isLoad: Boolean,
val isSuccess: Boolean,
val isError: Boolean,
val event: EventInfo,
val minutesLeft: Int,
val currentTime: LocalDateTime,
......@@ -16,9 +20,10 @@ data class State(
State(
isLoad = true,
isSuccess = false,
isError = false,
event = EventInfo.emptyEvent,
minutesLeft = 0,
currentTime = currentLocalDateTime,
)
}
}
\ Нет новой строки в конце файла
}
......@@ -3,23 +3,28 @@ package band.effective.office.tablet.feature.main.presentation.freeuproom
import band.effective.office.tablet.core.domain.model.EventInfo
import band.effective.office.tablet.core.ui.common.ModalWindow
import band.effective.office.tablet.core.ui.utils.componentCoroutineScope
import band.effective.office.tablet.feature.main.domain.FreeUpRoomUseCase
import com.arkivanov.decompose.ComponentContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class FreeSelectRoomComponent(
componentContext: ComponentContext,
private val eventInfo: EventInfo,
private val onRemoveEvent: (EventInfo) -> Unit,
private val roomName: String,
private val onCloseRequest: () -> Unit,
) : ComponentContext by componentContext, ModalWindow {
) : ComponentContext by componentContext, ModalWindow, KoinComponent {
private val scope = componentCoroutineScope()
private val mutableState = MutableStateFlow(State.defaultState)
val state = mutableState.asStateFlow()
private val freeUpRoomUseCase: FreeUpRoomUseCase by inject()
fun sendIntent(intent: Intent) {
when (intent) {
Intent.OnCloseWindowRequest -> {
......@@ -32,7 +37,11 @@ class FreeSelectRoomComponent(
}
private fun freeRoom() = scope.launch {
onRemoveEvent(eventInfo)
mutableState.update { it.copy(isLoad = true) }
freeUpRoomUseCase(
roomName = roomName,
eventInfo = eventInfo,
)
onCloseRequest()
mutableState.update { State.defaultState }
}
......
......@@ -30,7 +30,6 @@ import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import io.github.aakira.napier.Napier
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.delay
......@@ -270,23 +269,11 @@ class MainComponent(
return FreeSelectRoomComponent(
componentContext = componentContext,
eventInfo = config.event,
onRemoveEvent = ::handleRemoveEvent,
roomName = getCurrentRoomName(),
onCloseRequest = navigation::dismiss,
)
}
/**
* Handles removing an event from a room.
*/
private fun handleRemoveEvent(event: EventInfo) {
coroutineScope.launch {
freeUpRoomUseCase(
roomName = getCurrentRoomName(),
eventInfo = event
)
}
}
/**
* Creates a BookingEditorComponent.
*/
......
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать