Compare commits

11 Commits
main ... main

Author SHA1 Message Date
b40749faa1 UI/UX solution:
fix POST booking request
2025-12-05 22:19:51 +07:00
dbe735e541 UI/UX solution:
Added clickable date tabs for booking
2025-12-05 21:28:06 +07:00
40a8428a19 Merge remote-tracking branch 'origin/main' 2025-12-05 05:01:00 +07:00
9e603f87e6 UI/UX solution 2025-12-05 05:00:33 +07:00
beb48ab41e With avatar 2025-12-01 21:45:54 +06:00
04ac941ba4 No ava 2025-12-01 14:03:20 +06:00
99c100929a work no server 2025-12-01 13:26:14 +06:00
ce20de0daa No auth 2025-11-30 21:29:30 +06:00
523c373a04 kotlin poezdec 2025-11-30 20:26:32 +06:00
01cf2c1439 Before test crutches 2025-11-30 17:36:19 +06:00
371d09995c Start 2025-11-30 16:51:33 +06:00
44 changed files with 2136 additions and 140 deletions

View 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

View 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

View 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

View 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

View File

@@ -1,4 +1,4 @@
# НТО 2025. II отборочный этап. Командные задания — Android
# НТО 2025. II отборочный этап. Командные задания — Android
## 📖 Предыстория

View File

