forked from Olympic/NTO-2025-Android-TeamTask
Compare commits
5 Commits
04ac941ba4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b40749faa1 | |||
| dbe735e541 | |||
| 40a8428a19 | |||
| 9e603f87e6 | |||
| beb48ab41e |
76
.kotlin/errors/errors-1764603791320.log
Normal file
76
.kotlin/errors/errors-1764603791320.log
Normal file
@@ -0,0 +1,76 @@
|
||||
kotlin version: 2.0.21
|
||||
error message: org.jetbrains.kotlin.util.FileAnalysisException: While analysing C:/Users/Admin/AndroidStudioProjects/NTO-2025-Android-TeamTask/app/src/main/java/ru/myitschool/work/App.kt:7:5: java.lang.IllegalArgumentException: source must not be null
|
||||
at org.jetbrains.kotlin.util.AnalysisExceptionsKt.wrapIntoFileAnalysisExceptionIfNeeded(AnalysisExceptions.kt:57)
|
||||
at org.jetbrains.kotlin.fir.FirCliExceptionHandler.handleExceptionOnFileAnalysis(Utils.kt:249)
|
||||
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:46)
|
||||
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.resolveAndCheckFir(firUtils.kt:77)
|
||||
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildResolveAndCheckFirViaLightTree(firUtils.kt:88)
|
||||
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:319)
|
||||
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:118)
|
||||
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:148)
|
||||
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
|
||||
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
|
||||
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
|
||||
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:464)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:73)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:129)
|
||||
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
|
||||
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
|
||||
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
|
||||
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
|
||||
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
|
||||
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
|
||||
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
|
||||
at java.base/java.lang.Thread.run(Unknown Source)
|
||||
Caused by: java.lang.IllegalArgumentException: source must not be null
|
||||
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.requireNotNull(KtDiagnosticReportHelpers.kt:68)
|
||||
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn(KtDiagnosticReportHelpers.kt:39)
|
||||
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn$default(KtDiagnosticReportHelpers.kt:31)
|
||||
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkSourceElement(FirIncompatibleClassExpressionChecker.kt:50)
|
||||
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkType$checkers(FirIncompatibleClassExpressionChecker.kt:42)
|
||||
at org.jetbrains.kotlin.fir.analysis.checkers.type.FirIncompatibleClassTypeChecker.check(FirIncompatibleClassTypeChecker.kt:17)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.check(TypeCheckersDiagnosticComponent.kt:81)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:53)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:19)
|
||||
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.CheckerRunningDiagnosticCollectorVisitor.checkElement(CheckerRunningDiagnosticCollectorVisitor.kt:24)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:248)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
|
||||
at org.jetbrains.kotlin.fir.declarations.impl.FirSimpleFunctionImpl.acceptChildren(FirSimpleFunctionImpl.kt:63)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:118)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.declarations.FirSimpleFunction.accept(FirSimpleFunction.kt:51)
|
||||
at org.jetbrains.kotlin.fir.declarations.impl.FirRegularClassImpl.acceptChildren(FirRegularClassImpl.kt:63)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitClassAndChildren(AbstractDiagnosticCollectorVisitor.kt:87)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:92)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.declarations.FirRegularClass.accept(FirRegularClass.kt:48)
|
||||
at org.jetbrains.kotlin.fir.declarations.impl.FirFileImpl.acceptChildren(FirFileImpl.kt:57)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:1151)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.declarations.FirFile.accept(FirFile.kt:42)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollector.collectDiagnostics(AbstractDiagnosticCollector.kt:36)
|
||||
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:34)
|
||||
... 33 more
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# НТО 2025. II отборочный этап. Командные задания — Android
|
||||
# НТО 2025. II отборочный этап. Командные задания — Android
|
||||
|
||||
## 📖 Предыстория
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ object DateUtils {
|
||||
private val bookFormatter: DateTimeFormatter =
|
||||
DateTimeFormatter.ofPattern("dd.MM")
|
||||
|
||||
/** Пытаемся распарсить дату в нескольких форматах */
|
||||
fun parseDate(raw: String): LocalDate? {
|
||||
return runCatching { LocalDate.parse(raw) }.getOrElse {
|
||||
runCatching { OffsetDateTime.parse(raw).toLocalDate() }.getOrElse {
|
||||
@@ -22,13 +21,13 @@ object DateUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/** Формат для главного экрана: dd.MM.yyyy */
|
||||
// Формат для главного экрана: dd.MM.yyyy
|
||||
fun formatForMain(raw: String): String {
|
||||
val date = parseDate(raw) ?: return raw
|
||||
return date.format(mainFormatter)
|
||||
}
|
||||
|
||||
/** Формат для экрана бронирования: dd.MM */
|
||||
// Формат для экрана бронирования: dd.MM
|
||||
fun formatForBook(raw: String): String {
|
||||
val date = parseDate(raw) ?: return raw
|
||||
return date.format(bookFormatter)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.myitschool.work.data.repo
|
||||
package ru.myitschool.work.data.repo
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
|
||||
@@ -13,7 +13,7 @@ object AuthRepository {
|
||||
private val KEY_CODE = stringPreferencesKey("auth_code")
|
||||
private var codeCache: String? = null
|
||||
|
||||
// Проверка кода на сервере и сохранение при успехе
|
||||
|
||||
suspend fun checkAndSave(text: String): Result<Boolean> {
|
||||
return NetworkDataSource.checkAuth(text).onSuccess { success ->
|
||||
if (success) {
|
||||
@@ -25,7 +25,7 @@ object AuthRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/** Сохранённый код (из кэша или DataStore) */
|
||||
|
||||
suspend fun getSavedCode(): String? {
|
||||
codeCache?.let { return it }
|
||||
|
||||
@@ -37,7 +37,7 @@ object AuthRepository {
|
||||
return code
|
||||
}
|
||||
|
||||
/** Полная очистка данных авторизации (для logout) */
|
||||
|
||||
suspend fun clear() {
|
||||
codeCache = null
|
||||
dataStore.edit { prefs ->
|
||||
|
||||
@@ -11,9 +11,7 @@ import ru.myitschool.work.domain.entities.UserEntity
|
||||
|
||||
object UserRepository {
|
||||
|
||||
/**
|
||||
* Получение информации о пользователе через GET /api/<CODE>/info
|
||||
*/
|
||||
|
||||
suspend fun getUserInfo(): Result<UserEntity> {
|
||||
val code = AuthRepository.getSavedCode()
|
||||
?: return Result.failure(IllegalStateException("Auth code is not saved"))
|
||||
@@ -23,9 +21,7 @@ object UserRepository {
|
||||
.map { dto -> dto.toDomainUser() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Доступные слоты бронирования через GET /api/<CODE>/booking
|
||||
*/
|
||||
|
||||
suspend fun getAvailableBookings(): Result<List<BookingEntity>> {
|
||||
val code = AuthRepository.getSavedCode()
|
||||
?: return Result.failure(IllegalStateException("Auth code is not saved"))
|
||||
@@ -35,9 +31,7 @@ object UserRepository {
|
||||
.map { map -> map.toDomainBookings() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание нового бронирования через POST /api/<CODE>/book
|
||||
*/
|
||||
|
||||
suspend fun book(date: String, placeId: Int): Result<Unit> {
|
||||
val code = AuthRepository.getSavedCode()
|
||||
?: return Result.failure(IllegalStateException("Auth code is not saved"))
|
||||
@@ -49,7 +43,6 @@ object UserRepository {
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------- Маппинг DTO -> domain --------------------
|
||||
|
||||
private fun UserDto.toDomainUser(): UserEntity {
|
||||
val bookings = booking
|
||||
|
||||
@@ -8,7 +8,10 @@ import io.ktor.client.request.get
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.client.utils.EmptyContent.contentType
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -21,10 +24,7 @@ import ru.myitschool.work.data.source.dto.UserDto
|
||||
|
||||
object NetworkDataSource {
|
||||
|
||||
/**
|
||||
* Поставь false, когда поднимешь настоящий бэкенд.
|
||||
* При true используются локальные заглушки.
|
||||
*/
|
||||
|
||||
private const val USE_STUB: Boolean = false
|
||||
|
||||
private val client by lazy {
|
||||
@@ -73,9 +73,7 @@ object NetworkDataSource {
|
||||
|
||||
// ----------------- Публичные методы -----------------
|
||||
|
||||
/**
|
||||
* Проверка кода авторизации через GET /api/<CODE>/auth
|
||||
*/
|
||||
|
||||
suspend fun checkAuth(code: String): Result<Boolean> {
|
||||
if (USE_STUB) {
|
||||
// Примитивная проверка заглушки: 4+ символа → ок
|
||||
@@ -95,9 +93,7 @@ object NetworkDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение информации о пользователе через GET /api/<CODE>/info
|
||||
*/
|
||||
|
||||
suspend fun getUserInfo(code: String): Result<UserDto> {
|
||||
if (USE_STUB) {
|
||||
return Result.success(stubUser)
|
||||
@@ -116,9 +112,7 @@ object NetworkDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение доступных слотов бронирования через GET /api/<CODE>/booking
|
||||
*/
|
||||
|
||||
suspend fun getAvailableBookings(
|
||||
code: String,
|
||||
): Result<Map<String, List<AvailablePlaceDto>>> {
|
||||
@@ -140,11 +134,7 @@ object NetworkDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание нового бронирования через POST /api/<CODE>/book
|
||||
*
|
||||
* Тело: { "date": "2025-01-05", "placeID": 1 }
|
||||
*/
|
||||
|
||||
suspend fun book(
|
||||
code: String,
|
||||
date: String,
|
||||
@@ -158,6 +148,7 @@ object NetworkDataSource {
|
||||
return withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val response = client.post(getUrl(code, Constants.BOOK_URL)) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(
|
||||
BookRequestDto(
|
||||
date = date,
|
||||
|
||||
@@ -3,19 +3,6 @@ package ru.myitschool.work.data.source.dto
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* DTO для ответа GET /api/<CODE>/info
|
||||
*
|
||||
* Пример:
|
||||
* {
|
||||
* "name":"Иванов Петр Федорович",
|
||||
* "photoUrl":"https://...",
|
||||
* "booking":{
|
||||
* "2025-01-05": {"id":1,"place":"102"},
|
||||
* "2025-01-06": {"id":2,"place":"209.13"}
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@Serializable
|
||||
data class UserDto(
|
||||
@SerialName("name")
|
||||
@@ -26,9 +13,7 @@ data class UserDto(
|
||||
val booking: Map<String, BookedPlaceDto> = emptyMap(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Элемент бронирования в ответе /info и /booking.
|
||||
*/
|
||||
|
||||
@Serializable
|
||||
data class BookedPlaceDto(
|
||||
@SerialName("id")
|
||||
@@ -37,13 +22,7 @@ data class BookedPlaceDto(
|
||||
val place: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO для доступных мест в ответе GET /api/<CODE>/booking:
|
||||
*
|
||||
* {
|
||||
* "2025-01-05": [{"id": 1, "place": "102"}, ...]
|
||||
* }
|
||||
*/
|
||||
|
||||
@Serializable
|
||||
data class AvailablePlaceDto(
|
||||
@SerialName("id")
|
||||
@@ -52,14 +31,7 @@ data class AvailablePlaceDto(
|
||||
val place: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Тело запроса POST /api/<CODE>/book:
|
||||
*
|
||||
* {
|
||||
* "date": "2025-01-05",
|
||||
* "placeID": 1
|
||||
* }
|
||||
*/
|
||||
|
||||
@Serializable
|
||||
data class BookRequestDto(
|
||||
@SerialName("date")
|
||||
|
||||
@@ -2,16 +2,12 @@ package ru.myitschool.work.domain.booking
|
||||
|
||||
import ru.myitschool.work.data.repo.UserRepository
|
||||
|
||||
/**
|
||||
* Юзкейс для создания бронирования.
|
||||
*
|
||||
* @param date дата бронирования в формате yyyy-MM-dd
|
||||
* @param placeId идентификатор места (placeID из бэкенда)
|
||||
*/
|
||||
class BookPlaceUseCase(
|
||||
private val repository: UserRepository,
|
||||
) {
|
||||
suspend operator fun invoke(date: String, placeId: Int): Result<Unit> {
|
||||
return repository.book(date, placeId)
|
||||
suspend operator fun invoke(roomName: String, placeId: Int): Result<Unit> {
|
||||
// Нужно получить дату из какого-то источника
|
||||
// Либо изменить логику в ViewModel
|
||||
return repository.book(roomName, placeId) // ← но repository.book ожидает date, а не roomName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ class RootActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
actionBar?.hide()
|
||||
setContent {
|
||||
WorkTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
|
||||
@@ -40,7 +40,7 @@ fun AppNavHost(
|
||||
}
|
||||
}
|
||||
|
||||
// Пока не знаем стартовый экран — ничего не рисуем (можно сюда потом воткнуть Splash)
|
||||
|
||||
val destination = startDestination ?: return
|
||||
|
||||
NavHost(
|
||||
|
||||
@@ -2,6 +2,7 @@ package ru.myitschool.work.ui.screen.auth
|
||||
|
||||
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
|
||||
@@ -9,7 +10,9 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -20,11 +23,14 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import io.ktor.websocket.Frame
|
||||
import ru.myitschool.work.R
|
||||
import ru.myitschool.work.core.TestIds
|
||||
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||
@@ -50,8 +56,20 @@ fun AuthScreen(
|
||||
.fillMaxSize()
|
||||
.padding(all = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
Spacer(modifier = Modifier.size(48.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.auth_name),
|
||||
style = MaterialTheme.typography.displaySmall.copy(
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(48.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.auth_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
@@ -60,7 +78,7 @@ fun AuthScreen(
|
||||
when (val currentState = state) {
|
||||
is AuthState.Data -> Content(viewModel, currentState)
|
||||
is AuthState.Loading -> {
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(64.dp)
|
||||
)
|
||||
@@ -76,28 +94,31 @@ private fun Content(
|
||||
) {
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Auth.CODE_INPUT)
|
||||
.fillMaxWidth(),
|
||||
value = state.code,
|
||||
onValueChange = { viewModel.onIntent(AuthIntent.TextInput(it)) },
|
||||
label = { Text(stringResource(R.string.auth_label)) },
|
||||
placeholder = { Text("Введите код сотрудника") }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
|
||||
if (state.isErrorVisible) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestIds.Auth.ERROR),
|
||||
text = "Неверный код или ошибка сервера",
|
||||
color = Color.Red,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
}
|
||||
|
||||
TextField(
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Auth.CODE_INPUT)
|
||||
.fillMaxWidth(),
|
||||
value = state.code,
|
||||
onValueChange = { viewModel.onIntent(AuthIntent.TextInput(it)) },
|
||||
label = { Text(stringResource(R.string.auth_label)) }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
Button(
|
||||
@@ -109,6 +130,16 @@ private fun Content(
|
||||
},
|
||||
enabled = state.isButtonEnabled
|
||||
) {
|
||||
Text(stringResource(R.string.auth_sign_in))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(stringResource(R.string.auth_sign_in))
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_arrow_right),
|
||||
contentDescription = "Войти"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,11 @@ class AuthViewModel : ViewModel() {
|
||||
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data())
|
||||
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
|
||||
|
||||
// единичное событие навигации на главный экран
|
||||
|
||||
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
|
||||
val actionFlow: SharedFlow<Unit> = _actionFlow
|
||||
|
||||
init {
|
||||
// Если код уже сохранён — сразу уходим на главный экран
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val saved = getSavedAuthCodeUseCase()
|
||||
if (saved != null) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package ru.myitschool.work.ui.screen.book
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -13,9 +14,11 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Surface
|
||||
@@ -26,10 +29,14 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import ru.myitschool.work.R
|
||||
import ru.myitschool.work.core.TestIds
|
||||
|
||||
@Composable
|
||||
@@ -112,14 +119,34 @@ private fun BookErrorContent(
|
||||
modifier = Modifier.testTag(TestIds.Book.REFRESH_BUTTON),
|
||||
onClick = onRefresh
|
||||
) {
|
||||
Text("Обновить")
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = stringResource(R.string.main_screen_refresh))
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_refresh),
|
||||
contentDescription = "Обновить"
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Button(
|
||||
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON),
|
||||
onClick = onBack
|
||||
) {
|
||||
Text("Назад")
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_back),
|
||||
contentDescription = "Назад"
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(text = stringResource(R.string.book_screen_back))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,14 +172,34 @@ private fun BookEmptyContent(
|
||||
modifier = Modifier.testTag(TestIds.Book.REFRESH_BUTTON),
|
||||
onClick = onRefresh
|
||||
) {
|
||||
Text("Обновить")
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = stringResource(R.string.main_screen_refresh))
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_refresh),
|
||||
contentDescription = "Обновить"
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Button(
|
||||
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON),
|
||||
onClick = onBack
|
||||
) {
|
||||
Text("Назад")
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_back),
|
||||
contentDescription = "Назад"
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(text = stringResource(R.string.book_screen_back))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,34 +226,31 @@ private fun BookDataContent(
|
||||
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON),
|
||||
onClick = onBack
|
||||
) {
|
||||
Text("Назад")
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_back),
|
||||
contentDescription = "Назад"
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(text = stringResource(R.string.book_screen_back))
|
||||
}
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier.testTag(TestIds.Book.REFRESH_BUTTON),
|
||||
onClick = onRefresh
|
||||
) {
|
||||
Text("Обновить")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
// Вкладки с датами
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
itemsIndexed(state.dates) { index, dateLabel ->
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Book.getIdDateItemByPosition(index)),
|
||||
tonalElevation = if (index == state.selectedDateIndex) 4.dp else 0.dp
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
.testTag(TestIds.Book.ITEM_DATE),
|
||||
text = dateLabel,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
Text(text = stringResource(R.string.main_screen_refresh))
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_refresh),
|
||||
contentDescription = "Обновить"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -214,7 +258,42 @@ private fun BookDataContent(
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
// Список мест
|
||||
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
itemsIndexed(state.dates) { index, dateLabel ->
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Book.getIdDateItemByPosition(index))
|
||||
.clickable { onSelectDate(index) }
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
tonalElevation = if (index == state.selectedDateIndex) 4.dp else 0.dp,
|
||||
color = if (index == state.selectedDateIndex) {
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surface
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
.testTag(TestIds.Book.ITEM_DATE),
|
||||
text = dateLabel,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (index == state.selectedDateIndex) {
|
||||
MaterialTheme.colorScheme.onPrimaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
@@ -262,7 +341,17 @@ private fun BookDataContent(
|
||||
onClick = onBook,
|
||||
enabled = state.places.any { it.isSelected }
|
||||
) {
|
||||
Text("Забронировать")
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = stringResource(R.string.book_screen_book))
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_arrow_right),
|
||||
contentDescription = "Забронировать"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,5 @@ sealed interface BookState {
|
||||
val message: String,
|
||||
) : BookState
|
||||
|
||||
/** Нет доступных дат — показываем "Всё забронировано" */
|
||||
object Empty : BookState
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class BookViewModel : ViewModel() {
|
||||
|
||||
private var allGroups: List<DateGroup> = emptyList()
|
||||
private var selectedDateIndex: Int = 0
|
||||
private var selectedPlaceId: Int? = null
|
||||
private var selectedSlotId: Int? = null
|
||||
|
||||
fun onIntent(intent: BookIntent) {
|
||||
when (intent) {
|
||||
@@ -51,7 +51,7 @@ class BookViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
is BookIntent.SelectPlace -> {
|
||||
selectedPlaceId = intent.id
|
||||
selectedSlotId = intent.id
|
||||
val current = _uiState.value
|
||||
if (current is BookState.Data) {
|
||||
val updatedPlaces = current.places.map { item ->
|
||||
@@ -77,7 +77,7 @@ class BookViewModel : ViewModel() {
|
||||
if (bookings.isEmpty()) {
|
||||
_uiState.value = BookState.Empty
|
||||
allGroups = emptyList()
|
||||
selectedPlaceId = null
|
||||
selectedSlotId = null
|
||||
selectedDateIndex = 0
|
||||
return@fold
|
||||
}
|
||||
@@ -86,18 +86,18 @@ class BookViewModel : ViewModel() {
|
||||
|
||||
if (allGroups.isEmpty()) {
|
||||
_uiState.value = BookState.Empty
|
||||
selectedPlaceId = null
|
||||
selectedSlotId = null
|
||||
selectedDateIndex = 0
|
||||
return@fold
|
||||
}
|
||||
|
||||
selectedDateIndex = 0
|
||||
selectedPlaceId = null
|
||||
selectedSlotId = null
|
||||
val firstGroup = allGroups[0]
|
||||
|
||||
val places = firstGroup.slots.mapIndexed { index, slot ->
|
||||
val places = firstGroup.slots.map { slot ->
|
||||
BookPlaceItem(
|
||||
id = index,
|
||||
id = slot.id,
|
||||
roomName = slot.roomName,
|
||||
time = slot.time,
|
||||
isSelected = false,
|
||||
@@ -124,10 +124,9 @@ class BookViewModel : ViewModel() {
|
||||
if (index !in groups.indices) return
|
||||
|
||||
selectedDateIndex = index
|
||||
selectedPlaceId = null
|
||||
|
||||
selectedSlotId = null
|
||||
val group = groups[index]
|
||||
val places = group.slots.mapIndexed { idx, slot ->
|
||||
val places = group.slots.map { slot ->
|
||||
BookPlaceItem(
|
||||
id = slot.id,
|
||||
roomName = slot.roomName,
|
||||
@@ -135,7 +134,6 @@ class BookViewModel : ViewModel() {
|
||||
isSelected = false,
|
||||
)
|
||||
}
|
||||
|
||||
val datesLabels = groups.map { it.label }
|
||||
|
||||
val current = _uiState.value
|
||||
@@ -158,11 +156,19 @@ class BookViewModel : ViewModel() {
|
||||
val current = _uiState.value
|
||||
if (current !is BookState.Data) return
|
||||
|
||||
val placeId = selectedPlaceId ?: return
|
||||
val place = current.places.firstOrNull { it.id == placeId } ?: return
|
||||
val slotId = selectedSlotId ?: return
|
||||
|
||||
allGroups
|
||||
.flatMap { it.slots }
|
||||
.firstOrNull { it.id == slotId }
|
||||
?: return
|
||||
|
||||
|
||||
val selectedDate = allGroups[selectedDateIndex].date.toString()
|
||||
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
bookPlaceUseCase(place.roomName, place.id)
|
||||
|
||||
bookPlaceUseCase(selectedDate, slotId)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
_actionFlow.emit(Action.CloseWithSuccess)
|
||||
|
||||
@@ -16,10 +16,12 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.SegmentedButtonDefaults.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -28,12 +30,19 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import coil3.compose.SubcomposeAsyncImage
|
||||
import org.intellij.lang.annotations.JdkConstants
|
||||
import ru.myitschool.work.R
|
||||
import ru.myitschool.work.core.TestIds
|
||||
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
||||
import ru.myitschool.work.ui.nav.BookScreenDestination
|
||||
@@ -130,6 +139,8 @@ private fun MainDataContent(
|
||||
.padding(16.dp)
|
||||
) {
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -147,38 +158,47 @@ private fun MainDataContent(
|
||||
}
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
Text(
|
||||
modifier = Modifier.testTag(TestIds.Main.PROFILE_NAME),
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Main.PROFILE_NAME)
|
||||
.weight(1f),
|
||||
text = state.name,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Main.LOGOUT_BUTTON),
|
||||
onClick = onLogout,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
contentColor = MaterialTheme.colorScheme.errorContainer,
|
||||
containerColor = MaterialTheme.colorScheme.onErrorContainer
|
||||
|
||||
)
|
||||
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Выход")
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_logout),
|
||||
contentDescription = "Выйти"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
Spacer(modifier = Modifier.size(24.dp))
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier.testTag(TestIds.Main.LOGOUT_BUTTON),
|
||||
onClick = onLogout
|
||||
) {
|
||||
Text("Выход")
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON),
|
||||
onClick = onRefresh
|
||||
) {
|
||||
Text("Обновить")
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier.testTag(TestIds.Main.ADD_BUTTON),
|
||||
onClick = onAddBooking
|
||||
) {
|
||||
Text("Бронь")
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.main_screen_title),
|
||||
style = MaterialTheme.typography.titleLarge.copy(
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
Spacer(modifier = Modifier.size(20.dp))
|
||||
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
@@ -192,20 +212,85 @@ private fun MainDataContent(
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.testTag(TestIds.Main.ITEM_DATE),
|
||||
Row(modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = stringResource(R.string.main_screen_date),
|
||||
style = MaterialTheme.typography.bodyLarge.copy(
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.testTag(TestIds.Main.ITEM_DATE),
|
||||
text = item.dateLabel,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE),
|
||||
text = item.roomName,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
style = MaterialTheme.typography.bodyLarge.copy(
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.main_screen_place),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE),
|
||||
text = item.roomName,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON),
|
||||
onClick = onRefresh
|
||||
) { Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = stringResource(R.string.main_screen_refresh))
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_refresh),
|
||||
contentDescription = "Обновить"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier.testTag(TestIds.Main.ADD_BUTTON),
|
||||
onClick = onAddBooking
|
||||
) { Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = stringResource(R.string.main_screen_new_book))
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_add),
|
||||
contentDescription = "Новая бронь"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
|
||||
@@ -32,26 +32,26 @@ private val LightColorScheme = lightColorScheme(
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun WorkTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
@Composable
|
||||
fun WorkTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
14
app/src/main/res/drawable/ic_add.xml
Normal file
14
app/src/main/res/drawable/ic_add.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M0.5,8.5v-1h15v1Z"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M8.5,15.5h-1V0.5h1Z"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_arrow_right.xml
Normal file
5
app/src/main/res/drawable/ic_arrow_right.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:viewportHeight="24" android:viewportWidth="24" android:width="16dp">
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="M11.293,4.707l6.293,6.293l-13.586,0l0,2l13.586,0l-6.293,6.293l1.414,1.414l8.707,-8.707l-8.707,-8.707l-1.414,1.414z"/>
|
||||
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_back.xml
Normal file
9
app/src/main/res/drawable/ic_back.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M11.62,3.81l-4.19,4.19l4.19,4.19l-1.53,1.52l-5.71,-5.71l5.71,-5.71l1.53,1.52z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_logout.xml
Normal file
9
app/src/main/res/drawable/ic_logout.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:viewportHeight="32" android:viewportWidth="32" android:width="16dp">
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="M27.9,2.58a0.86,0.86 0,0 0,-0.07 -0.1,0.71 0.71,0 0,0 -0.19,-0.23l0,0 -0.09,0a1.12,1.12 0,0 0,-0.25 -0.11L27.1,2 27,2H12a1,1 0,0 0,-1 1V9a1,1 0,0 0,2 0V4h7.19L16.71,5A1,1 0,0 0,16 6V25H13V22a1,1 0,0 0,-2 0v4a1,1 0,0 0,1 1h4v2a1,1 0,0 0,0.4 0.8,1 1,0 0,0 0.6,0.2 1,1 0,0 0,0.29 0l10,-3A1,1 0,0 0,28 26V3A1,1 0,0 0,27.9 2.58ZM26,25.26l-8,2.4V6.74l8,-2.4Z"/>
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="M7.41,17H14a1,1 0,0 0,0 -2H7.41l1.3,-1.29a1,1 0,0 0,-1.42 -1.42l-3,3a1,1 0,0 0,-0.21 0.33,1 1,0 0,0 0,0.76 1,1 0,0 0,0.21 0.33l3,3a1,1 0,0 0,1.42 0,1 1,0 0,0 0,-1.42Z"/>
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="M20,17a1,1 0,0 0,0 -2h0a1,1 0,1 0,0 2Z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_refresh.xml
Normal file
5
app/src/main/res/drawable/ic_refresh.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:viewportHeight="24" android:viewportWidth="24" android:width="16dp">
|
||||
|
||||
<path android:fillColor="#FF000000" android:pathData="M12,3A8.959,8.959 0,0 0,5 6.339V4H3v6H9V8H6.274A6.982,6.982 0,1 1,5.22 13.751l-1.936,0.5A9,9 0,1 0,12 3Z"/>
|
||||
|
||||
</vector>
|
||||
@@ -1,7 +1,15 @@
|
||||
<resources>
|
||||
<string name="app_name">Work</string>
|
||||
<string name="title_activity_root">RootActivity</string>
|
||||
<string name="auth_title">Привет! Введи код для авторизации</string>
|
||||
<string name="auth_name">Система бронирования</string>
|
||||
<string name="auth_title">Вход в систему</string>
|
||||
<string name="auth_label">Код</string>
|
||||
<string name="auth_sign_in">Войти</string>
|
||||
<string name="main_screen_date">Дата:</string>
|
||||
<string name="main_screen_place">Место:</string>
|
||||
<string name="main_screen_title">Мои бронирования</string>
|
||||
<string name="main_screen_refresh">Обновить</string>
|
||||
<string name="main_screen_new_book">Новая бронь</string>
|
||||
<string name="book_screen_book">Забронировать</string>
|
||||
<string name="book_screen_back">Назад</string>
|
||||
</resources>
|
||||
@@ -1,21 +1,16 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
## For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
#
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# Default value: -Xmx1024m -XX:MaxPermSize=256m
|
||||
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
#
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# This option should only be used with decoupled projects. For more details, visit
|
||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
#Thu Dec 04 14:16:52 GMT+07:00 2025
|
||||
android.nonTransitiveRClass=true
|
||||
android.useAndroidX=true
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" -Dfile.encoding\=UTF-8
|
||||
|
||||
Reference in New Issue
Block a user