main #6

Closed
student-20690 wants to merge 20 commits from (deleted):main into main
5 changed files with 476 additions and 22 deletions
Showing only changes of commit 46b82ee353 - Show all commits

View File

@@ -2,10 +2,7 @@ package ru.myitschool.work.ui.screen
import androidx.compose.animation.EnterTransition import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExitTransition
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
@@ -36,10 +33,21 @@ fun AppNavHost(
AuthScreen(navController = navController) AuthScreen(navController = navController)
} }
composable<MainScreenDestination> { composable<MainScreenDestination> {
MainScreen(navController = navController) MainScreen(
navController = navController,
onNavigateToBooking = {
navController.navigate(BookScreenDestination)
}
)
} }
composable<BookScreenDestination> { composable<BookScreenDestination> {
BookScreen(navController = navController) BookScreen(
onBack = { navController.popBackStack() },
onBookingSuccess = {
// Возвращаемся на главный экран и обновляем его
navController.popBackStack()
}
)
} }
} }
} }

View File

@@ -22,7 +22,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign

View File

@@ -1,16 +1,168 @@
package ru.myitschool.work.ui.screen.book package ru.myitschool.work.ui.screen.book
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.navigation.NavController 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.selection.selectable
import androidx.compose.material3.Button
import androidx.compose.material3.RadioButton
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable @Composable
fun BookScreen(navController: NavController){ fun BookingScreen(
Box( uiState: BookingUiState, // состояние интерфейса
contentAlignment = Alignment.Center onSelectDate: (LocalDate) -> Unit, // callback при выборе даты
) { onSelectPlace: (String) -> Unit, // callback при выборе места
Text(text = "Hello") onBook: () -> Unit, // callback при бронировании
onBack: () -> Unit, // callback при нажатии "Назад"
onRefresh: () -> Unit // callback при обновлении
) {
// Сортировка дат по порядку
val sortedDates = uiState.dates.sorted()
// Фильтрация дат, для которых есть доступные места
val availableDates = sortedDates.filter { date -> uiState.places[date]?.isNotEmpty() == true }
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
// Вкладки для выбора дат
if (availableDates.isNotEmpty()) {
ScrollableTabRow(selectedTabIndex = availableDates.indexOf(uiState.selectedDate)) {
availableDates.forEachIndexed { index, date ->
Tab(
selected = date == uiState.selectedDate,
onClick = { onSelectDate(date) },
text = {
Text(
text = date.format(DateTimeFormatter.ofPattern("dd.MM")),
modifier = Modifier.testTag("book_date_pos_$index")
)
},
modifier = Modifier.testTag("book_date")
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// Список мест для выбранной даты
val placesForDate = uiState.selectedDate?.let { uiState.places[it] } ?: emptyList()
if (placesForDate.isNotEmpty()) {
Column {
placesForDate.forEachIndexed { index, place ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
.selectable(
selected = uiState.selectedPlace == place,
onClick = { onSelectPlace(place) }
)
.testTag("book_place_pos_$index"),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = place,
modifier = Modifier.weight(1f).testTag("book_place_text")
)
RadioButton(
selected = uiState.selectedPlace == place,
onClick = { onSelectPlace(place) },
modifier = Modifier.testTag("book_place_selector")
)
}
}
}
}
// пустой список (все забронировано)
if (availableDates.isEmpty() && !uiState.isError) {
Text(
text = "Всё забронировано",
modifier = Modifier.testTag("book_empty")
)
}
// ошибка
if (uiState.isError) {
Text(
text = uiState.errorMessage ?: "Ошибка загрузки",
color = Color.Red,
modifier = Modifier.testTag("book_error")
)
Button(
onClick = onRefresh,
modifier = Modifier.testTag("book_refresh_button")
) {
Text("Обновить")
}
}
Spacer(modifier = Modifier.weight(1f))
// Кнопки: Забронировать и Назад
if (!uiState.isError && placesForDate.isNotEmpty()) {
Button(
onClick = onBook,
enabled = uiState.selectedPlace != null, // активна только при выбранном месте
modifier = Modifier.fillMaxWidth().testTag("book_book_button")
) { Text("Забронировать") }
}
Button(
onClick = onBack,
modifier = Modifier.fillMaxWidth().padding(top = 8.dp).testTag("book_back_button")
) {
Text("Назад")
}
} }
} }
// Модель состояния интерфейса
data class BookingUiState(
val dates: List<LocalDate> = emptyList(), // список доступных дат
val places: Map<LocalDate, List<String>> = emptyMap(), // места по датам
val selectedDate: LocalDate? = null, // выбранная дата
val selectedPlace: String? = null, // выбранное место
val isError: Boolean = false, // флаг ошибки
val errorMessage: String? = null // сообщение об ошибке
)
@Composable
fun BookScreen(
onBack: () -> Unit, // callback при возврате назад
onBookingSuccess: () -> Unit // callback при успешном бронировании
) {
val viewModel: BookingViewModel = viewModel()
val uiState by viewModel.uiState.collectAsState()
BookingScreen(
uiState = uiState,
onSelectDate = { date -> viewModel.selectDate(date) },
onSelectPlace = { place -> viewModel.selectPlace(place) },
onBook = {
viewModel.bookPlace()
onBookingSuccess()
},
onBack = onBack,
onRefresh = { viewModel.refresh() }
)
}

View File

@@ -0,0 +1,87 @@
package ru.myitschool.work.ui.screen.book
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.time.LocalDate
class BookingViewModel : ViewModel() {
private val _uiState = MutableStateFlow(BookingUiState())
val uiState: StateFlow<BookingUiState> = _uiState.asStateFlow()
init {
loadBookingData()
}
fun loadBookingData() {
viewModelScope.launch {
try {
// Временные mock данные
val mockDates = listOf(
LocalDate.now().plusDays(1),
LocalDate.now().plusDays(2),
LocalDate.now().plusDays(3)
)
val mockPlaces = mapOf(
mockDates[0] to listOf("Место 1", "Место 2", "Место 3"),
mockDates[1] to listOf("Место 1", "Место 2"),
mockDates[2] to listOf("Место 1")
)
val sortedDates = mockDates.sorted()
val availableDates = sortedDates.filter { mockPlaces[it]?.isNotEmpty() == true }
val defaultDate = availableDates.firstOrNull()
_uiState.value = _uiState.value.copy(
dates = sortedDates,
places = mockPlaces,
selectedDate = defaultDate,
selectedPlace = null,
isError = false,
errorMessage = null
)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isError = true,
errorMessage = "Ошибка загрузки данных"
)
}
}
}
fun selectDate(date: LocalDate) {
_uiState.value = _uiState.value.copy(
selectedDate = date,
selectedPlace = null
)
}
fun selectPlace(place: String) {
_uiState.value = _uiState.value.copy(
selectedPlace = place
)
}
fun bookPlace() {
viewModelScope.launch {
try {
//вызов API для бронирования
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isError = true,
errorMessage = "Ошибка бронирования"
)
}
}
}
fun refresh() {
loadBookingData()
}
}

