main #6

Closed
student-20690 wants to merge 20 commits from (deleted):main into main
27 changed files with 544 additions and 129 deletions
Showing only changes of commit fea1cbeaca - Show all commits

View File

@@ -1,7 +1,7 @@
package ru.myitschool.work.core package ru.myitschool.work.core
object Constants { object Constants {
const val HOST = "http://10.0.2.2:8080" const val HOST = "http://192.168.1.39:8080"
const val AUTH_URL = "/auth" const val AUTH_URL = "/auth"
const val INFO_URL = "/info" const val INFO_URL = "/info"
const val BOOKING_URL = "/booking" const val BOOKING_URL = "/booking"

View File

@@ -0,0 +1,9 @@
package ru.myitschool.work.core
import androidx.datastore.preferences.core.intPreferencesKey
// Не добавляйте ничего, что уже есть в Constants!
object OurConstants {
const val SHABLON = "^[a-zA-Z0-9]*\$"
const val DS_AUTH_KEY = "authkey"
}

View File

@@ -0,0 +1,11 @@
package ru.myitschool.work.core
import ru.myitschool.work.core.OurConstants.SHABLON
class Utils {
companion object {
fun CheckCodeInput(text : String) : Boolean{
return !text.isEmpty() && text.length == 4 && text.matches(Regex(SHABLON))
}
}
}

View File

@@ -0,0 +1,11 @@
package ru.myitschool.work.data.entity
import java.time.LocalDate
data class Booking ( val id: Long,
val date: LocalDate,
val place: Place,
val employeeCode: String){
}

View File

@@ -0,0 +1,9 @@
package ru.myitschool.work.data.entity
data class Employee (
val name: String,
val code: String,
val photoUrl: String,
val bookingList: MutableList<Booking?>) {
}

View File

@@ -0,0 +1,5 @@
package ru.myitschool.work.data.entity
data class Place(
val id: Long,
val place: String ){}

View File

@@ -1,16 +1,21 @@
package ru.myitschool.work.data.repo package ru.myitschool.work.data.repo
import android.content.Context
import ru.myitschool.work.data.source.DataStoreDataSource.createAuthCode
import ru.myitschool.work.data.source.NetworkDataSource import ru.myitschool.work.data.source.NetworkDataSource
object AuthRepository { object AuthRepository {
private var codeCache: String? = null private var codeCache: String? = null
suspend fun checkAndSave(text: String): Result<Boolean> { suspend fun checkAndSave(text: String): Result<Boolean> {
return NetworkDataSource.checkAuth(text).onSuccess { success -> /* return NetworkDataSource.checkAuth(text).onSuccess { success ->
if (success) { if (success) {
codeCache = text codeCache = text
createAuthCode(code = text)
} }
} }
} */
codeCache = text
createAuthCode(code = text)
return Result.success(true) // TODO: ВЕРНУТЬ СЕТЕВОЙ ЗАПРОС
} }
} }

View File

@@ -0,0 +1,34 @@
package ru.myitschool.work.data.repo
import ru.myitschool.work.data.entity.Employee
import ru.myitschool.work.data.source.DataStoreDataSource
import ru.myitschool.work.data.source.DataStoreDataSource.createAuthCode
import ru.myitschool.work.data.source.DataStoreDataSource.getAuthCode
import ru.myitschool.work.data.source.NetworkDataSource
class MainRepository {
private var employee: Employee? = null
suspend fun getUserInfo(): Result<Employee> {
return try {
val code = getCode()
val result = NetworkDataSource.getUserInfo(code)
result.onSuccess { success ->
employee = success
}
result
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun getCode(): String {
return getAuthCode()
}
suspend fun logOut(){
DataStoreDataSource.logOut()
}
}

View File

@@ -0,0 +1,49 @@
package ru.myitschool.work.data.source
import android.content.Context
import android.util.Log
import androidx.compose.material3.rememberTimePickerState
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import ru.myitschool.work.App
import ru.myitschool.work.core.OurConstants.DS_AUTH_KEY
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "auth")
val AUTH_KEY = stringPreferencesKey(DS_AUTH_KEY)
object DataStoreDataSource {
fun authFlow(): Flow<String> {
Log.d("AnnaKonda", "Code is checking")
return App.context.dataStore.data.map { preferences ->
(preferences[AUTH_KEY] ?: 0).toString()
}
}
suspend fun createAuthCode(code: String) {
App.context.dataStore.updateData {
it.toMutablePreferences().also { preferences ->
preferences[AUTH_KEY] = code
}
}
}
suspend fun getAuthCode(): String {
return App.context.dataStore.data.map { preferences ->
preferences[AUTH_KEY] ?: ""
}.first()
}
suspend fun logOut() {
App.context.dataStore.updateData {
it.toMutablePreferences().also { preferences ->
preferences.remove(AUTH_KEY)
}
}
}
}

View File

@@ -11,6 +11,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.myitschool.work.core.Constants import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.entity.Employee
import kotlinx.serialization.json.*
import ru.myitschool.work.data.entity.Booking
import ru.myitschool.work.data.entity.Place
import java.time.LocalDate
object NetworkDataSource { object NetworkDataSource {
private val client by lazy { private val client by lazy {
@@ -30,13 +35,76 @@ object NetworkDataSource {
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) { suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
return@withContext runCatching { return@withContext runCatching {
val response = client.get(getUrl(code, Constants.AUTH_URL)) val response = client.get(getUrl(code, Constants.AUTH_URL)) // TODO: Отпрвка запроса на сервер
when (response.status) { when (response.status) {
HttpStatusCode.OK -> true HttpStatusCode.OK -> true
else -> error(response.bodyAsText()) else -> error(response.bodyAsText())
} }
} }
} }
suspend fun getUserInfo(code: String): Result<Employee> = withContext(Dispatchers.IO) {
return@withContext runCatching {
val response = client.get(getUrl(code, Constants.INFO_URL))
when (response.status) {
HttpStatusCode.OK -> {
val json = response.bodyAsText()
if (json.isBlank()) {
error("Пустой ответ от сервера")
}
val jsonObject = try {
Json.parseToJsonElement(json).jsonObject
} catch (e: Exception) {
error("Ошибка парсинга: ${e.message}")
}
val name = jsonObject["name"]?.jsonPrimitive?.content
?: error("Отсутствует поле 'name'")
val photoUrl = jsonObject["photoUrl"]?.jsonPrimitive?.content
?: error("Отсутствует поле 'photoUrl'")
val bookingJson = jsonObject["booking"]?.jsonObject
?: error("Отсутствует поле 'booking' в ответе")
val employee = Employee(
name = name,
code = code,
photoUrl = photoUrl,
bookingList = mutableListOf()
)
val bookingList = mutableListOf<Booking>()
for ((dateString, bookingElement) in bookingJson) {
val date = LocalDate.parse(dateString)
val bookingObj = bookingElement.jsonObject
val bookingId = bookingObj["id"]?.jsonPrimitive?.long
?: error("Отсутствует поле id")
val placeString = bookingObj["place"]?.jsonPrimitive?.content
?: error("Отсутствует поле 'place' $dateString")
if (placeString.isBlank()) {
error("Пустое поле 'place' $dateString")
}
val placeId = bookingId
val place = Place(placeId, placeString)
val booking = Booking(
id = bookingId,
date = date,
place = place,
employeeCode = employee.code
)
bookingList.add(booking)
}
if (bookingList.isEmpty()) {
error("Список бронирований пуст")
}
employee.bookingList.addAll(bookingList)
employee
}
else -> error(response.bodyAsText())
}
}
}
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl" private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl"
} }

View File

@@ -0,0 +1,13 @@
package ru.myitschool.work.domain.main
import ru.myitschool.work.data.entity.Employee
import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.data.repo.MainRepository
class GetUserDataUseCase(
private val repository: MainRepository
) {
suspend operator fun invoke(): Result<Employee> {
return repository.getUserInfo()
}
}

View File

@@ -8,12 +8,15 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import ru.myitschool.work.App
import ru.myitschool.work.data.source.DataStoreDataSource.authFlow
import ru.myitschool.work.ui.screen.AppNavHost import ru.myitschool.work.ui.screen.AppNavHost
import ru.myitschool.work.ui.theme.WorkTheme import ru.myitschool.work.ui.theme.WorkTheme
class RootActivity : ComponentActivity() { class RootActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
App.context = applicationContext
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
WorkTheme { WorkTheme {

View File

@@ -8,10 +8,12 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import ru.myitschool.work.data.source.DataStoreDataSource.authFlow
import ru.myitschool.work.ui.nav.AuthScreenDestination import ru.myitschool.work.ui.nav.AuthScreenDestination
import ru.myitschool.work.ui.nav.BookScreenDestination import ru.myitschool.work.ui.nav.BookScreenDestination
import ru.myitschool.work.ui.nav.MainScreenDestination import ru.myitschool.work.ui.nav.MainScreenDestination
import ru.myitschool.work.ui.screen.auth.AuthScreen import ru.myitschool.work.ui.screen.auth.AuthScreen
import ru.myitschool.work.ui.screen.auth.AuthViewModel
import ru.myitschool.work.ui.screen.book.BookScreen import ru.myitschool.work.ui.screen.book.BookScreen
import ru.myitschool.work.ui.screen.main.MainScreen import ru.myitschool.work.ui.screen.main.MainScreen
@@ -31,12 +33,7 @@ fun AppNavHost(
AuthScreen(navController = navController) AuthScreen(navController = navController)
} }
composable<MainScreenDestination> { composable<MainScreenDestination> {
MainScreen( MainScreen(navController = navController)
navController = navController,
onNavigateToBooking = {
navController.navigate(BookScreenDestination)
}
)
} }
composable<BookScreenDestination> { composable<BookScreenDestination> {
BookScreen( BookScreen(

View File

@@ -0,0 +1,7 @@
package ru.myitschool.work.ui.screen.auth
sealed interface AuthAction {
data class ShowError(val message: String?) : AuthAction
data class LogIn(val isLogged: Boolean): AuthAction
data class AuthBtnEnabled(val enabled: Boolean) : AuthAction
}

View File

@@ -3,4 +3,5 @@ package ru.myitschool.work.ui.screen.auth
sealed interface AuthIntent { sealed interface AuthIntent {
data class Send(val text: String): AuthIntent data class Send(val text: String): AuthIntent
data class TextInput(val text: String): AuthIntent data class TextInput(val text: String): AuthIntent
object CheckLogIntent: AuthIntent
} }

View File

@@ -1,5 +1,6 @@
package ru.myitschool.work.ui.screen.auth package ru.myitschool.work.ui.screen.auth
import android.util.Log
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -27,8 +28,12 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import io.ktor.util.collections.setValue
import ru.myitschool.work.App
import ru.myitschool.work.R import ru.myitschool.work.R
import ru.myitschool.work.core.OurConstants.SHABLON
import ru.myitschool.work.core.TestIds import ru.myitschool.work.core.TestIds
import ru.myitschool.work.core.Utils
import ru.myitschool.work.ui.nav.MainScreenDestination import ru.myitschool.work.ui.nav.MainScreenDestination
@Composable @Composable
@@ -37,13 +42,20 @@ fun AuthScreen(
navController: NavController navController: NavController
) { ) {
val state by viewModel.uiState.collectAsState() val state by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.actionFlow.collect { viewModel.onIntent(AuthIntent.CheckLogIntent)
navController.navigate(MainScreenDestination)
}
} }
val event = viewModel.actionFlow.collectAsState(initial = null)
LaunchedEffect(event.value) {
if (event.value is AuthAction.LogIn) {
if ((event.value as AuthAction.LogIn).isLogged) {
navController.navigate(MainScreenDestination)
}
}
}
Log.d("AnnaKonda", state.javaClass.toString())
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -63,6 +75,10 @@ fun AuthScreen(
modifier = Modifier.size(64.dp) modifier = Modifier.size(64.dp)
) )
} }
is AuthState.LoggedIn -> {
navController.navigate(MainScreenDestination)
}
} }
} }
} }
@@ -73,9 +89,25 @@ private fun Content(
state: AuthState.Data state: AuthState.Data
) { ) {
var inputText by remember { mutableStateOf("") } var inputText by remember { mutableStateOf("") }
var errorText: String? by remember { mutableStateOf(null) }
var btnEnabled: Boolean by remember { mutableStateOf(false) }
val event = viewModel.actionFlow.collectAsState(initial = null)
LaunchedEffect(event.value) {
if (event.value is AuthAction.ShowError) {
errorText = (event.value as AuthAction.ShowError).message
} else if (event.value is AuthAction.AuthBtnEnabled){
Log.d("AnnaKonda", btnEnabled.toString())
btnEnabled = if ((event.value as AuthAction.AuthBtnEnabled).enabled){ true } else { false }
}
}
Spacer(modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.size(16.dp))
TextField( TextField(
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(), modifier = Modifier
.testTag(TestIds.Auth.CODE_INPUT)
.fillMaxWidth(),
value = inputText, value = inputText,
onValueChange = { onValueChange = {
inputText = it inputText = it
@@ -85,14 +117,20 @@ private fun Content(
) )
Spacer(modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.size(16.dp))
Button( Button(
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(), modifier = Modifier
.testTag(TestIds.Auth.SIGN_BUTTON)
.fillMaxWidth(),
onClick = { onClick = {
viewModel.onIntent(AuthIntent.Send(inputText)) if (Utils.CheckCodeInput(inputText)) {
viewModel.onIntent(AuthIntent.Send(inputText))
} else {
errorText = App.context.getString(R.string.auth_nasty_code)
}
}, },
enabled = true enabled = btnEnabled
) { Text(stringResource(R.string.auth_sign_in))
) { Text(stringResource(R.string.auth_sign_in)) }
if (errorText != null) {
Text(errorText.toString(), modifier = Modifier.testTag(TestIds.Auth.ERROR))
} }
} }

View File

@@ -3,4 +3,5 @@ package ru.myitschool.work.ui.screen.auth
sealed interface AuthState { sealed interface AuthState {
object Loading: AuthState object Loading: AuthState
object Data: AuthState object Data: AuthState
object LoggedIn: AuthState
} }

View File

@@ -1,5 +1,7 @@
package ru.myitschool.work.ui.screen.auth package ru.myitschool.work.ui.screen.auth
import android.util.Log
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -10,7 +12,11 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.myitschool.work.App
import ru.myitschool.work.R
import ru.myitschool.work.core.Utils.Companion.CheckCodeInput
import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.data.source.DataStoreDataSource.authFlow
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
class AuthViewModel : ViewModel() { class AuthViewModel : ViewModel() {
@@ -18,26 +24,55 @@ class AuthViewModel : ViewModel() {
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data) private val _uiState = MutableStateFlow<AuthState>(AuthState.Data)
val uiState: StateFlow<AuthState> = _uiState.asStateFlow() val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow() private val _actionFlow: MutableSharedFlow<AuthAction> = MutableSharedFlow()
val actionFlow: SharedFlow<Unit> = _actionFlow val actionFlow: SharedFlow<AuthAction> = _actionFlow
fun onIntent(intent: AuthIntent) { fun onIntent(intent: AuthIntent) {
when (intent) { when (intent) {
is AuthIntent.Send -> { is AuthIntent.Send -> {
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.IO) {
_uiState.update { AuthState.Loading } _uiState.update { AuthState.Loading }
checkAndSaveAuthCodeUseCase.invoke("9999").fold( checkAndSaveAuthCodeUseCase.invoke(intent.text).fold(
onSuccess = { onSuccess = {
_actionFlow.emit(Unit) _uiState.update { AuthState.LoggedIn }
}, },
onFailure = { error -> onFailure = { error ->
error.printStackTrace() error.printStackTrace()
_actionFlow.emit(Unit) if (error.message != null) {
_actionFlow.emit(AuthAction.ShowError(error.message.toString()))
}
_uiState.update { AuthState.Data }
} }
) )
} }
} }
is AuthIntent.TextInput -> Unit
is AuthIntent.TextInput -> {
viewModelScope.launch {
authFlow().collect {
if (CheckCodeInput(intent.text)) {
_actionFlow.emit(AuthAction.AuthBtnEnabled(true))
} else {
_actionFlow.emit(AuthAction.AuthBtnEnabled(false))
}
}
}
}
is AuthIntent.CheckLogIntent -> {
viewModelScope.launch {
_uiState.update { AuthState.Loading }
authFlow().collect {
Log.d("AnnaKonda", it)
if (it != "0") {
_actionFlow.emit(AuthAction.LogIn(true))
_uiState.update { AuthState.LoggedIn }
} else {
_actionFlow.emit(AuthAction.ShowError(App.context.getString(R.string.auth_wrong_code)))
_uiState.update { AuthState.Data }
}
}
}
}
} }
} }
} }

View File

@@ -28,7 +28,7 @@ import ru.myitschool.work.core.TestIds
@Composable @Composable
fun BookingScreen( fun BookingScreen(
uiState: BookingUiState, // состояние интерфейса uiState: BookingState, // состояние интерфейса
onSelectDate: (LocalDate) -> Unit, // callback при выборе даты onSelectDate: (LocalDate) -> Unit, // callback при выборе даты
onSelectPlace: (String) -> Unit, // callback при выборе места onSelectPlace: (String) -> Unit, // callback при выборе места
onBook: () -> Unit, // callback при бронировании onBook: () -> Unit, // callback при бронировании
@@ -139,22 +139,15 @@ fun BookingScreen(
} }
} }
// Модель состояния интерфейса
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 @Composable
fun BookScreen( fun BookScreen(
onBack: () -> Unit, // callback при возврате назад onBack: () -> Unit, // callback при возврате назад
onBookingSuccess: () -> Unit // callback при успешном бронировании onBookingSuccess: () -> Unit // callback при успешном бронировании
) { ) {
val viewModel: BookingViewModel = viewModel() val viewModel: BookingViewModel = BookingViewModel()
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
BookingScreen( BookingScreen(

View File

@@ -0,0 +1,12 @@
package ru.myitschool.work.ui.screen.book
import java.time.LocalDate
data class BookingState(
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 // сообщение об ошибке
)

View File

@@ -10,8 +10,8 @@ import java.time.LocalDate
class BookingViewModel : ViewModel() { class BookingViewModel : ViewModel() {
private val _uiState = MutableStateFlow(BookingUiState()) private val _uiState = MutableStateFlow(BookingState())
val uiState: StateFlow<BookingUiState> = _uiState.asStateFlow() val uiState: StateFlow<BookingState> = _uiState.asStateFlow()
init { init {
loadBookingData() loadBookingData()

View File

@@ -0,0 +1,8 @@
package ru.myitschool.work.ui.screen.main
import ru.myitschool.work.ui.screen.auth.AuthAction
sealed interface MainAction {
data class SetName(val name: String)
data class ShowError(val message: String?) : MainAction
}

View File

@@ -0,0 +1,9 @@
package ru.myitschool.work.ui.screen.main
sealed interface MainIntent {
/* data class Send(val text: String): AuthIntent
data class TextInput(val text: String): AuthIntent
object CheckLogIntent: AuthIntent*/
object LoadData: MainIntent
object LogOut: MainIntent
}

View File

@@ -1,100 +1,81 @@
package ru.myitschool.work.ui.screen.main package ru.myitschool.work.ui.screen.main
import android.util.Log import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
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.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import kotlinx.coroutines.launch import coil3.compose.AsyncImage
import ru.myitschool.work.core.TestIds import ru.myitschool.work.core.TestIds
import java.text.SimpleDateFormat import ru.myitschool.work.data.entity.Booking
import java.util.* import ru.myitschool.work.data.entity.Employee
import ru.myitschool.work.ui.nav.AuthScreenDestination
import ru.myitschool.work.ui.nav.BookScreenDestination 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( fun MainScreen(
navController: NavController, navController: NavController,
onNavigateToBooking: () -> Unit
) { ) {
val viewModel = MainViewModel()
// Состояния // Состояния
var userName by remember { mutableStateOf("Иван Иванов") } val event = viewModel.actionFlow.collectAsState(initial = null)
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) { LaunchedEffect(Unit) {
loadData() viewModel.onIntent(MainIntent.LoadData)
} }
var errorMessage: String? by remember { mutableStateOf("") }
LaunchedEffect(event.value) {
if (event.value is MainAction.ShowError) {
errorMessage = (event.value as MainAction.ShowError).message
}
}
Log.d("AnnaKonda", errorMessage.toString())
// Если ошибка - показываем только ошибку и кнопку обновления // Если ошибка - показываем только ошибку и кнопку обновления
if (hasError) { if (errorMessage != null) {
Column( ErrorScreen(viewModel = viewModel, navController = navController, errorMessage)
modifier = Modifier.fillMaxSize(), } else {
horizontalAlignment = Alignment.CenterHorizontally, DefaultScreen(viewModel = viewModel, navController = navController)
verticalArrangement = Arrangement.Center }
) { }
// Текстовое поле с ошибкой (main_error) @Composable
Text( fun DefaultScreen(viewModel: MainViewModel,
text = errorMessage, navController: NavController){
color = MaterialTheme.colorScheme.error, val state by viewModel.uiState.collectAsState()
modifier = Modifier.testTag(TestIds.Main.ERROR) var employee : Employee? by remember { mutableStateOf(null) }
var errorMessage by remember { mutableStateOf("") }
var bookingItems : List<Booking?>? by remember { mutableStateOf(emptyList<Booking>()) }
var isLoading by remember { mutableStateOf(true) }
) LaunchedEffect(state) {
when (state) {
Spacer(modifier = Modifier.height(16.dp)) is MainState.Loading -> {
errorMessage = ""
// Кнопка обновления (main_refresh_button) isLoading = true
Button(onClick = { loadData() }) { }
Text("Обновить") is MainState.Data -> {
isLoading = false
employee = (state as MainState.Data).employee
if (employee == null){
navController.navigate(AuthScreenDestination) { popUpTo(0) }
} else {
bookingItems = employee?.bookingList?.sortedBy { item ->
item?.date
}
}
} }
} }
} else { }
// Нормальное состояние employee?.let {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -106,29 +87,34 @@ fun MainScreen(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// Фото пользователя (main_photo) // Фото пользователя (main_photo)
Image( employee?.photoUrl?.let { msg -> Log.d("AnnaKonda", msg) }
painter = painterResource(id = android.R.drawable.ic_menu_gallery), AsyncImage(
model = employee?.photoUrl ?: "",
contentDescription = "Фото", contentDescription = "Фото",
modifier = Modifier.size(64.dp).testTag(TestIds.Main.PROFILE_IMAGE) modifier = Modifier
.size(64.dp)
.clip(CircleShape)
.testTag(TestIds.Main.PROFILE_IMAGE),
error = painterResource(id = android.R.drawable.ic_menu_gallery)
) )
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
// Имя пользователя (main_name) // Имя пользователя (main_name)
Text( Text(
text = userName, text = employee!!.name,
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
modifier = Modifier.weight(1f).testTag(TestIds.Main.PROFILE_NAME), modifier = Modifier.weight(1f).testTag(TestIds.Main.PROFILE_NAME),
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )
// Кнопка выхода (main_logout_button) // Кнопка выхода (main_logout_button)
Button(onClick = { Button(
// Очистка данных и переход на авторизацию onClick = {
userName = "" // Очистка данных и переход на авторизацию
bookingItems = emptyList() viewModel.onIntent(MainIntent.LogOut)
navController.navigate("auth") { popUpTo(0) } bookingItems = emptyList()
}, },
modifier = Modifier.testTag(TestIds.Main.LOGOUT_BUTTON) modifier = Modifier.testTag(TestIds.Main.LOGOUT_BUTTON)
) { ) {
Text("Выход") Text("Выход")
@@ -145,11 +131,11 @@ fun MainScreen(
) { ) {
// Кнопка обновления (main_refresh_button) // Кнопка обновления (main_refresh_button)
Button( Button(
onClick = { loadData() }, onClick = { viewModel.onIntent(MainIntent.LoadData) },
enabled = !isLoading, enabled = state !is MainState.Loading,
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON) modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON)
) { ) {
if (isLoading) { if (state is MainState.Loading) {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
color = MaterialTheme.colorScheme.onPrimary color = MaterialTheme.colorScheme.onPrimary
@@ -160,7 +146,8 @@ fun MainScreen(
} }
// кнопка бронирования // кнопка бронирования
Button( Button(
onClick = { navController.navigate(BookScreenDestination) onClick = {
navController.navigate(BookScreenDestination)
}, },
modifier = Modifier.testTag(TestIds.Main.ADD_BUTTON) modifier = Modifier.testTag(TestIds.Main.ADD_BUTTON)
) { ) {
@@ -171,14 +158,13 @@ fun MainScreen(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// Список бронирований // Список бронирований
if (bookingItems.isNotEmpty()) { if (!bookingItems.isNullOrEmpty()) {
LazyColumn( LazyColumn(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
itemsIndexed(bookingItems) { index, item -> itemsIndexed(bookingItems as List<Booking?>) { index, item ->
// Элемент списка (main_book_pos_{index}) // Элемент списка (main_book_pos_{index})
Log.d("Nicoly", index.toString())
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -187,10 +173,13 @@ fun MainScreen(
containerColor = MaterialTheme.colorScheme.surfaceVariant containerColor = MaterialTheme.colorScheme.surfaceVariant
) )
) { ) {
Column(modifier = Modifier.padding(16.dp).testTag(TestIds.Main.getIdItemByPosition(index))) { Column(
modifier = Modifier.padding(16.dp)
.testTag(TestIds.Main.getIdItemByPosition(index))
) {
// Дата бронирования (main_item_date) // Дата бронирования (main_item_date)
Text( Text(
text = "Дата: ${item.date}", text = "Дата: ${item?.date}",
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.testTag(TestIds.Main.ITEM_DATE) modifier = Modifier.testTag(TestIds.Main.ITEM_DATE)
) )
@@ -199,7 +188,7 @@ fun MainScreen(
// Место бронирования (main_item_place) // Место бронирования (main_item_place)
Text( Text(
text = "Место: ${item.place}", text = "Место: ${item?.place?.place}",
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE) modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE)
) )
@@ -223,3 +212,34 @@ fun MainScreen(
} }
} }
} }
@Composable
fun ErrorScreen(viewModel: MainViewModel,
navController: NavController,
errorMessage: String?){
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// Текстовое поле с ошибкой (main_error)
if (errorMessage != null) {
Text(
text = errorMessage,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.testTag(TestIds.Main.ERROR)
)
}
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { viewModel.onIntent(MainIntent.LoadData) }) {
Text("Обновить")
}
}
}

View File

@@ -0,0 +1,9 @@
package ru.myitschool.work.ui.screen.main
import ru.myitschool.work.data.entity.Employee
import ru.myitschool.work.ui.screen.auth.AuthState
sealed interface MainState {
object Loading: MainState
data class Data (val employee: Employee?): MainState
}

View File

@@ -0,0 +1,66 @@
package ru.myitschool.work.ui.screen.main
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.data.repo.MainRepository
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
import ru.myitschool.work.domain.main.GetUserDataUseCase
import ru.myitschool.work.ui.screen.auth.AuthAction
import ru.myitschool.work.ui.screen.auth.AuthIntent
import ru.myitschool.work.ui.screen.auth.AuthState
class MainViewModel : ViewModel() {
init {
loadData()
}
private val repository by lazy{ MainRepository() }
private val getUserDataUseCase by lazy { GetUserDataUseCase(repository) }
private val _uiState = MutableStateFlow<MainState>(MainState.Loading)
val uiState: StateFlow<MainState> = _uiState.asStateFlow()
private val _actionFlow: MutableSharedFlow<MainAction> = MutableSharedFlow()
val actionFlow: SharedFlow<MainAction> = _actionFlow
fun onIntent(intent: MainIntent) {
when (intent) {
is MainIntent.LoadData -> {
loadData()
}
is MainIntent.LogOut -> {
viewModelScope.launch(Dispatchers.IO) {
_uiState.update { MainState.Data(null) }
repository.logOut()
}
}
}
}
fun loadData() {
viewModelScope.launch(Dispatchers.IO) {
_uiState.update { MainState.Loading }
getUserDataUseCase.invoke().fold(
onSuccess = { employee ->
_uiState.update { MainState.Data(employee) }
_actionFlow.emit(MainAction.ShowError(null))
},
onFailure = { error ->
error.printStackTrace()
if (error.message != null) {
_actionFlow.emit(MainAction.ShowError(error.message.toString()))
}
}
)
}
}
}

View File

@@ -4,4 +4,6 @@
<string name="auth_title">Привет! Введи код для авторизации</string> <string name="auth_title">Привет! Введи код для авторизации</string>
<string name="auth_label">Код</string> <string name="auth_label">Код</string>
<string name="auth_sign_in">Войти</string> <string name="auth_sign_in">Войти</string>
<string name="auth_wrong_code">Введён неверный код</string>
<string name="auth_nasty_code">Неправильный формат кода</string>
</resources> </resources>