@@ -1,20 +1,19 @@
plugins {
composeCompiler
kotlinAndroid
kotlinSerialization version Version.Kotlin.language
androidApplication
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("org.jetbrains.kotlin.plugin.serialization")
}
val packageName = "ru.myitschool.work"
android {
namespace = packageName
compileSdk = Version.Android.Sdk.compile
compileSdk = 36
defaultConfig {
applicationId = packageName
minSdk = Version.Android.Sdk.min
targetSdk = Version.Android.Sdk.target
minSdk = 28
targetSdk = 36
versionCode = 1
versionName = "1.0"
@@ -25,17 +24,17 @@ android {
buildFeatures.viewBinding = true
compileOptions {
sourceCompatibility = Version.Kotlin.javaSource
targetCompatibility = Version.Kotlin.javaSource
sourceCompatibility = /*Version.Kotlin.javaSource*/JavaVersion.VERSION_17
targetCompatibility = /*Version.Kotlin.javaSource*/JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = Version.Kotlin.jvmTarget
jvmTarget = /*Version.Kotlin.jvmTarget*/"17"
}
}
dependencies {
defaultComposeLibrary()
/* defaultComposeLibrary()*/
implementation("androidx.datastore:datastore-preferences:1.1.7")
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0")
implementation("androidx.navigation:navigation-compose:2.9.6")
@@ -48,4 +47,34 @@ dependencies {
implementation("io.ktor:ktor-client-content-negotiation:$ktor")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("io.ktor:ktor-client-core:$ktor")
implementation("io.ktor:ktor-client-cio:$ktor")
implementation("io.ktor:ktor-client-content-negotiation:$ktor")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
implementation("androidx.activity:activity-compose:1.9.3")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
implementation(platform("androidx.compose:compose-bom:2024.09.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}

View File

@@ -0,0 +1,35 @@
package ru.myitschool.work.core
import java.time.LocalDate
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter
object DateUtils {
private val mainFormatter: DateTimeFormatter =
DateTimeFormatter.ofPattern("dd.MM.yyyy")
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 {
runCatching {
LocalDate.parse(raw, DateTimeFormatter.ofPattern("dd.MM.yyyy"))
}.getOrNull()
}
}
}
// Формат для главного экрана: dd.MM.yyyy
fun formatForMain(raw: String): String {
val date = parseDate(raw) ?: return raw
return date.format(mainFormatter)
}
// Формат для экрана бронирования: dd.MM
fun formatForBook(raw: String): String {
val date = parseDate(raw) ?: return raw
return date.format(bookFormatter)
}
}

View File

@@ -0,0 +1,7 @@
package ru.myitschool.work.data.repo
import android.content.Context
import androidx.datastore.preferences.preferencesDataStore
// DataStore для хранения auth-кода
val Context.authDataStore by preferencesDataStore(name = "auth_data")

View File

@@ -1,16 +1,47 @@
package ru.myitschool.work.data.repo
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import ru.myitschool.work.App
import ru.myitschool.work.data.source.NetworkDataSource
object AuthRepository {
private val dataStore get() = App.context.authDataStore
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) {
codeCache = text
dataStore.edit { prefs ->
prefs[KEY_CODE] = text
}
}
}
}
suspend fun getSavedCode(): String? {
codeCache?.let { return it }
val code = dataStore.data
.map { prefs -> prefs[KEY_CODE] }
.first()
codeCache = code
return code
}
suspend fun clear() {
codeCache = null
dataStore.edit { prefs ->
prefs.clear()
}
}
}

View File

@@ -0,0 +1,92 @@
package ru.myitschool.work.data.repo
import java.time.LocalDate
import ru.myitschool.work.core.DateUtils
import ru.myitschool.work.data.source.NetworkDataSource
import ru.myitschool.work.data.source.dto.AvailablePlaceDto
import ru.myitschool.work.data.source.dto.BookedPlaceDto
import ru.myitschool.work.data.source.dto.UserDto
import ru.myitschool.work.domain.entities.BookingEntity
import ru.myitschool.work.domain.entities.UserEntity
object UserRepository {
suspend fun getUserInfo(): Result<UserEntity> {
val code = AuthRepository.getSavedCode()
?: return Result.failure(IllegalStateException("Auth code is not saved"))
return NetworkDataSource
.getUserInfo(code)
.map { dto -> dto.toDomainUser() }
}
suspend fun getAvailableBookings(): Result<List<BookingEntity>> {
val code = AuthRepository.getSavedCode()
?: return Result.failure(IllegalStateException("Auth code is not saved"))
return NetworkDataSource
.getAvailableBookings(code)
.map { map -> map.toDomainBookings() }
}
suspend fun book(date: String, placeId: Int): Result<Unit> {
val code = AuthRepository.getSavedCode()
?: return Result.failure(IllegalStateException("Auth code is not saved"))
return NetworkDataSource.book(
code = code,
date = date,
placeId = placeId,
)
}
private fun UserDto.toDomainUser(): UserEntity {
val bookings = booking
.flatMap { (dateString, bookedPlace) ->
listOfNotNull(bookedPlace.toDomainBooking(dateString))
}
.sortedBy { booking ->
DateUtils.parseDate(booking.time) ?: LocalDate.MAX
}
return UserEntity(
name = name,
photoUrl = photoUrl,
bookings = bookings,
)
}
private fun Map<String, List<AvailablePlaceDto>>.toDomainBookings(): List<BookingEntity> {
return entries
.flatMap { (dateString, places) ->
places.mapNotNull { dto ->
dto.toDomainBooking(dateString)
}
}
.sortedBy { booking ->
DateUtils.parseDate(booking.time) ?: LocalDate.MAX
}
}
private fun BookedPlaceDto.toDomainBooking(dateString: String): BookingEntity? {
val parsedDate = DateUtils.parseDate(dateString) ?: return null
return BookingEntity(
id = id,
roomName = place,
time = parsedDate.toString(),
)
}
private fun AvailablePlaceDto.toDomainBooking(dateString: String): BookingEntity? {
val parsedDate = DateUtils.parseDate(dateString) ?: return null
return BookingEntity(
id = id,
roomName = place,
time = parsedDate.toString(),
)
}
}

View File

@@ -1,18 +1,32 @@
package ru.myitschool.work.data.source
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
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
import kotlinx.serialization.json.Json
import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.source.dto.AvailablePlaceDto
import ru.myitschool.work.data.source.dto.BookRequestDto
import ru.myitschool.work.data.source.dto.BookedPlaceDto
import ru.myitschool.work.data.source.dto.UserDto
object NetworkDataSource {
private const val USE_STUB: Boolean = false
private val client by lazy {
HttpClient(CIO) {
install(ContentNegotiation) {
@@ -20,7 +34,7 @@ object NetworkDataSource {
Json {
isLenient = true
ignoreUnknownKeys = true
explicitNulls = true
explicitNulls = false
encodeDefaults = true
}
)
@@ -28,15 +42,132 @@ object NetworkDataSource {
}
}
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
return@withContext runCatching {
val response = client.get(getUrl(code, Constants.AUTH_URL))
when (response.status) {
HttpStatusCode.OK -> true
else -> error(response.bodyAsText())
// --------- Заглушечные данные ---------
private val stubUser = UserDto(
name = "Тестовый пользователь",
photoUrl = null,
booking = mapOf(
"2025-01-05" to BookedPlaceDto(id = 1, place = "102"),
"2025-01-06" to BookedPlaceDto(id = 2, place = "209.13"),
"2025-01-09" to BookedPlaceDto(id = 3, place = "Зона 51. 50"),
),
)
private val stubAvailableBookings: Map<String, List<AvailablePlaceDto>> = mapOf(
"2025-01-05" to listOf(
AvailablePlaceDto(id = 1, place = "102"),
AvailablePlaceDto(id = 2, place = "209.13"),
),
"2025-01-06" to listOf(
AvailablePlaceDto(id = 3, place = "Зона 51. 50"),
),
"2025-01-07" to listOf(
AvailablePlaceDto(id = 4, place = "102"),
AvailablePlaceDto(id = 5, place = "209.13"),
),
"2025-01-08" to listOf(
AvailablePlaceDto(id = 6, place = "209.13"),
),
)
// ----------------- Публичные методы -----------------
suspend fun checkAuth(code: String): Result<Boolean> {
if (USE_STUB) {
// Примитивная проверка заглушки: 4+ символа → ок
return Result.success(code.length >= 4)
}
return withContext(Dispatchers.IO) {
runCatching {
val response = client.get(getUrl(code, Constants.AUTH_URL))
when (response.status) {
HttpStatusCode.OK -> true
HttpStatusCode.BadRequest,
HttpStatusCode.Unauthorized -> error(response.bodyAsText())
else -> error(response.bodyAsText())
}
}
}
}
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl"
suspend fun getUserInfo(code: String): Result<UserDto> {
if (USE_STUB) {
return Result.success(stubUser)
}
return withContext(Dispatchers.IO) {
runCatching {
val response = client.get(getUrl(code, Constants.INFO_URL))
when (response.status) {
HttpStatusCode.OK -> response.body<UserDto>()
HttpStatusCode.BadRequest,
HttpStatusCode.Unauthorized -> error(response.bodyAsText())
else -> error(response.bodyAsText())
}
}
}
}
suspend fun getAvailableBookings(
code: String,
): Result<Map<String, List<AvailablePlaceDto>>> {
if (USE_STUB) {
return Result.success(stubAvailableBookings)
}
return withContext(Dispatchers.IO) {
runCatching {
val response = client.get(getUrl(code, Constants.BOOKING_URL))
when (response.status) {
HttpStatusCode.OK ->
response.body<Map<String, List<AvailablePlaceDto>>>()
HttpStatusCode.BadRequest,
HttpStatusCode.Unauthorized -> error(response.bodyAsText())
else -> error(response.bodyAsText())
}
}
}
}
suspend fun book(
code: String,
date: String,
placeId: Int,
): Result<Unit> {
if (USE_STUB) {
// В режиме заглушки считаем, что всё прошло успешно
return Result.success(Unit)
}
return withContext(Dispatchers.IO) {
runCatching {
val response = client.post(getUrl(code, Constants.BOOK_URL)) {
contentType(ContentType.Application.Json)
setBody(
BookRequestDto(
date = date,
placeId = placeId,
)
)
}
when (response.status) {
HttpStatusCode.Created,
HttpStatusCode.OK -> Unit
HttpStatusCode.Conflict -> error("Уже забронировано")
HttpStatusCode.BadRequest,
HttpStatusCode.Unauthorized -> error(response.bodyAsText())
else -> error(response.bodyAsText())
}
}
}
}
private fun getUrl(code: String, targetUrl: String): String =
"${Constants.HOST}/api/$code$targetUrl"
}

View File

@@ -0,0 +1,41 @@
package ru.myitschool.work.data.source.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class UserDto(
@SerialName("name")
val name: String,
@SerialName("photoUrl")
val photoUrl: String? = null,
@SerialName("booking")
val booking: Map<String, BookedPlaceDto> = emptyMap(),
)
@Serializable
data class BookedPlaceDto(
@SerialName("id")
val id: Int,
@SerialName("place")
val place: String,
)
@Serializable
data class AvailablePlaceDto(
@SerialName("id")
val id: Int,
@SerialName("place")
val place: String,
)
@Serializable
data class BookRequestDto(
@SerialName("date")
val date: String,
@SerialName("placeID")
val placeId: Int,
)

View File

@@ -0,0 +1,11 @@
package ru.myitschool.work.domain.auth
import ru.myitschool.work.data.repo.AuthRepository
class ClearAuthDataUseCase(
private val repository: AuthRepository
) {
suspend operator fun invoke() {
repository.clear()
}
}

View File

@@ -0,0 +1,11 @@
package ru.myitschool.work.domain.auth
import ru.myitschool.work.data.repo.AuthRepository
class GetSavedAuthCodeUseCase(
private val repository: AuthRepository
) {
suspend operator fun invoke(): String? {
return repository.getSavedCode()
}
}

View File

@@ -0,0 +1,13 @@
package ru.myitschool.work.domain.booking
import ru.myitschool.work.data.repo.UserRepository
class BookPlaceUseCase(
private val repository: UserRepository,
) {
suspend operator fun invoke(roomName: String, placeId: Int): Result<Unit> {
// Нужно получить дату из какого-то источника
// Либо изменить логику в ViewModel
return repository.book(roomName, placeId) // ← но repository.book ожидает date, а не roomName
}
}

View File

@@ -0,0 +1,12 @@
//package ru.myitschool.work.domain.booking
//
//import ru.myitschool.work.data.repo.UserRepository
//import ru.myitschool.work.domain.entities.BookingEntity
//
//class GetAvailableBookingsUseCase(
// private val repository: UserRepository
//) {
// suspend operator fun invoke(): Result<List<BookingEntity>> {
// return repository.getAvailableBookings()
// }
//}

View File

@@ -0,0 +1,7 @@
package ru.myitschool.work.domain.entities
data class BookingEntity(
val id: Int,
val roomName: String,
val time: String,
)

View File

@@ -0,0 +1,7 @@
package ru.myitschool.work.domain.entities
data class UserEntity(
val name: String,
val photoUrl: String?,
val bookings: List<BookingEntity>,
)

View File

@@ -0,0 +1,12 @@
package ru.myitschool.work.domain.booking
import ru.myitschool.work.data.repo.UserRepository
import ru.myitschool.work.domain.entities.BookingEntity
class GetAvailableBookingsUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(): Result<List<BookingEntity>> {
return repository.getAvailableBookings()
}
}

View File

@@ -0,0 +1,12 @@
package ru.myitschool.work.domain.user
import ru.myitschool.work.data.repo.UserRepository
import ru.myitschool.work.domain.entities.UserEntity
class GetUserInfoUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(): Result<UserEntity> {
return repository.getUserInfo()
}
}

View File

@@ -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 ->

View File

@@ -2,48 +2,62 @@ package ru.myitschool.work.ui.screen
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.domain.auth.GetSavedAuthCodeUseCase
import ru.myitschool.work.ui.nav.AuthScreenDestination
import ru.myitschool.work.ui.nav.BookScreenDestination
import ru.myitschool.work.ui.nav.MainScreenDestination
import ru.myitschool.work.ui.screen.auth.AuthScreen
import ru.myitschool.work.ui.screen.book.BookScreen
import ru.myitschool.work.ui.screen.main.MainScreen
@Composable
fun AppNavHost(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController()
) {
// use case для проверки, есть ли сохранённый код
val getSavedAuthCodeUseCase = remember {
GetSavedAuthCodeUseCase(AuthRepository)
}
// асинхронно определяем, с какого экрана стартовать
val startDestination by produceState<Any?>(initialValue = null) {
val savedCode = getSavedAuthCodeUseCase()
value = if (savedCode.isNullOrBlank()) {
AuthScreenDestination
} else {
MainScreenDestination
}
}
val destination = startDestination ?: return
NavHost(
modifier = modifier,
enterTransition = { EnterTransition.None },
exitTransition = { ExitTransition.None },
navController = navController,
startDestination = AuthScreenDestination,
startDestination = destination,
) {
composable<AuthScreenDestination> {
AuthScreen(navController = navController)
}
composable<MainScreenDestination> {
Box(
contentAlignment = Alignment.Center
) {
Text(text = "Hello")
}
MainScreen(navController = navController)
}
composable<BookScreenDestination> {
Box(
contentAlignment = Alignment.Center
) {
Text(text = "Hello")
}
BookScreen(navController = navController)
}
}
}

View File

@@ -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,25 +10,27 @@ 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
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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
@@ -41,7 +44,10 @@ fun AuthScreen(
LaunchedEffect(Unit) {
viewModel.actionFlow.collect {
navController.navigate(MainScreenDestination)
navController.navigate(MainScreenDestination) {
// очищаем backstack до экрана авторизации
popUpTo(MainScreenDestination) { inclusive = true }
}
}
}
@@ -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,6 +78,7 @@ fun AuthScreen(
when (val currentState = state) {
is AuthState.Data -> Content(viewModel, currentState)
is AuthState.Loading -> {
Spacer(modifier = Modifier.size(24.dp))
CircularProgressIndicator(
modifier = Modifier.size(64.dp)
)
@@ -73,25 +92,54 @@ private fun Content(
viewModel: AuthViewModel,
state: AuthState.Data
) {
var inputText by remember { mutableStateOf("") }
Spacer(modifier = Modifier.size(16.dp))
TextField(
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
value = inputText,
onValueChange = {
inputText = it
viewModel.onIntent(AuthIntent.TextInput(it))
},
label = { Text(stringResource(R.string.auth_label)) }
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 = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.size(8.dp))
}
Spacer(modifier = Modifier.size(16.dp))
Button(
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
modifier = Modifier
.testTag(TestIds.Auth.SIGN_BUTTON)
.fillMaxWidth(),
onClick = {
viewModel.onIntent(AuthIntent.Send(inputText))
viewModel.onIntent(AuthIntent.Send(state.code))
},
enabled = true
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 = "Войти"
)
}
}
}

View File

@@ -1,6 +1,11 @@
package ru.myitschool.work.ui.screen.auth
sealed interface AuthState {
object Loading: AuthState
object Data: AuthState
object Loading : AuthState
data class Data(
val code: String = "",
val isButtonEnabled: Boolean = false,
val isErrorVisible: Boolean = false,
) : AuthState
}

View File

@@ -12,32 +12,82 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
import ru.myitschool.work.domain.auth.GetSavedAuthCodeUseCase
class AuthViewModel : ViewModel() {
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data)
private val checkAndSaveAuthCodeUseCase by lazy {
CheckAndSaveAuthCodeUseCase(AuthRepository)
}
private val getSavedAuthCodeUseCase by lazy {
GetSavedAuthCodeUseCase(AuthRepository)
}
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) {
_actionFlow.emit(Unit)
}
}
}
fun onIntent(intent: AuthIntent) {
when (intent) {
is AuthIntent.TextInput -> {
val newCode = intent.text
val isValid = isValidCode(newCode)
_uiState.update {
AuthState.Data(
code = newCode,
isButtonEnabled = isValid,
isErrorVisible = false,
)
}
}
is AuthIntent.Send -> {
val currentState = _uiState.value
if (currentState !is AuthState.Data) return
val code = currentState.code
if (!isValidCode(code)) return
viewModelScope.launch(Dispatchers.Default) {
_uiState.update { AuthState.Loading }
checkAndSaveAuthCodeUseCase.invoke("9999").fold(
checkAndSaveAuthCodeUseCase.invoke(code).fold(
onSuccess = {
// успешная авторизация → навигация на главный экран
_actionFlow.emit(Unit)
},
onFailure = { error ->
error.printStackTrace()
_actionFlow.emit(Unit)
_uiState.update {
AuthState.Data(
code = code,
isButtonEnabled = true,
isErrorVisible = true,
)
}
}
)
}
}
is AuthIntent.TextInput -> Unit
}
}
private fun isValidCode(text: String): Boolean {
if (text.length != 4) return false
return text.all { ch ->
ch.isDigit() || (ch in 'a'..'z') || (ch in 'A'..'Z')
}
}
}

View File

@@ -0,0 +1,9 @@
package ru.myitschool.work.ui.screen.book
sealed interface BookIntent {
data object Load : BookIntent
data object Refresh : BookIntent
data class SelectDate(val index: Int) : BookIntent
data class SelectPlace(val id: Int) : BookIntent
data object Book : BookIntent
}

View File

@@ -0,0 +1,357 @@
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
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.padding
import androidx.compose.foundation.layout.size
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
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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
fun BookScreen(
navController: NavController,
viewModel: BookViewModel = viewModel()
) {
val state by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) {
viewModel.onIntent(BookIntent.Load)
viewModel.actionFlow.collect { action ->
when (action) {
BookViewModel.Action.CloseWithSuccess -> {
navController.popBackStack()
}
}
}
}
when (val current = state) {
is BookState.Loading -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
is BookState.Error -> {
BookErrorContent(
message = current.message,
onBack = { navController.popBackStack() },
onRefresh = { viewModel.onIntent(BookIntent.Refresh) }
)
}
is BookState.Empty -> {
BookEmptyContent(
onBack = { navController.popBackStack() },
onRefresh = { viewModel.onIntent(BookIntent.Refresh) }
)
}
is BookState.Data -> {
BookDataContent(
state = current,
onBack = { navController.popBackStack() },
onRefresh = { viewModel.onIntent(BookIntent.Refresh) },
onSelectDate = { viewModel.onIntent(BookIntent.SelectDate(it)) },
onSelectPlace = { viewModel.onIntent(BookIntent.SelectPlace(it)) },
onBook = { viewModel.onIntent(BookIntent.Book) }
)
}
}
}
@Composable
private fun BookErrorContent(
message: String,
onBack: () -> Unit,
onRefresh: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier.testTag(TestIds.Book.ERROR),
text = message,
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.size(16.dp))
Button(
modifier = Modifier.testTag(TestIds.Book.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 = "Обновить"
)
}
}
Spacer(modifier = Modifier.size(8.dp))
Button(
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON),
onClick = onBack
) {
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))
}
}
}
}
@Composable
private fun BookEmptyContent(
onBack: () -> Unit,
onRefresh: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier.testTag(TestIds.Book.EMPTY),
text = "Всё забронировано"
)
Spacer(modifier = Modifier.size(16.dp))
Button(
modifier = Modifier.testTag(TestIds.Book.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 = "Обновить"
)
}
}
Spacer(modifier = Modifier.size(8.dp))
Button(
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON),
onClick = onBack
) {
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))
}
}
}
}
@Composable
private fun BookDataContent(
state: BookState.Data,
onBack: () -> Unit,
onRefresh: () -> Unit,
onSelectDate: (Int) -> Unit,
onSelectPlace: (Int) -> Unit,
onBook: () -> Unit,
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Button(
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON),
onClick = onBack
) {
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
) {
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(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)
) {
itemsIndexed(state.places, key = { _, item -> item.id }) { index, item ->
Card(
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Book.getIdPlaceItemByPosition(index))
.selectable(
selected = item.isSelected,
onClick = { onSelectPlace(item.id) }
)
) {
Row(
modifier = Modifier
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_TEXT),
text = "${item.roomName}${item.time}",
style = MaterialTheme.typography.bodyMedium
)
}
RadioButton(
modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_SELECTOR),
selected = item.isSelected,
onClick = { onSelectPlace(item.id) }
)
}
}
}
}
Spacer(modifier = Modifier.size(8.dp))
Button(
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Book.BOOK_BUTTON),
onClick = onBook,
enabled = state.places.any { it.isSelected }
) {
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 = "Забронировать"
)
}
}
}
}