View File

@@ -1,17 +1,225 @@
package ru.myitschool.work.ui.screen.main package ru.myitschool.work.ui.screen.main
import androidx.compose.foundation.layout.Box import android.util.Log
import androidx.compose.material3.Text import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import kotlinx.coroutines.launch
import ru.myitschool.work.core.TestIds
import java.text.SimpleDateFormat
import java.util.*
import ru.myitschool.work.ui.nav.BookScreenDestination
// Модель данных для бронирования
data class BookingItem(
val date: String, // Формат "dd.MM.yyyy"
val place: String,
val id: Int
)
@Composable @Composable
fun MainScreen(navController: NavController) { fun MainScreen(
Box( navController: NavController,
contentAlignment = Alignment.Center onNavigateToBooking: () -> Unit
) { ) {
Text(text = "Hello") // Состояния
var userName by remember { mutableStateOf("Иван Иванов") }
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf("") }
var bookingItems by remember { mutableStateOf(emptyList<BookingItem>()) }
var hasError by remember { mutableStateOf(false) }
// Для корутин
val coroutineScope = rememberCoroutineScope()
// Функция загрузки данных
fun loadData() {
isLoading = true
hasError = false
coroutineScope.launch {
kotlinx.coroutines.delay(1000) // Имитация задержки
// Имитация ответа от сервера
val response = listOf(
BookingItem("20.12.2023", "Конференц-зал А", 1),
BookingItem("15.12.2023", "Переговорная Б", 2),
BookingItem("25.12.2023", "Спортзал", 3)
)
// Сортировка по дате (увеличение)
bookingItems = response.sortedBy {
SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()).parse(it.date)
}
isLoading = false
}
}
// Первая загрузка при открытии экрана
LaunchedEffect(Unit) {
loadData()
}
// Если ошибка - показываем только ошибку и кнопку обновления
if (hasError) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// Текстовое поле с ошибкой (main_error)
Text(
text = errorMessage,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.testTag(TestIds.Main.ERROR)
)
Spacer(modifier = Modifier.height(16.dp))
// Кнопка обновления (main_refresh_button)
Button(onClick = { loadData() }) {
Text("Обновить")
}
}
} else {
// Нормальное состояние
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
// Верхняя строка
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
// Фото пользователя (main_photo)
Image(
painter = painterResource(id = android.R.drawable.ic_menu_gallery),
contentDescription = "Фото",
modifier = Modifier.size(64.dp).testTag(TestIds.Main.PROFILE_IMAGE)
)
Spacer(modifier = Modifier.width(16.dp))
// Имя пользователя (main_name)
Text(
text = userName,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.weight(1f).testTag(TestIds.Main.PROFILE_NAME),
color = MaterialTheme.colorScheme.onSurface
)
// Кнопка выхода (main_logout_button)
Button(onClick = {
// Очистка данных и переход на авторизацию
userName = ""
bookingItems = emptyList()
navController.navigate("auth") { popUpTo(0) }
},
modifier = Modifier.testTag(TestIds.Main.LOGOUT_BUTTON)
) {
Text("Выход")
}
}
Spacer(modifier = Modifier.height(16.dp))
// Кнопки действий
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
// Кнопка обновления (main_refresh_button)
Button(
onClick = { loadData() },
enabled = !isLoading,
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON)
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
color = MaterialTheme.colorScheme.onPrimary
)
} else {
Text("Обновить")
}
}
// кнопка бронирования
Button(
onClick = { navController.navigate(BookScreenDestination)
},
modifier = Modifier.testTag(TestIds.Main.ADD_BUTTON)
) {
Text("Перейти к бронированию")
}
}
Spacer(modifier = Modifier.height(16.dp))
// Список бронирований
if (bookingItems.isNotEmpty()) {
LazyColumn(
modifier = Modifier.weight(1f)
) {
itemsIndexed(bookingItems) { index, item ->
// Элемент списка (main_book_pos_{index})
Log.d("Nicoly", index.toString())
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(modifier = Modifier.padding(16.dp).testTag(TestIds.Main.getIdItemByPosition(index))) {
// Дата бронирования (main_item_date)
Text(
text = "Дата: ${item.date}",
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.testTag(TestIds.Main.ITEM_DATE)
)
Spacer(modifier = Modifier.height(4.dp))
// Место бронирования (main_item_place)
Text(
text = "Место: ${item.place}",
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE)
)
}
}
}
}
} else {
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Text(
text = "Нет бронирований",
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
}
}
}
} }
} }