View File

@@ -0,0 +1,24 @@
package ru.myitschool.work.ui.screen.book
data class BookPlaceItem(
val id: Int,
val roomName: String,
val time: String,
val isSelected: Boolean,
)
sealed interface BookState {
object Loading : BookState
data class Data(
val dates: List<String>,
val selectedDateIndex: Int,
val places: List<BookPlaceItem>,
) : BookState
data class Error(
val message: String,
) : BookState
object Empty : BookState
}

View File

@@ -0,0 +1,210 @@
package ru.myitschool.work.ui.screen.book
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import java.time.LocalDate
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.launch
import ru.myitschool.work.core.DateUtils
import ru.myitschool.work.data.repo.UserRepository
import ru.myitschool.work.domain.booking.BookPlaceUseCase
import ru.myitschool.work.domain.booking.GetAvailableBookingsUseCase
import ru.myitschool.work.domain.entities.BookingEntity
class BookViewModel : ViewModel() {
private val getAvailableBookingsUseCase by lazy {
GetAvailableBookingsUseCase(UserRepository)
}
private val bookPlaceUseCase by lazy {
BookPlaceUseCase(UserRepository)
}
private val _uiState = MutableStateFlow<BookState>(BookState.Loading)
val uiState: StateFlow<BookState> = _uiState.asStateFlow()
sealed interface Action {
data object CloseWithSuccess : Action
}
private val _actionFlow = MutableSharedFlow<Action>()
val actionFlow: SharedFlow<Action> = _actionFlow
private var allGroups: List<DateGroup> = emptyList()
private var selectedDateIndex: Int = 0
private var selectedSlotId: Int? = null
fun onIntent(intent: BookIntent) {
when (intent) {
BookIntent.Load,
BookIntent.Refresh -> {
load()
}
is BookIntent.SelectDate -> {
selectDate(intent.index)
}
is BookIntent.SelectPlace -> {
selectedSlotId = intent.id
val current = _uiState.value
if (current is BookState.Data) {
val updatedPlaces = current.places.map { item ->
item.copy(isSelected = item.id == intent.id)
}
_uiState.value = current.copy(places = updatedPlaces)
}
}
BookIntent.Book -> {
book()
}
}
}
private fun load() {
viewModelScope.launch(Dispatchers.Default) {
_uiState.value = BookState.Loading
getAvailableBookingsUseCase()
.fold(
onSuccess = { bookings ->
if (bookings.isEmpty()) {
_uiState.value = BookState.Empty
allGroups = emptyList()
selectedSlotId = null
selectedDateIndex = 0
return@fold
}
allGroups = groupByDate(bookings)
if (allGroups.isEmpty()) {
_uiState.value = BookState.Empty
selectedSlotId = null
selectedDateIndex = 0
return@fold
}
selectedDateIndex = 0
selectedSlotId = null
val firstGroup = allGroups[0]
val places = firstGroup.slots.map { slot ->
BookPlaceItem(
id = slot.id,
roomName = slot.roomName,
time = slot.time,
isSelected = false,
)
}
_uiState.value = BookState.Data(
dates = allGroups.map { it.label },
selectedDateIndex = 0,
places = places,
)
},
onFailure = { error ->
_uiState.value = BookState.Error(
message = error.message ?: "Ошибка загрузки бронирований",
)
}
)
}
}
private fun selectDate(index: Int) {
val groups = allGroups
if (index !in groups.indices) return
selectedDateIndex = index
selectedSlotId = null
val group = groups[index]
val places = group.slots.map { slot ->
BookPlaceItem(
id = slot.id,
roomName = slot.roomName,
time = slot.time,
isSelected = false,
)
}
val datesLabels = groups.map { it.label }
val current = _uiState.value
_uiState.value = if (current is BookState.Data) {
current.copy(
dates = datesLabels,
selectedDateIndex = index,
places = places,
)
} else {
BookState.Data(
dates = datesLabels,
selectedDateIndex = index,
places = places,
)
}
}
private fun book() {
val current = _uiState.value
if (current !is BookState.Data) 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(selectedDate, slotId)
.fold(
onSuccess = {
_actionFlow.emit(Action.CloseWithSuccess)
},
onFailure = { error ->
_uiState.value = BookState.Error(
message = error.message ?: "Ошибка бронирования",
)
}
)
}
}
private data class DateGroup(
val date: LocalDate,
val label: String,
val slots: List<BookingEntity>,
)
private fun groupByDate(bookings: List<BookingEntity>): List<DateGroup> {
val grouped: Map<LocalDate, List<BookingEntity>> =
bookings
.mapNotNull { booking ->
val date = DateUtils.parseDate(booking.time) ?: return@mapNotNull null
date to booking
}
.groupBy({ it.first }, { it.second })
return grouped.entries
.sortedBy { it.key }
.map { (date, slots) ->
DateGroup(
date = date,
label = DateUtils.formatForBook(date.toString()),
slots = slots,
)
}
}
}

View File

@@ -0,0 +1,8 @@
package ru.myitschool.work.ui.screen.main
sealed interface MainIntent {
data object Load : MainIntent
data object Refresh : MainIntent
data object Logout : MainIntent
data object AddBooking : MainIntent
}

View File

@@ -0,0 +1,334 @@
package ru.myitschool.work.ui.screen.main
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
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
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.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
@Composable
fun MainScreen(
navController: NavController,
viewModel: MainViewModel = viewModel()
) {
val state by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) {
viewModel.onIntent(MainIntent.Load)
viewModel.actionFlow.collect { action ->
when (action) {
MainViewModel.Action.NavigateToAuth -> {
navController.navigate(AuthScreenDestination) {
popUpTo(AuthScreenDestination) { inclusive = true }
}
}
MainViewModel.Action.NavigateToBooking -> {
navController.navigate(BookScreenDestination)
}
}
}
}
when (val current = state) {
is MainState.Loading -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
is MainState.Error -> {
MainErrorContent(
message = current.message,
onRefresh = { viewModel.onIntent(MainIntent.Refresh) }
)
}
is MainState.Data -> {
MainDataContent(
state = current,
onRefresh = { viewModel.onIntent(MainIntent.Refresh) },
onLogout = { viewModel.onIntent(MainIntent.Logout) },
onAddBooking = { viewModel.onIntent(MainIntent.AddBooking) }
)
}
}
}
@Composable
private fun MainErrorContent(
message: String,
onRefresh: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier.testTag(TestIds.Main.ERROR),
text = message,
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.size(16.dp))
Button(
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON),
onClick = onRefresh
) {
Text("Обновить")
}
}
}
@Composable
private fun MainDataContent(
state: MainState.Data,
onRefresh: () -> Unit,
onLogout: () -> Unit,
onAddBooking: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Spacer(modifier = Modifier.size(16.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(64.dp)
.testTag(TestIds.Main.PROFILE_IMAGE),
contentAlignment = Alignment.Center
) {
UserAvatar(
photoUrl = state.photoUrl,
modifier = Modifier
.size(64.dp)
)
}
Spacer(modifier = Modifier.size(16.dp))
Text(
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(24.dp))
Text(
text = stringResource(R.string.main_screen_title),
style = MaterialTheme.typography.titleLarge.copy(
fontWeight = FontWeight.Bold
)
)
Spacer(modifier = Modifier.size(20.dp))
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(state.bookings, key = { _, item -> item.id }) { index, item ->
Card(
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Main.getIdItemByPosition(index))
) {
Column(
modifier = Modifier.padding(12.dp)
) {
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.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
private fun UserAvatar(
photoUrl: String?,
modifier: Modifier = Modifier,
) {
if (photoUrl.isNullOrBlank()) {
// Фолбек на старый смайл / иконку
Image(
imageVector = Icons.Default.Person,
contentDescription = null,
modifier = modifier
.size(64.dp)
.clip(CircleShape),
)
} else {
SubcomposeAsyncImage(
model = photoUrl,
contentDescription = null,
modifier = modifier
.size(64.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop,
loading = {
Image(
imageVector = Icons.Default.Person,
contentDescription = null,
modifier = Modifier.size(64.dp),
)
},
error = {
Image(
imageVector = Icons.Default.Person,
contentDescription = null,
modifier = Modifier.size(64.dp),
)
}
)
}
}

View File

@@ -0,0 +1,21 @@
package ru.myitschool.work.ui.screen.main
data class MainBookingItem(
val id: Int,
val dateLabel: String,
val roomName: String,
)
sealed interface MainState {
object Loading : MainState
data class Data(
val name: String,
val photoUrl: String?,
val bookings: List<MainBookingItem>,
) : MainState
data class Error(
val message: String,
) : MainState
}

View File

@@ -0,0 +1,91 @@
package ru.myitschool.work.ui.screen.main
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import java.time.LocalDate
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.launch
import ru.myitschool.work.core.DateUtils
import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.data.repo.UserRepository
import ru.myitschool.work.domain.auth.ClearAuthDataUseCase
import ru.myitschool.work.domain.user.GetUserInfoUseCase
class MainViewModel : ViewModel() {
private val getUserInfoUseCase by lazy { GetUserInfoUseCase(UserRepository) }
private val clearAuthDataUseCase by lazy { ClearAuthDataUseCase(AuthRepository) }
private val _uiState = MutableStateFlow<MainState>(MainState.Loading)
val uiState: StateFlow<MainState> = _uiState.asStateFlow()
sealed interface Action {
data object NavigateToAuth : Action
data object NavigateToBooking : Action
}
private val _actionFlow = MutableSharedFlow<Action>()
val actionFlow: SharedFlow<Action> = _actionFlow
fun onIntent(intent: MainIntent) {
when (intent) {
MainIntent.Load,
MainIntent.Refresh -> {
load()
}
MainIntent.Logout -> {
viewModelScope.launch(Dispatchers.Default) {
clearAuthDataUseCase()
_actionFlow.emit(Action.NavigateToAuth)
}
}
MainIntent.AddBooking -> {
viewModelScope.launch {
_actionFlow.emit(Action.NavigateToBooking)
}
}
}
}
private fun load() {
viewModelScope.launch(Dispatchers.Default) {
_uiState.value = MainState.Loading
getUserInfoUseCase()
.fold(
onSuccess = { user ->
val bookings = user.bookings
.sortedWith(
compareBy { booking ->
DateUtils.parseDate(booking.time) ?: LocalDate.MAX
}
)
.mapIndexed { index, booking ->
MainBookingItem(
id = index,
dateLabel = DateUtils.formatForMain(booking.time),
roomName = booking.roomName,
)
}
_uiState.value = MainState.Data(
name = user.name,
photoUrl = user.photoUrl,
bookings = bookings,
)
},
onFailure = { error ->
_uiState.value = MainState.Error(
message = error.message ?: "Ошибка загрузки данных",
)
}
)
}
}
}

View File

@@ -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
)
}

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -1,7 +1,9 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
androidApplication version Version.agp apply false
kotlinJvm version Version.Kotlin.language apply false
kotlinAndroid version Version.Kotlin.language apply false
composeCompiler version Version.Kotlin.language apply false
// id("com.android.application") version "8.5.2" apply false
id("com.android.application") version "8.6.0" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.2.20" apply false
}

View File

@@ -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
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
#Thu Dec 04 14:16:52 GMT+07:00 2025
android.nonTransitiveRClass=true
android.useAndroidX=true
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" -Dfile.encoding\=UTF-8

43
gradlew vendored
View File

@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +82,11 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -133,22 +133,29 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -193,11 +200,15 @@ if "$cygwin" || "$msys" ; then
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
@@ -205,6 +216,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.

37
gradlew.bat vendored
View File

@@ -13,8 +13,10 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +27,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal