From 02b3e891a683f9213a04074971647657b532b5c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B0=D1=82=D1=8C=D1=8F=D0=BD=D0=B0=20=D0=90=D1=80?= =?UTF-8?q?=D1=82=D0=B0=D0=BC=D0=BE=D0=BD=D0=BE=D0=B2=D0=B0?= Date: Sat, 23 Dec 2023 09:14:46 +0400 Subject: [PATCH] commit --- .gitignore | 16 + app/.gitignore | 1 + app/build.gradle.kts | 101 ++ app/proguard-rules.pro | 21 + .../ExampleInstrumentedTest.kt | 24 + app/src/main/AndroidManifest.xml | 30 + .../is/airticketrentservice/MainActivity.kt | 42 + .../airticketrentservice/TicketApplication.kt | 14 + .../is/airticketrentservice/api/AppService.kt | 152 ++ .../api/mediator/FlightRemoteMediator.kt | 107 ++ .../api/mediator/RentRemoteMediator.kt | 109 ++ .../api/mediator/TicketRemoteMediator.kt | 110 ++ .../api/model/FlightRemote.kt | 35 + .../api/model/RentRemote.kt | 26 + .../api/model/TicketRemote.kt | 26 + .../api/model/UserRemote.kt | 38 + .../api/repository/RestFlightRepository.kt | 118 ++ .../api/repository/RestRentRepository.kt | 81 + .../api/repository/RestTicketRepository.kt | 99 ++ .../api/repository/RestUserRepository.kt | 81 + .../database/AppContainer.kt | 75 + .../database/AppDatabase.kt | 102 ++ .../database/dao/FlightDao.kt | 37 + .../database/dao/RemoteKeysDao.kt | 20 + .../database/dao/RentDao.kt | 34 + .../database/dao/TicketDao.kt | 39 + .../database/dao/UserDao.kt | 32 + .../database/models/Flight.kt | 28 + .../database/models/RemoteKeys.kt | 27 + .../database/models/Rent.kt | 44 + .../database/models/RoleEnum.kt | 6 + .../database/models/Ticket.kt | 36 + .../database/models/User.kt | 34 + .../database/repository/FlightRepository.kt | 14 + .../repository/OfflineFlightRepository.kt | 31 + .../repository/OfflineRemoteKeyRepository.kt | 16 + .../repository/OfflineRentRepository.kt | 30 + .../repository/OfflineTicketRepository.kt | 31 + .../repository/OfflineUserRepository.kt | 15 + .../repository/RemoteKeysRepository.kt | 10 + .../database/repository/RentRepository.kt | 15 + .../database/repository/TicketRepository.kt | 15 + .../database/repository/UserRepository.kt | 13 + .../elements/EmailTextInput.kt | 59 + .../graphs/AuthNavGraph.kt | 32 + .../graphs/HomeNavGraph.kt | 135 ++ .../graphs/RootNavGraph.kt | 59 + .../navigation/BottomBar.kt | 113 ++ .../navigation/BottomBarScreen.kt | 110 ++ .../navigation/MainNavbar.kt | 362 +++++ .../airticketrentservice/navigation/Screen.kt | 76 + .../airticketrentservice/navigation/TopBar.kt | 49 + .../is/airticketrentservice/screen/Admin.kt | 83 + .../airticketrentservice/screen/FlightEdit.kt | 253 ++++ .../airticketrentservice/screen/FlightInfo.kt | 148 ++ .../airticketrentservice/screen/FlightList.kt | 243 +++ .../screen/FoundFlights.kt | 109 ++ .../airticketrentservice/screen/MainPage.kt | 245 +++ .../is/airticketrentservice/screen/MyRents.kt | 219 +++ .../is/airticketrentservice/screen/Profile.kt | 189 +++ .../airticketrentservice/screen/RentEdit.kt | 248 +++ .../airticketrentservice/screen/RentList.kt | 255 ++++ .../airticketrentservice/screen/TicketEdit.kt | 198 +++ .../airticketrentservice/screen/UserEdit.kt | 206 +++ .../airticketrentservice/screen/UserList.kt | 208 +++ .../airticketrentservice/screen/auth/Login.kt | 179 +++ .../screen/auth/Registration.kt | 343 +++++ .../is/airticketrentservice/ui/theme/Color.kt | 11 + .../is/airticketrentservice/ui/theme/Theme.kt | 70 + .../is/airticketrentservice/ui/theme/Type.kt | 34 + .../viewModel/AppViewModelProvider.kt | 74 + .../viewModel/CurrentUserViewModel.kt | 29 + .../viewModel/FindFlightsViewModel.kt | 36 + .../viewModel/FlightEditViewModel.kt | 104 ++ .../viewModel/FlightListViewModel.kt | 40 + .../viewModel/LoginViewModel.kt | 20 + .../viewModel/RegistrationViewModel.kt | 25 + .../viewModel/RentEditViewModel.kt | 85 ++ .../viewModel/RentListViewModel.kt | 44 + .../viewModel/TicketEditViewModel.kt | 108 ++ .../viewModel/TicketListViewModel.kt | 34 + .../viewModel/UserEditViewModel.kt | 126 ++ .../viewModel/UserListViewModel.kt | 33 + .../viewModel/UsersRentsViewModel.kt | 38 + .../res/drawable/ic_launcher_background.xml | 170 +++ .../res/drawable/ic_launcher_foreground.xml | 30 + app/src/main/res/drawable/logo.png | Bin 0 -> 56284 bytes .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes app/src/main/res/values/colors.xml | 13 + app/src/main/res/values/strings.xml | 65 + app/src/main/res/values/themes.xml | 5 + app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + .../main/res/xml/network_security_config.xml | 6 + .../airticketrentservice/ExampleUnitTest.kt | 17 + build.gradle.kts | 7 + gradle.properties | 23 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 185 +++ gradlew.bat | 89 ++ server/data.json | 134 ++ server/data.json.bak | 134 ++ server/package-lock.json | 1335 +++++++++++++++++ server/package.json | 12 + settings.gradle.kts | 18 + 117 files changed, 9260 insertions(+) create mode 100644 .gitignore create mode 100644 app/.gitignore create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/ru/ulstu/is/airticketrentservice/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/MainActivity.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/TicketApplication.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/api/AppService.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/api/mediator/FlightRemoteMediator.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/api/mediator/RentRemoteMediator.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/api/mediator/TicketRemoteMediator.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/api/model/FlightRemote.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/api/model/RentRemote.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/api/model/TicketRemote.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/api/model/UserRemote.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/api/repository/RestFlightRepository.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/api/repository/RestRentRepository.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/api/repository/RestTicketRepository.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/api/repository/RestUserRepository.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/AppContainer.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/AppDatabase.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/FlightDao.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/RemoteKeysDao.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/RentDao.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/TicketDao.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/UserDao.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/Flight.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/RemoteKeys.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/Rent.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/RoleEnum.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/Ticket.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/User.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/FlightRepository.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineFlightRepository.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineRemoteKeyRepository.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineRentRepository.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineTicketRepository.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineUserRepository.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/RemoteKeysRepository.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/RentRepository.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/TicketRepository.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/UserRepository.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/elements/EmailTextInput.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/graphs/AuthNavGraph.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/graphs/HomeNavGraph.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/graphs/RootNavGraph.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/BottomBar.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/BottomBarScreen.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/MainNavbar.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/Screen.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/TopBar.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/screen/Admin.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/screen/FlightEdit.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/screen/FlightInfo.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/screen/FlightList.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/screen/FoundFlights.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/screen/MainPage.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/screen/MyRents.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/screen/Profile.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/screen/RentEdit.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/screen/RentList.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/screen/TicketEdit.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/screen/UserEdit.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/screen/UserList.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/screen/auth/Login.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/screen/auth/Registration.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/ui/theme/Color.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/ui/theme/Theme.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/ui/theme/Type.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/AppViewModelProvider.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/CurrentUserViewModel.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/FindFlightsViewModel.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/FlightEditViewModel.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/FlightListViewModel.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/LoginViewModel.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/RegistrationViewModel.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/RentEditViewModel.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/RentListViewModel.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/TicketEditViewModel.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/TicketListViewModel.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/UserEditViewModel.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/UserListViewModel.kt create mode 100644 app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/UsersRentsViewModel.kt create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/logo.png create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/main/res/xml/network_security_config.xml create mode 100644 app/src/test/java/ru/ulstu/is/airticketrentservice/ExampleUnitTest.kt create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 server/data.json create mode 100644 server/data.json.bak create mode 100644 server/package-lock.json create mode 100644 server/package.json create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d7d8711 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +/server/node_modules/ diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..585020d --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,101 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("com.google.devtools.ksp") + id("org.jetbrains.kotlin.plugin.serialization") +} + +android { + namespace = "ru.ulstu.is.airticketrentservice" + compileSdk = 34 + + defaultConfig { + applicationId = "ru.ulstu.is.airticketrentservice" + minSdk = 24 + targetSdk = 33 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.4.5" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + // Core + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-beta01") + + // UI + implementation("androidx.activity:activity-compose:1.7.2") + implementation(platform("androidx.compose:compose-bom:2023.03.00")) + implementation("androidx.navigation:navigation-compose:2.6.0") + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.ui:ui-graphics") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material3:material3:1.1.2") + + // Room + val room_version = "2.5.2" + implementation("androidx.room:room-runtime:$room_version") + annotationProcessor("androidx.room:room-compiler:$room_version") + ksp("androidx.room:room-compiler:$room_version") + implementation("androidx.room:room-ktx:$room_version") + implementation("androidx.room:room-paging:$room_version") + + implementation("com.google.android.material:material:1.4.0") + + // Tests + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00")) + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + debugImplementation("androidx.compose.ui:ui-tooling") + debugImplementation("androidx.compose.ui:ui-test-manifest") + debugImplementation("androidx.compose.material:material-icons-extended:1.5.3") + implementation("androidx.compose.material:material:1.4.3") + + //Paging + implementation ("androidx.paging:paging-compose:3.2.1") + implementation ("androidx.paging:paging-runtime-ktx:3.2.1") + + // retrofit + val retrofitVersion = "2.9.0" + implementation("com.squareup.retrofit2:retrofit:$retrofitVersion") + implementation("com.squareup.okhttp3:logging-interceptor:4.11.0") + implementation("androidx.paging:paging-compose:3.2.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") + implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/ru/ulstu/is/airticketrentservice/ExampleInstrumentedTest.kt b/app/src/androidTest/java/ru/ulstu/is/airticketrentservice/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..76e76bf --- /dev/null +++ b/app/src/androidTest/java/ru/ulstu/is/airticketrentservice/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package ru.ulstu.`is`.airticketrentservice + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("ru.ulstu.is.airticketrentservice", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b245d4c --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/MainActivity.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/MainActivity.kt new file mode 100644 index 0000000..0285f51 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/MainActivity.kt @@ -0,0 +1,42 @@ +package ru.ulstu.`is`.airticketrentservice + +import android.os.Build +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.annotation.RequiresApi +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.compose.rememberNavController +import ru.ulstu.`is`.airticketrentservice.graphs.RootNavigationGraph +//import ru.ulstu.`is`.airticketrentservice.navigation.MainNavbar +import ru.ulstu.`is`.airticketrentservice.ui.theme.AirTicketRentServiceTheme +import ru.ulstu.`is`.airticketrentservice.viewModel.AppViewModelProvider +import ru.ulstu.`is`.airticketrentservice.viewModel.CurrentUserViewModel + +class MainActivity : ComponentActivity() { +// @RequiresApi(Build.VERSION_CODES.O) +// override fun onCreate(savedInstanceState: Bundle?) { +// super.onCreate(savedInstanceState) +// setContent { +// AirTicketRentServiceTheme { +// Surface( +// modifier = Modifier.fillMaxSize(), +// color = MaterialTheme.colorScheme.background +// ) { +// MainNavbar() +// } +// } +// } +// } + private val currentUserViewModel: CurrentUserViewModel by viewModels() + @RequiresApi(Build.VERSION_CODES.O) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + AirTicketRentServiceTheme { + RootNavigationGraph(navController = rememberNavController(), currentUserViewModel = viewModel(factory = AppViewModelProvider.Factory)) + } + } + } +} diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/TicketApplication.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/TicketApplication.kt new file mode 100644 index 0000000..3d39116 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/TicketApplication.kt @@ -0,0 +1,14 @@ +package ru.ulstu.`is`.airticketrentservice + +import android.app.Application +import ru.ulstu.`is`.airticketrentservice.database.AppContainer +import ru.ulstu.`is`.airticketrentservice.database.AppDataContainer + +class TicketApplication : Application() { + lateinit var container: AppContainer + + override fun onCreate() { + super.onCreate() + container = AppDataContainer(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/api/AppService.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/AppService.kt new file mode 100644 index 0000000..540fc8c --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/AppService.kt @@ -0,0 +1,152 @@ +package ru.ulstu.`is`.airticketrentservice.api + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import kotlinx.coroutines.flow.Flow +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Call +import retrofit2.Retrofit +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query +import ru.ulstu.`is`.airticketrentservice.api.model.FlightRemote +import ru.ulstu.`is`.airticketrentservice.api.model.RentRemote +import ru.ulstu.`is`.airticketrentservice.api.model.TicketRemote +import ru.ulstu.`is`.airticketrentservice.api.model.UserRemote + +interface AppService { + @GET("users") + suspend fun getAllUsers(): List + @GET("rents") + suspend fun getRents( + @Query("_page") page: Int, + @Query("_limit") limit: Int, + ): List + @GET("findFlights/{direction_from}-{direction_to}-{departure_date}") + suspend fun findFlights( + @Path("direction_from") from: String, + @Path("direction_to") to: String, + @Path("departure_date") departureDate: String + ): List + @GET("tickets") + suspend fun getAllTickets(): List + @GET("tickets") + suspend fun getTickets( + @Query("_page") page: Int, + @Query("_limit") limit: Int, + ): List + @GET("flights") + suspend fun getFlights( + @Query("_page") page: Int, + @Query("_limit") limit: Int, + ): List + + @GET("tickets") + suspend fun getFlightsTickets(@Query("flightId") flightId: Int): List + + @GET("userrents/{userId}") + suspend fun getUserRents(@Path("userId") userId: Int): List + + @GET("users/{id}") + suspend fun getUser( + @Path("id") id: Int, + ): UserRemote + @POST("users") + suspend fun createUser( + @Body user: UserRemote, + ): UserRemote + @PUT("users/{id}") + suspend fun updateUser( + @Path("id") id: Int, + @Body user: UserRemote, + ): UserRemote + @DELETE("users/{id}") + suspend fun deleteUser( + @Path("id") id: Int, + ): UserRemote + + + @GET("rents/{id}") + suspend fun getRent( + @Path("id") id: Int, + ): RentRemote + @POST("rents") + suspend fun createRent( + @Body rent: RentRemote, + ): RentRemote + @PUT("rents/{id}") + suspend fun updateRent( + @Path("id") id: Int, + @Body rent: RentRemote, + ): RentRemote + @DELETE("rents/{id}") + suspend fun deleteRent( + @Path("id") id: Int, + ): RentRemote + + @GET("flights/{id}") + suspend fun getFlight( + @Path("id") id: Int, + ): FlightRemote + @POST("flights") + suspend fun createFlight( + @Body flight: FlightRemote, + ): FlightRemote + @PUT("flights/{id}") + suspend fun updateFlight( + @Path("id") id: Int, + @Body flight: FlightRemote, + ): FlightRemote + @DELETE("flights/{id}") + suspend fun deleteFlight( + @Path("id") id: Int, + ): FlightRemote + + @GET("tickets/{id}") + suspend fun getTicket( + @Path("id") id: Int, + ): TicketRemote + @POST("tickets") + suspend fun createTicket( + @Body ticket: TicketRemote, + ): TicketRemote + @PUT("tickets/{id}") + suspend fun updateTicket( + @Path("id") id: Int, + @Body ticket: TicketRemote, + ): TicketRemote + @DELETE("tickets/{id}") + suspend fun deleteTicket( + @Path("id") id: Int, + ): TicketRemote + + companion object { + private const val BASE_URL = "http://192.168.1.100:8079/" + + @Volatile + private var INSTANCE: AppService? = null + + fun getInstance(): AppService { + return INSTANCE ?: synchronized(this) { + val logger = HttpLoggingInterceptor() + logger.level = HttpLoggingInterceptor.Level.BASIC + val client = OkHttpClient.Builder() + .addInterceptor(logger) + .build() + return Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) + .build() + .create(AppService::class.java) + .also { INSTANCE = it } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/api/mediator/FlightRemoteMediator.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/mediator/FlightRemoteMediator.kt new file mode 100644 index 0000000..93d87ca --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/mediator/FlightRemoteMediator.kt @@ -0,0 +1,107 @@ +package ru.ulstu.`is`.airticketrentservice.api.mediator + +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import androidx.room.withTransaction +import retrofit2.HttpException +import ru.ulstu.`is`.airticketrentservice.api.AppService +import ru.ulstu.`is`.airticketrentservice.api.model.toFlight +import ru.ulstu.`is`.airticketrentservice.database.AppDatabase +import ru.ulstu.`is`.airticketrentservice.database.models.Flight +import ru.ulstu.`is`.airticketrentservice.database.models.RemoteKeyType +import ru.ulstu.`is`.airticketrentservice.database.models.RemoteKeys +import ru.ulstu.`is`.airticketrentservice.database.repository.OfflineFlightRepository +import ru.ulstu.`is`.airticketrentservice.database.repository.OfflineRemoteKeyRepository +import java.io.IOException + +@OptIn(ExperimentalPagingApi::class) +class FlightRemoteMediator( + private val service: AppService, + private val dbFlightRepository: OfflineFlightRepository, + private val dbRemoteKeyRepository: OfflineRemoteKeyRepository, + private val database: AppDatabase +) : RemoteMediator() { + + override suspend fun initialize(): InitializeAction { + return InitializeAction.LAUNCH_INITIAL_REFRESH + } + + override suspend fun load( + loadType: LoadType, + state: PagingState + ): MediatorResult { + val page = when (loadType) { + LoadType.REFRESH -> { + val remoteKeys = getRemoteKeyClosestToCurrentPosition(state) + remoteKeys?.nextKey?.minus(1) ?: 1 + } + + LoadType.PREPEND -> { + val remoteKeys = getRemoteKeyForFirstItem(state) + remoteKeys?.prevKey + ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null) + } + + LoadType.APPEND -> { + val remoteKeys = getRemoteKeyForLastItem(state) + remoteKeys?.nextKey + ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null) + } + } + + try { + val flights = service.getFlights(page, state.config.pageSize).map { it.toFlight() } + val endOfPaginationReached = flights.isEmpty() + database.withTransaction { + if (loadType == LoadType.REFRESH) { + dbRemoteKeyRepository.deleteRemoteKey(RemoteKeyType.FLIGHT) + dbFlightRepository.clearFlights() + } + val prevKey = if (page == 1) null else page - 1 + val nextKey = if (endOfPaginationReached) null else page + 1 + val keys = flights.map { + RemoteKeys( + entityId = it.id, + type = RemoteKeyType.FLIGHT, + prevKey = prevKey, + nextKey = nextKey + ) + } + dbRemoteKeyRepository.createRemoteKeys(keys) + dbFlightRepository.insertFlights(flights) + } + return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) + } catch (exception: IOException) { + return MediatorResult.Error(exception) + } catch (exception: HttpException) { + return MediatorResult.Error(exception) + } + } + + private suspend fun getRemoteKeyForLastItem(state: PagingState): RemoteKeys? { + return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull() + ?.let { flight -> + flight.id.let { dbRemoteKeyRepository.getAllRemoteKeys(it, RemoteKeyType.FLIGHT) } + } + } + + private suspend fun getRemoteKeyForFirstItem(state: PagingState): RemoteKeys? { + return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull() + ?.let { flight -> + flight.id.let { dbRemoteKeyRepository.getAllRemoteKeys(it, RemoteKeyType.FLIGHT) } + } + } + + private suspend fun getRemoteKeyClosestToCurrentPosition( + state: PagingState + ): RemoteKeys? { + return state.anchorPosition?.let { position -> + state.closestItemToPosition(position)?.id?.let { flightid -> + dbRemoteKeyRepository.getAllRemoteKeys(flightid, RemoteKeyType.FLIGHT) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/api/mediator/RentRemoteMediator.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/mediator/RentRemoteMediator.kt new file mode 100644 index 0000000..c88726e --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/mediator/RentRemoteMediator.kt @@ -0,0 +1,109 @@ +package ru.ulstu.`is`.airticketrentservice.api.mediator + +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import androidx.room.withTransaction +import retrofit2.HttpException +import ru.ulstu.`is`.airticketrentservice.api.AppService +import ru.ulstu.`is`.airticketrentservice.api.model.toRent +import ru.ulstu.`is`.airticketrentservice.api.repository.RestTicketRepository +import ru.ulstu.`is`.airticketrentservice.database.AppDatabase +import ru.ulstu.`is`.airticketrentservice.database.repository.OfflineRemoteKeyRepository +import ru.ulstu.`is`.airticketrentservice.database.repository.OfflineRentRepository +import ru.ulstu.`is`.airticketrentservice.database.models.RemoteKeyType +import ru.ulstu.`is`.airticketrentservice.database.models.RemoteKeys +import ru.ulstu.`is`.airticketrentservice.database.models.Rent +import java.io.IOException + +@OptIn(ExperimentalPagingApi::class) +class RentRemoteMediator( + private val service: AppService, + private val dbRentRepository: OfflineRentRepository, + private val dbRemoteKeyRepository: OfflineRemoteKeyRepository, + private val ticketRestRepository: RestTicketRepository, + private val database: AppDatabase +) : RemoteMediator() { + + override suspend fun initialize(): InitializeAction { + return InitializeAction.LAUNCH_INITIAL_REFRESH + } + + override suspend fun load( + loadType: LoadType, + state: PagingState + ): MediatorResult { + val page = when (loadType) { + LoadType.REFRESH -> { + val remoteKeys = getRemoteKeyClosestToCurrentPosition(state) + remoteKeys?.nextKey?.minus(1) ?: 1 + } + + LoadType.PREPEND -> { + val remoteKeys = getRemoteKeyForFirstItem(state) + remoteKeys?.prevKey + ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null) + } + + LoadType.APPEND -> { + val remoteKeys = getRemoteKeyForLastItem(state) + remoteKeys?.nextKey + ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null) + } + } + + try { + val rents = service.getRents(page, state.config.pageSize).map { it.toRent() } + val endOfPaginationReached = rents.isEmpty() + database.withTransaction { + if (loadType == LoadType.REFRESH) { + dbRemoteKeyRepository.deleteRemoteKey(RemoteKeyType.RENT) + dbRentRepository.clearRents() + } + val prevKey = if (page == 1) null else page - 1 + val nextKey = if (endOfPaginationReached) null else page + 1 + val keys = rents.map { + RemoteKeys( + entityId = it.id, + type = RemoteKeyType.RENT, + prevKey = prevKey, + nextKey = nextKey + ) + } + dbRemoteKeyRepository.createRemoteKeys(keys) + ticketRestRepository.getTickets() + dbRentRepository.insertRents(rents) + } + return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) + } catch (exception: IOException) { + return MediatorResult.Error(exception) + } catch (exception: HttpException) { + return MediatorResult.Error(exception) + } + } + + private suspend fun getRemoteKeyForLastItem(state: PagingState): RemoteKeys? { + return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull() + ?.let { rent -> + dbRemoteKeyRepository.getAllRemoteKeys(rent.id, RemoteKeyType.RENT) + } + } + + private suspend fun getRemoteKeyForFirstItem(state: PagingState): RemoteKeys? { + return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull() + ?.let { rent -> + dbRemoteKeyRepository.getAllRemoteKeys(rent.id, RemoteKeyType.RENT) + } + } + + private suspend fun getRemoteKeyClosestToCurrentPosition( + state: PagingState + ): RemoteKeys? { + return state.anchorPosition?.let { position -> + state.closestItemToPosition(position)?.id?.let { rentUid -> + dbRemoteKeyRepository.getAllRemoteKeys(rentUid, RemoteKeyType.RENT) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/api/mediator/TicketRemoteMediator.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/mediator/TicketRemoteMediator.kt new file mode 100644 index 0000000..d8bd85b --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/mediator/TicketRemoteMediator.kt @@ -0,0 +1,110 @@ +package ru.ulstu.`is`.airticketrentservice.api.mediator + +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import androidx.room.withTransaction +import retrofit2.HttpException +import ru.ulstu.`is`.airticketrentservice.api.AppService +import ru.ulstu.`is`.airticketrentservice.api.model.toTicket +import ru.ulstu.`is`.airticketrentservice.api.repository.RestFlightRepository +import ru.ulstu.`is`.airticketrentservice.database.AppDatabase +import ru.ulstu.`is`.airticketrentservice.database.models.RemoteKeyType +import ru.ulstu.`is`.airticketrentservice.database.models.RemoteKeys +import ru.ulstu.`is`.airticketrentservice.database.models.Ticket +import ru.ulstu.`is`.airticketrentservice.database.repository.OfflineRemoteKeyRepository +import ru.ulstu.`is`.airticketrentservice.database.repository.OfflineTicketRepository +import java.io.IOException + +@OptIn(ExperimentalPagingApi::class) +class TicketRemoteMediator( + private val service: AppService, + private val dbTicketRepository: OfflineTicketRepository, + private val dbRemoteKeyRepository: OfflineRemoteKeyRepository, + private val flightRestRepository: RestFlightRepository, + private val database: AppDatabase +) : RemoteMediator() { + + override suspend fun initialize(): InitializeAction { + return InitializeAction.LAUNCH_INITIAL_REFRESH + } + + override suspend fun load( + loadType: LoadType, + state: PagingState + ): MediatorResult { + val page = when (loadType) { + LoadType.REFRESH -> { + val remoteKeys = getRemoteKeyClosestToCurrentPosition(state) + remoteKeys?.nextKey?.minus(1) ?: 1 + } + + LoadType.PREPEND -> { + val remoteKeys = getRemoteKeyForFirstItem(state) + remoteKeys?.prevKey + ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null) + } + + LoadType.APPEND -> { + val remoteKeys = getRemoteKeyForLastItem(state) + remoteKeys?.nextKey + ?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null) + } + } + + try { + val tickets = service.getTickets(page, state.config.pageSize).map { it.toTicket() } + val endOfPaginationReached = tickets.isEmpty() + database.withTransaction { + if (loadType == LoadType.REFRESH) { + dbRemoteKeyRepository.deleteRemoteKey(RemoteKeyType.TICKET) + dbTicketRepository.clearTickets() + } + val prevKey = if (page == 1) null else page - 1 + val nextKey = if (endOfPaginationReached) null else page + 1 + val keys = tickets.map { + RemoteKeys( + entityId = it.id, + type = RemoteKeyType.TICKET, + prevKey = prevKey, + nextKey = nextKey + ) + } + flightRestRepository.getFlights() + dbRemoteKeyRepository.createRemoteKeys(keys) + dbTicketRepository.insertTickets(tickets) + } + return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) + } catch (exception: IOException) { + return MediatorResult.Error(exception) + } catch (exception: HttpException) { + return MediatorResult.Error(exception) + } + } + + private suspend fun getRemoteKeyForLastItem(state: PagingState): RemoteKeys? { + return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull() + ?.let { ticket -> + dbRemoteKeyRepository.getAllRemoteKeys(ticket.id, RemoteKeyType.TICKET) + } + } + + private suspend fun getRemoteKeyForFirstItem(state: PagingState): RemoteKeys? { + return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull() + ?.let { ticket -> + dbRemoteKeyRepository.getAllRemoteKeys(ticket.id, RemoteKeyType.TICKET) + } + } + + private suspend fun getRemoteKeyClosestToCurrentPosition( + state: PagingState + ): RemoteKeys? { + return state.anchorPosition?.let { position -> + state.closestItemToPosition(position)?.id?.let { ticketUid -> + dbRemoteKeyRepository.getAllRemoteKeys(ticketUid, RemoteKeyType.TICKET) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/api/model/FlightRemote.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/model/FlightRemote.kt new file mode 100644 index 0000000..821e770 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/model/FlightRemote.kt @@ -0,0 +1,35 @@ +package ru.ulstu.`is`.airticketrentservice.api.model + +import kotlinx.serialization.Serializable +import ru.ulstu.`is`.airticketrentservice.database.models.Flight + +@Serializable +data class FlightRemote( + val id: Int = 0, + var direction_from: String = "", + var direction_to: String = "", + var departure_date: String = "", + var arrival_date: String = "", + var tickets_count: Int = 0, + val one_ticket_cost: Double = 0.0 +) + +fun FlightRemote.toFlight(): Flight = Flight( + id, + direction_from, + direction_to, + departure_date, + arrival_date, + tickets_count, + one_ticket_cost +) + +fun Flight.toFlightRemote(): FlightRemote = FlightRemote( + id, + direction_from, + direction_to, + departure_date, + arrival_date, + tickets_count, + one_ticket_cost +) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/api/model/RentRemote.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/model/RentRemote.kt new file mode 100644 index 0000000..b8cf57f --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/model/RentRemote.kt @@ -0,0 +1,26 @@ +package ru.ulstu.`is`.airticketrentservice.api.model + +import kotlinx.serialization.Serializable +import ru.ulstu.`is`.airticketrentservice.database.models.Rent + +@Serializable +data class RentRemote( + val id: Int = 0, + var status: String = "", + val userId: Int = 0, + val ticketId: Int = 0 +) + +fun RentRemote.toRent(): Rent = Rent( + id, + status, + userId, + ticketId +) + +fun Rent.toRentRemote(): RentRemote = RentRemote( + id, + status, + userId, + ticketId +) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/api/model/TicketRemote.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/model/TicketRemote.kt new file mode 100644 index 0000000..a04ace6 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/model/TicketRemote.kt @@ -0,0 +1,26 @@ +package ru.ulstu.`is`.airticketrentservice.api.model + +import kotlinx.serialization.Serializable +import ru.ulstu.`is`.airticketrentservice.database.models.Ticket + +@Serializable +data class TicketRemote( + val id: Int = 0, + var passengers_count: Int = 0, + val ticket_cost: Double = 0.0, + val flightId: Int = 0 +) + +fun TicketRemote.toTicket(): Ticket = Ticket( + id, + passengers_count, + ticket_cost, + flightId +) + +fun Ticket.toTicketRemote(): TicketRemote = TicketRemote( + id, + passengers_count, + ticket_cost, + flightId +) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/api/model/UserRemote.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/model/UserRemote.kt new file mode 100644 index 0000000..5b5c2dd --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/model/UserRemote.kt @@ -0,0 +1,38 @@ +package ru.ulstu.`is`.airticketrentservice.api.model + +import kotlinx.serialization.Serializable +import ru.ulstu.`is`.airticketrentservice.database.models.User + +@Serializable +data class UserRemote( + val id: Int = 0, + var surname: String="", + var name: String="", + var patronymic: String? = "", + var date_of_birth: String = "", + var email: String = "", + var password: String = "", + val role: String = "" +) + +fun UserRemote.toUser(): User = User( + id, + surname, + name, + patronymic, + date_of_birth, + email, + password, + role +) + +fun User.toUserRemote(): UserRemote = UserRemote( + id, + surname, + name, + patronymic, + date_of_birth, + email, + password, + role +) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/api/repository/RestFlightRepository.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/repository/RestFlightRepository.kt new file mode 100644 index 0000000..634cabe --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/repository/RestFlightRepository.kt @@ -0,0 +1,118 @@ +package ru.ulstu.`is`.airticketrentservice.api.repository + +import android.util.Log +import androidx.paging.ExperimentalPagingApi +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.forEach +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.suspendCancellableCoroutine +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import androidx.lifecycle.viewModelScope +import ru.ulstu.`is`.airticketrentservice.api.AppService +import ru.ulstu.`is`.airticketrentservice.api.model.toFlight +import ru.ulstu.`is`.airticketrentservice.api.model.toFlightRemote +import ru.ulstu.`is`.airticketrentservice.database.AppContainer +import ru.ulstu.`is`.airticketrentservice.database.AppDatabase +import ru.ulstu.`is`.airticketrentservice.database.models.Flight +import ru.ulstu.`is`.airticketrentservice.database.repository.OfflineFlightRepository +import ru.ulstu.`is`.airticketrentservice.database.repository.FlightRepository +import ru.ulstu.`is`.airticketrentservice.database.repository.OfflineRemoteKeyRepository +import ru.ulstu.`is`.airticketrentservice.api.mediator.FlightRemoteMediator +import ru.ulstu.`is`.airticketrentservice.api.model.FlightRemote +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class RestFlightRepository( + private val service: AppService, + private val dbFlightRepository: OfflineFlightRepository, + private val dbRemoteKeyRepository: OfflineRemoteKeyRepository, + private val database: AppDatabase +) : FlightRepository { + override fun getFlights(): Flow> { + Log.d(RestFlightRepository::class.simpleName, "Get flights") + + val pagingSourceFactory = { dbFlightRepository.getAllFlightsPagingSource() } + + @OptIn(ExperimentalPagingApi::class) + return Pager( + config = PagingConfig( + pageSize = AppContainer.LIMIT, + enablePlaceholders = false + ), + remoteMediator = FlightRemoteMediator( + service, + dbFlightRepository, + dbRemoteKeyRepository, + database + ), + pagingSourceFactory = pagingSourceFactory + ).flow + } + + override suspend fun getFlightById(flightId: Int): Flight = + service.getFlight(flightId).toFlight() + +// override suspend fun findFlights( +// from: String, +// to: String, +// departureDate: String +// ): List = suspendCancellableCoroutine { continuation -> +// val call = service.findFlights(from, to, departureDate) +// call.enqueue(object: Callback> { +// override fun onResponse(call: Call>, response: Response>) { +// if (response.isSuccessful) { +// val flightRemoteList = response.body() ?: emptyList() +// val flightList = flightRemoteList.map { it.toFlight() } +// continuation.resume(flightList) +// } else { +// continuation.resumeWithException(Exception("Server error: ${response.code()}")) +// } +// } +// +// override fun onFailure(call: Call>, t: Throwable) { +// continuation.resumeWithException(t) +// } +// }) +// +// continuation.invokeOnCancellation { +// call.cancel() +// } +// } + + override suspend fun findFlights( + from: String, + to: String, + departureDate: String + ): List { + Log.d(RestFlightRepository::class.simpleName, "Find flights") + + val existFlights = dbFlightRepository.findFlights(from, to, departureDate).associateBy { it.id }.toMutableMap() + + service.findFlights(from, to, departureDate) + .map { it.toFlight() } + .forEach { flight -> + existFlights[flight.id] = flight + } + + return existFlights.map { it.value }.sortedBy { it.id } + } + + override suspend fun insertFlight(flight: Flight) { + service.createFlight(flight.toFlightRemote()).toFlight() + } + + override suspend fun updateFlight(flight: Flight) { + service.updateFlight(flight.id, flight.toFlightRemote()).toFlight() + } + + override suspend fun deleteFlight(flight: Flight) { + service.deleteFlight(flight.id).toFlight() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/api/repository/RestRentRepository.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/repository/RestRentRepository.kt new file mode 100644 index 0000000..6d0b1ca --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/repository/RestRentRepository.kt @@ -0,0 +1,81 @@ +package ru.ulstu.`is`.airticketrentservice.api.repository + +import android.util.Log +import androidx.paging.ExperimentalPagingApi +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.airticketrentservice.api.AppService +import ru.ulstu.`is`.airticketrentservice.api.mediator.RentRemoteMediator +import ru.ulstu.`is`.airticketrentservice.api.model.toRent +import ru.ulstu.`is`.airticketrentservice.api.model.toRentRemote +import ru.ulstu.`is`.airticketrentservice.database.AppContainer +import ru.ulstu.`is`.airticketrentservice.database.AppDatabase +import ru.ulstu.`is`.airticketrentservice.database.models.Rent +import ru.ulstu.`is`.airticketrentservice.database.repository.OfflineRemoteKeyRepository +import ru.ulstu.`is`.airticketrentservice.database.repository.OfflineRentRepository +import ru.ulstu.`is`.airticketrentservice.database.repository.RentRepository + +class RestRentRepository( + private val service: AppService, + private val dbRentRepository: OfflineRentRepository, + private val dbRemoteKeyRepository: OfflineRemoteKeyRepository, + private val ticketRestRepository: RestTicketRepository, + private val database: AppDatabase +) : RentRepository { + override fun getRents(): Flow> { + Log.d(RestRentRepository::class.simpleName, "Get rents") + + val pagingSourceFactory = { dbRentRepository.getAllRentsPagingSource() } + + @OptIn(ExperimentalPagingApi::class) + return Pager( + config = PagingConfig( + pageSize = AppContainer.LIMIT, + enablePlaceholders = false + ), + remoteMediator = RentRemoteMediator( + service, + dbRentRepository, + dbRemoteKeyRepository, + ticketRestRepository, + database + ), + pagingSourceFactory = pagingSourceFactory + ).flow + } + + override suspend fun getRentById(rentId: Int): Rent = + rentId.let { service.getRent(it).toRent() } + + override suspend fun insertRent(rent: Rent) { + service.createRent(rent.toRentRemote()).toRent() + } + + override suspend fun updateRent(rent: Rent) { + rent.id.let { service.updateRent(it, rent.toRentRemote()).toRent() } + } + + override suspend fun deleteRent(rent: Rent) { + rent.id.let { service.deleteRent(it).toRent() } + } + +// override suspend fun getAllRents(): List { +// val existRents = dbRentRepository.getAllRents().associateBy { it.id }.toMutableMap() +// +// service.getAllRents() +// .map { it.toRent() } +// .forEach { rent -> +// val existRent = existRents[rent.id] +// if (existRent == null) { +// dbRentRepository.insertRent(rent) +// } else if (existRent != rent) { +// dbRentRepository.updateRent(rent) +// } +// existRents[rent.id] = rent +// } +// +// return existRents.map { it.value }.sortedBy { it.id } +// } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/api/repository/RestTicketRepository.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/repository/RestTicketRepository.kt new file mode 100644 index 0000000..8a3bc63 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/repository/RestTicketRepository.kt @@ -0,0 +1,99 @@ +package ru.ulstu.`is`.airticketrentservice.api.repository + +import android.util.Log +import androidx.paging.ExperimentalPagingApi +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.airticketrentservice.api.AppService +import ru.ulstu.`is`.airticketrentservice.api.mediator.TicketRemoteMediator +import ru.ulstu.`is`.airticketrentservice.api.model.toTicket +import ru.ulstu.`is`.airticketrentservice.api.model.toTicketRemote +import ru.ulstu.`is`.airticketrentservice.database.AppContainer +import ru.ulstu.`is`.airticketrentservice.database.AppDatabase +import ru.ulstu.`is`.airticketrentservice.database.models.Ticket +import ru.ulstu.`is`.airticketrentservice.database.repository.OfflineRemoteKeyRepository +import ru.ulstu.`is`.airticketrentservice.database.repository.OfflineTicketRepository +import ru.ulstu.`is`.airticketrentservice.database.repository.TicketRepository + +class RestTicketRepository( + private val service: AppService, + private val dbTicketRepository: OfflineTicketRepository, + private val dbRemoteKeyRepository: OfflineRemoteKeyRepository, + private val flightRestRepository: RestFlightRepository, + private val database: AppDatabase +) : TicketRepository { + override fun getTickets(): Flow> { + Log.d(RestTicketRepository::class.simpleName, "Get tickets") + + val pagingSourceFactory = { dbTicketRepository.getAllTicketsPagingSource() } + + @OptIn(ExperimentalPagingApi::class) + return Pager( + config = PagingConfig( + pageSize = AppContainer.LIMIT, + enablePlaceholders = false + ), + remoteMediator = TicketRemoteMediator( + service, + dbTicketRepository, + dbRemoteKeyRepository, + flightRestRepository, + database, + ), + pagingSourceFactory = pagingSourceFactory + ).flow + } + + override suspend fun getFlightsTickets(flightId: Int): List { + val flightsTickets = dbTicketRepository.getFlightsTickets(flightId).associateBy { it.id }.toMutableMap() + + service.getFlightsTickets(flightId) + .map { it.toTicket() } + .forEach { ticket -> + val flightsTicket = flightsTickets[ticket.id] + if (flightsTicket == null) { + dbTicketRepository.insertTicket(ticket) + } else if (flightsTicket != ticket) { + dbTicketRepository.updateTicket(ticket) + } + flightsTickets[ticket.id] = ticket + } + + return flightsTickets.map { it.value }.sortedBy { it.id } + } + + override suspend fun getTicketById(ticketId: Int): Ticket = + ticketId.let { service.getTicket(it).toTicket() } + + override suspend fun insertTicket(ticket: Ticket) { + service.createTicket(ticket.toTicketRemote()).toTicket() + } + + override suspend fun updateTicket(ticket: Ticket) { + ticket.id.let { service.updateTicket(it, ticket.toTicketRemote()).toTicket() } + } + + override suspend fun deleteTicket(ticket: Ticket) { + ticket.id.let { service.deleteTicket(it).toTicket() } + } + + override suspend fun getAllTickets(): List { + val existTickets = dbTicketRepository.getAllTickets().associateBy { it.id }.toMutableMap() + + service.getAllTickets() + .map { it.toTicket() } + .forEach { ticket -> + val existTicket = existTickets[ticket.id] + if (existTicket == null) { + dbTicketRepository.insertTicket(ticket) + } else if (existTicket != ticket) { + dbTicketRepository.updateTicket(ticket) + } + existTickets[ticket.id] = ticket + } + + return existTickets.map { it.value }.sortedBy { it.id } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/api/repository/RestUserRepository.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/repository/RestUserRepository.kt new file mode 100644 index 0000000..b22dfe4 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/api/repository/RestUserRepository.kt @@ -0,0 +1,81 @@ +package ru.ulstu.`is`.airticketrentservice.api.repository + +import android.util.Log +import ru.ulstu.`is`.airticketrentservice.api.AppService +import ru.ulstu.`is`.airticketrentservice.api.model.toRent +import ru.ulstu.`is`.airticketrentservice.api.model.toUser +import ru.ulstu.`is`.airticketrentservice.api.model.toUserRemote +import ru.ulstu.`is`.airticketrentservice.database.models.Rent +import ru.ulstu.`is`.airticketrentservice.database.models.User +import ru.ulstu.`is`.airticketrentservice.database.repository.OfflineRentRepository +import ru.ulstu.`is`.airticketrentservice.database.repository.UserRepository +import ru.ulstu.`is`.airticketrentservice.database.repository.OfflineUserRepository + +class RestUserRepository( + private val service: AppService, + private val dbUserRepository: OfflineUserRepository, + private val dbRentRepository: OfflineRentRepository +) : UserRepository { + + override suspend fun getUserById(userId: Int): User = + service.getUser(userId).toUser() + + + override suspend fun insertUser(user: User) { + Log.d(RestUserRepository::class.simpleName, "Add user") + service.createUser(user.toUserRemote()).toUser() + } + + override suspend fun updateUser(user: User) { + Log.d(RestUserRepository::class.simpleName, "Update user") + service.updateUser(user.id, user.toUserRemote()).toUser() + } + + override suspend fun deleteUser(user: User) { + Log.d(RestUserRepository::class.simpleName, "Delete user") + service.deleteUser(user.id).toUser() + } + + override suspend fun getAllUsers(): List { + Log.d(RestUserRepository::class.simpleName, "Get users") + + val existUsers = dbUserRepository.getAllUsers().associateBy { it.id }.toMutableMap() + + service.getAllUsers() + .map { it.toUser() } + .forEach { user -> + val existUser = existUsers[user.id] + if (existUser == null) { + dbUserRepository.insertUser(user) + } else if (existUser != user) { + dbUserRepository.updateUser(user) + } + existUsers[user.id] = user + } + + return existUsers.map { it.value }.sortedBy { it.id } + } + + override suspend fun getUserRents(id: Int): List { + Log.d(RestUserRepository::class.simpleName, "Get users $id rents") + val existRents = dbUserRepository.getUserRents(id).associateBy { it.id }.toMutableMap() + + service.getUserRents(id) + .map { it.toRent() } + .forEach { rent -> + Log.d(RestUserRepository::class.simpleName, "айди брони: ${rent.id}, и пользователя: ${rent.userId}") + if(rent.userId == id) { + val existRent = existRents[rent.id] + if (existRent == null) { + Log.d(RestUserRepository::class.simpleName, "бронирования нет в бд") + dbRentRepository.insertRent(rent) + } else if (existRent != rent) { + Log.d(RestUserRepository::class.simpleName, "бронирование есть в бд") + dbRentRepository.updateRent(rent) + } + existRents[rent.id] = rent + } + } + return existRents.map { it.value }.sortedBy { it.id } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/AppContainer.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/AppContainer.kt new file mode 100644 index 0000000..add2b14 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/AppContainer.kt @@ -0,0 +1,75 @@ +package ru.ulstu.`is`.airticketrentservice.database + +import android.content.Context +import ru.ulstu.`is`.airticketrentservice.api.AppService +import ru.ulstu.`is`.airticketrentservice.api.repository.RestFlightRepository +import ru.ulstu.`is`.airticketrentservice.api.repository.RestRentRepository +import ru.ulstu.`is`.airticketrentservice.api.repository.RestTicketRepository +import ru.ulstu.`is`.airticketrentservice.api.repository.RestUserRepository +import ru.ulstu.`is`.airticketrentservice.database.repository.OfflineFlightRepository +import ru.ulstu.`is`.airticketrentservice.database.repository.OfflineRemoteKeyRepository +import ru.ulstu.`is`.airticketrentservice.database.repository.OfflineRentRepository +import ru.ulstu.`is`.airticketrentservice.database.repository.OfflineTicketRepository +import ru.ulstu.`is`.airticketrentservice.database.repository.OfflineUserRepository + +interface AppContainer { + val userRestRepository: RestUserRepository + val ticketRestRepository: RestTicketRepository + val rentRestRepository: RestRentRepository + val flightRestRepository: RestFlightRepository + companion object { + const val TIMEOUT = 5000L + const val LIMIT = 10 + } +} + +class AppDataContainer(private val context: Context) : AppContainer { + private val userRepository: OfflineUserRepository by lazy { + OfflineUserRepository(AppDatabase.getInstance(context).userDao()) + } + private val flightRepository: OfflineFlightRepository by lazy { + OfflineFlightRepository(AppDatabase.getInstance(context).flightDao()) + } + private val ticketRepository: OfflineTicketRepository by lazy { + OfflineTicketRepository(AppDatabase.getInstance(context).ticketDao()) + } + private val rentRepository: OfflineRentRepository by lazy { + OfflineRentRepository(AppDatabase.getInstance(context).rentDao()) + } + private val remoteKeyRepository: OfflineRemoteKeyRepository by lazy { + OfflineRemoteKeyRepository(AppDatabase.getInstance(context).remoteKeysDao()) + } + override val userRestRepository: RestUserRepository by lazy { + RestUserRepository( + AppService.getInstance(), + userRepository, + rentRepository + ) + } + override val flightRestRepository: RestFlightRepository by lazy { + RestFlightRepository( + AppService.getInstance(), + flightRepository, + remoteKeyRepository, + AppDatabase.getInstance(context) + ) + } + override val ticketRestRepository: RestTicketRepository by lazy { + RestTicketRepository( + AppService.getInstance(), + ticketRepository, + remoteKeyRepository, + flightRestRepository, + AppDatabase.getInstance(context) + ) + } + override val rentRestRepository: RestRentRepository by lazy { + RestRentRepository( + AppService.getInstance(), + rentRepository, + remoteKeyRepository, + ticketRestRepository, + AppDatabase.getInstance(context) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/AppDatabase.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/AppDatabase.kt new file mode 100644 index 0000000..81e6f7c --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/AppDatabase.kt @@ -0,0 +1,102 @@ +package ru.ulstu.`is`.airticketrentservice.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import ru.ulstu.`is`.airticketrentservice.database.dao.UserDao +import ru.ulstu.`is`.airticketrentservice.database.dao.RentDao +import ru.ulstu.`is`.airticketrentservice.database.dao.TicketDao +import ru.ulstu.`is`.airticketrentservice.database.dao.RemoteKeysDao +import ru.ulstu.`is`.airticketrentservice.database.dao.FlightDao +import ru.ulstu.`is`.airticketrentservice.database.models.Flight +import ru.ulstu.`is`.airticketrentservice.database.models.RemoteKeys +import ru.ulstu.`is`.airticketrentservice.database.models.Rent +import ru.ulstu.`is`.airticketrentservice.database.models.Ticket +import ru.ulstu.`is`.airticketrentservice.database.models.User + +@Database(entities = [RemoteKeys::class, User::class, Flight::class, Ticket::class, Rent::class], version = 1, exportSchema = false) +abstract class AppDatabase : RoomDatabase() { + abstract fun userDao(): UserDao + abstract fun rentDao(): RentDao + abstract fun ticketDao(): TicketDao + abstract fun flightDao(): FlightDao + abstract fun remoteKeysDao(): RemoteKeysDao + + companion object { + private const val DB_NAME: String = "ticketservicedatabase" + + @Volatile + private var INSTANCE: AppDatabase? = null + private suspend fun populateDatabase() { + INSTANCE?.let { database -> + val userDao = database.userDao() + val user1 = User(1, "Пупкин","Василий","Иванович","2003-10-17", "user1@mail.ru", "user1", "user") + val user2 = User(2, "Иванов","Иван","Петрович","2005-05-17", "user2@mail.ru", "user2", "user") + val user3 = User(3, "Сидорова","Екатерина","Денисовна","2004-10-17", "user3@mail.ru", "user3", "user") + userDao.insert(user1) + userDao.insert(user2) + userDao.insert(user3) + val rentDao = database.rentDao() + val rent1 = Rent(id=1, "Подтверждено",userId = 2, ticketId = 4) + val rent2 = Rent(id=2, "Подтверждено",userId = 1, ticketId = 2) + val rent3 = Rent(id=3, "Ожидает подтверждения",userId = 3, ticketId = 5) + val rent4 = Rent(id=4, "Подтверждено",userId = 2, ticketId = 4) + val rent5 = Rent(id=5, "Подтверждено",userId = 1, ticketId = 2) + val rent6 = Rent(id=6, "Ожидает подтверждения",userId = 3, ticketId = 5) + rentDao.insert(rent1) + rentDao.insert(rent2) + rentDao.insert(rent3) + rentDao.insert(rent4) + rentDao.insert(rent5) + rentDao.insert(rent6) + val flightDao = database.flightDao() + val flight1 = Flight(1, "Ульяновск", "Москва","17-12-2023 16:00", "17-12-2023 19:00",50, 2539.4) + val flight2 = Flight(2, "Ульяновск", "Париж","18-12-2023 12:25", "19-12-2023 17:15",200, 8362.2) + val flight3 = Flight(3, "Ульяновск", "Сочи","24-12-2023 17:28", "24-12-2023 19:23", 100, 1934.5) + val flight4 = Flight(4, "Ульяновск", "Нижний Новгород","24-12-2023 08:10", "24-12-2023 10:00", 100, 1934.5) + val flight5 = Flight(5, "Ульяновск", "Самара","27-12-2023 13:00", "27-12-2023 14:00", 100, 1934.5) + val flight6 = Flight(6, "Ульяновск", "Крым","24-11-2023 17:25", "24-11-2023 19:50", 100, 1934.5) + flightDao.insert(flight1) + flightDao.insert(flight2) + flightDao.insert(flight3) + flightDao.insert(flight4) + flightDao.insert(flight5) + flightDao.insert(flight6) + val ticketDao = database.ticketDao() + val ticket1 = Ticket(1, 1,2990.2, flightId = 1) + val ticket2 = Ticket(2, 3,8243.9,flightId = 2) + val ticket3 = Ticket(3, 2, 5034.34, flightId = 3) + val ticket4 = Ticket(4, 5, 17283.2, flightId = 4) + val ticket5 = Ticket(5, 1, 2119.2, flightId = 1) + val ticket6 = Ticket(6, 4, 23898.3, flightId = 4) + ticketDao.insert(ticket1) + ticketDao.insert(ticket2) + ticketDao.insert(ticket3) + ticketDao.insert(ticket4) + ticketDao.insert(ticket5) + ticketDao.insert(ticket6) + } + } + + fun getInstance(appContext: Context): AppDatabase { + return INSTANCE ?: synchronized(this) { + Room.databaseBuilder( + appContext, + AppDatabase::class.java, + DB_NAME + ) + /*.addCallback(object : Callback() { + override fun onCreate(db: SupportSQLiteDatabase) { + super.onCreate(db) + CoroutineScope(Dispatchers.IO).launch { + populateDatabase() + } + } + })*/ + .build() + .also { INSTANCE = it } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/FlightDao.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/FlightDao.kt new file mode 100644 index 0000000..2a72edc --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/FlightDao.kt @@ -0,0 +1,37 @@ +package ru.ulstu.`is`.airticketrentservice.database.dao + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.airticketrentservice.database.models.Flight + +@Dao +interface FlightDao { + @Query("select * from flights") + fun getAll(): List + + @Query("select * from flights where flights.id = :flightId") + fun getFlightById(flightId: Int): Flow + + @Query("select * from flights where flights.direction_from like :from or flights.direction_to like :to or flights.departure_date like :departureDate") + suspend fun findFlights(from: String, to: String, departureDate: String): List + + @Query("SELECT * FROM flights ORDER BY id ASC") + fun getFlights(): PagingSource + + @Insert + suspend fun insert(vararg flight: Flight) + + @Update + suspend fun update(flight: Flight) + + @Delete + suspend fun delete(flight: Flight) + + @Query("DELETE FROM flights") + suspend fun deleteAll() +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/RemoteKeysDao.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/RemoteKeysDao.kt new file mode 100644 index 0000000..3476e43 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/RemoteKeysDao.kt @@ -0,0 +1,20 @@ +package ru.ulstu.`is`.airticketrentservice.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import ru.ulstu.`is`.airticketrentservice.database.models.RemoteKeyType +import ru.ulstu.`is`.airticketrentservice.database.models.RemoteKeys + +@Dao +interface RemoteKeysDao { + @Query("SELECT * FROM remote_keys WHERE entityId = :entityId AND type = :type") + suspend fun getRemoteKeys(entityId: Int, type: RemoteKeyType): RemoteKeys? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(remoteKey: List) + + @Query("DELETE FROM remote_keys WHERE type = :type") + suspend fun clearRemoteKeys(type: RemoteKeyType) +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/RentDao.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/RentDao.kt new file mode 100644 index 0000000..b916cbb --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/RentDao.kt @@ -0,0 +1,34 @@ +package ru.ulstu.`is`.airticketrentservice.database.dao + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.airticketrentservice.database.models.Rent + +@Dao +interface RentDao { +// @Query("select * from rents") +// fun getAll(): List + + @Query("select * from rents where rents.id = :rentId") + fun getRentById(rentId: Int): Flow + + @Query("SELECT * FROM rents ORDER BY id ASC") + fun getRents(): PagingSource + + @Insert + suspend fun insert(vararg rent: Rent) + + @Update + suspend fun update(rent: Rent) + + @Delete + suspend fun delete(rent: Rent) + + @Query("DELETE FROM rents") + suspend fun deleteAll() +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/TicketDao.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/TicketDao.kt new file mode 100644 index 0000000..ee5ada3 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/TicketDao.kt @@ -0,0 +1,39 @@ +package ru.ulstu.`is`.airticketrentservice.database.dao + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.airticketrentservice.database.models.Flight +import ru.ulstu.`is`.airticketrentservice.database.models.Rent +import ru.ulstu.`is`.airticketrentservice.database.models.Ticket + +@Dao +interface TicketDao { + @Query("select * from tickets") + fun getAll(): List + + @Query("SELECT * FROM tickets ORDER BY id ASC") + fun getTickets(): PagingSource + + @Query("select * from tickets where tickets.id = :ticketId") + fun getTicketById(ticketId: Int): Flow + + @Query("select * from tickets where tickets.flight_id = :flightId") + fun getFlightsTickets(flightId: Int): List + + @Insert + suspend fun insert(vararg ticket: Ticket) + + @Update + suspend fun update(ticket: Ticket) + + @Delete + suspend fun delete(ticket: Ticket) + + @Query("DELETE FROM tickets") + suspend fun deleteAll() +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/UserDao.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/UserDao.kt new file mode 100644 index 0000000..5397377 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/dao/UserDao.kt @@ -0,0 +1,32 @@ +package ru.ulstu.`is`.airticketrentservice.database.dao + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.airticketrentservice.database.models.User +import ru.ulstu.`is`.airticketrentservice.database.models.Rent + +@Dao +interface UserDao { + @Query("select * from users") + suspend fun getAll(): List + + @Query("select * from users where users.id = :userId") + fun getUserById(userId: Int?): Flow + + @Query("select * from rents WHERE rents.user_id = :userId") + suspend fun getUserRents(userId: Int): List + + @Insert + suspend fun insert(user: User) + + @Update + suspend fun update(user: User) + + @Delete + suspend fun delete(user: User) +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/Flight.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/Flight.kt new file mode 100644 index 0000000..2c3a57a --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/Flight.kt @@ -0,0 +1,28 @@ +package ru.ulstu.`is`.airticketrentservice.database.models + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "flights") +data class Flight( + @PrimaryKey(autoGenerate = true) + var id: Int = 0, + var direction_from: String, + var direction_to: String, + var departure_date: String, + var arrival_date: String, + var tickets_count: Int, + val one_ticket_cost: Double +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as Flight + if (id != other.id) return false + return true + } + + override fun hashCode(): Int { + return id + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/RemoteKeys.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/RemoteKeys.kt new file mode 100644 index 0000000..c20ed39 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/RemoteKeys.kt @@ -0,0 +1,27 @@ +package ru.ulstu.`is`.airticketrentservice.database.models + +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverter +import androidx.room.TypeConverters + +enum class RemoteKeyType(private val type: String) { + FLIGHT(Flight::class.simpleName ?: "Flight"), + TICKET(Ticket::class.simpleName ?: "Ticket"), + RENT(Rent::class.simpleName ?: "Rent"); + + @TypeConverter + fun toRemoteKeyType(value: String) = RemoteKeyType.values().first { it.type == value } + + @TypeConverter + fun fromRemoteKeyType(value: RemoteKeyType) = value.type +} + +@Entity(tableName = "remote_keys") +data class RemoteKeys( + @PrimaryKey val entityId: Int, + @TypeConverters(RemoteKeyType::class) + val type: RemoteKeyType, + val prevKey: Int?, + val nextKey: Int? +) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/Rent.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/Rent.kt new file mode 100644 index 0000000..21c8e78 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/Rent.kt @@ -0,0 +1,44 @@ +package ru.ulstu.`is`.airticketrentservice.database.models + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey + +@Entity(tableName = "rents", + foreignKeys = [ + ForeignKey( + entity = User::class, + parentColumns = ["id"], + childColumns = ["user_id"], + onDelete = ForeignKey.RESTRICT, + onUpdate = ForeignKey.RESTRICT), + ForeignKey( + entity = Ticket::class, + parentColumns = ["id"], + childColumns = ["ticket_id"], + onDelete = ForeignKey.RESTRICT, + onUpdate = ForeignKey.RESTRICT) + ]) +data class Rent( + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + var status: String, + @ColumnInfo(name = "user_id") + val userId: Int, + @ColumnInfo(name = "ticket_id") + val ticketId: Int +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as Rent + if (id != other.id) return false + return true + } + + override fun hashCode(): Int { + return id + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/RoleEnum.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/RoleEnum.kt new file mode 100644 index 0000000..f715ca1 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/RoleEnum.kt @@ -0,0 +1,6 @@ +package ru.ulstu.`is`.airticketrentservice.database.models + +enum class RoleEnum { + Admin, + User +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/Ticket.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/Ticket.kt new file mode 100644 index 0000000..a0b4fd3 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/Ticket.kt @@ -0,0 +1,36 @@ +package ru.ulstu.`is`.airticketrentservice.database.models + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey + +@Entity(tableName = "tickets", + foreignKeys = [ + ForeignKey( + entity = Flight::class, + parentColumns = ["id"], + childColumns = ["flight_id"], + onDelete = ForeignKey.CASCADE, + onUpdate = ForeignKey.CASCADE)] +) +data class Ticket( + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + var passengers_count: Int, + val ticket_cost: Double, + @ColumnInfo(name = "flight_id") + val flightId: Int +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as Ticket + if (id != other.id) return false + return true + } + + override fun hashCode(): Int { + return id + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/User.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/User.kt new file mode 100644 index 0000000..f7b1559 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/models/User.kt @@ -0,0 +1,34 @@ +package ru.ulstu.`is`.airticketrentservice.database.models + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity(tableName = "users", + indices = [Index(value = ["email"], unique = true) + ]) +data class User( + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + var surname: String, + @ColumnInfo(name = "user_name") + var name: String, + var patronymic: String?, + var date_of_birth: String, + var email: String, + var password: String, + val role: String +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as User + if (id != other.id) return false + return true + } + + override fun hashCode(): Int { + return id + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/FlightRepository.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/FlightRepository.kt new file mode 100644 index 0000000..91f673e --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/FlightRepository.kt @@ -0,0 +1,14 @@ +package ru.ulstu.`is`.airticketrentservice.database.repository + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.airticketrentservice.database.models.Flight + +interface FlightRepository { + suspend fun insertFlight(flight: Flight) + suspend fun updateFlight(flight: Flight) + suspend fun deleteFlight(flight: Flight) + suspend fun getFlightById(flightId: Int): Flight + suspend fun findFlights(from: String, to: String, departureDate: String): List + fun getFlights(): Flow> +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineFlightRepository.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineFlightRepository.kt new file mode 100644 index 0000000..291c307 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineFlightRepository.kt @@ -0,0 +1,31 @@ +package ru.ulstu.`is`.airticketrentservice.database.repository + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingSource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import ru.ulstu.`is`.airticketrentservice.database.dao.FlightDao +import ru.ulstu.`is`.airticketrentservice.database.models.Flight +import ru.ulstu.`is`.airticketrentservice.database.models.Rent + +class OfflineFlightRepository(private val flightDao: FlightDao) : FlightRepository{ + override suspend fun insertFlight(flight: Flight) = flightDao.insert(flight) + override suspend fun updateFlight(flight: Flight) = flightDao.update(flight) + override suspend fun deleteFlight(flight: Flight) = flightDao.delete(flight) + override suspend fun getFlightById(flightId: Int): Flight = flightDao.getFlightById(flightId).first() + override fun getFlights(): Flow> = Pager( + config = PagingConfig( + pageSize = 4, + enablePlaceholders = false + ), + pagingSourceFactory = flightDao::getFlights + ).flow + + fun getAllFlightsPagingSource(): PagingSource = flightDao.getFlights() + override suspend fun findFlights(from: String, to: String, departureDate: String): List = flightDao.findFlights(from, to, departureDate) + suspend fun clearFlights() = flightDao.deleteAll() + suspend fun insertFlights(flights: List) = + flightDao.insert(*flights.toTypedArray()) +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineRemoteKeyRepository.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineRemoteKeyRepository.kt new file mode 100644 index 0000000..2d92cec --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineRemoteKeyRepository.kt @@ -0,0 +1,16 @@ +package ru.ulstu.`is`.airticketrentservice.database.repository + +import ru.ulstu.`is`.airticketrentservice.database.dao.RemoteKeysDao +import ru.ulstu.`is`.airticketrentservice.database.models.RemoteKeyType +import ru.ulstu.`is`.airticketrentservice.database.models.RemoteKeys + +class OfflineRemoteKeyRepository(private val remoteKeysDao: RemoteKeysDao) : RemoteKeysRepository { + override suspend fun getAllRemoteKeys(id: Int, type: RemoteKeyType) = + remoteKeysDao.getRemoteKeys(id, type) + + override suspend fun createRemoteKeys(remoteKeys: List) = + remoteKeysDao.insertAll(remoteKeys) + + override suspend fun deleteRemoteKey(type: RemoteKeyType) = + remoteKeysDao.clearRemoteKeys(type) +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineRentRepository.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineRentRepository.kt new file mode 100644 index 0000000..457053f --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineRentRepository.kt @@ -0,0 +1,30 @@ +package ru.ulstu.`is`.airticketrentservice.database.repository + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingSource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import ru.ulstu.`is`.airticketrentservice.database.dao.RentDao +import ru.ulstu.`is`.airticketrentservice.database.models.Rent +import ru.ulstu.`is`.airticketrentservice.database.models.Ticket + +class OfflineRentRepository(private val rentDao: RentDao) : RentRepository{ + override suspend fun insertRent(rent: Rent) = rentDao.insert(rent) + override suspend fun updateRent(rent: Rent) = rentDao.update(rent) + override suspend fun deleteRent(rent: Rent) = rentDao.delete(rent) +// override suspend fun getAllRents(): List = rentDao.getAll() + override suspend fun getRentById(rentId: Int): Rent = rentDao.getRentById(rentId).first() + override fun getRents(): Flow> = Pager( + config = PagingConfig( + pageSize = 4, + enablePlaceholders = false + ), + pagingSourceFactory = rentDao::getRents + ).flow + fun getAllRentsPagingSource(): PagingSource = rentDao.getRents() + suspend fun clearRents() = rentDao.deleteAll() + suspend fun insertRents(rents: List) = + rentDao.insert(*rents.toTypedArray()) +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineTicketRepository.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineTicketRepository.kt new file mode 100644 index 0000000..d286180 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineTicketRepository.kt @@ -0,0 +1,31 @@ +package ru.ulstu.`is`.airticketrentservice.database.repository + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingSource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import ru.ulstu.`is`.airticketrentservice.database.dao.TicketDao +import ru.ulstu.`is`.airticketrentservice.database.models.Flight +import ru.ulstu.`is`.airticketrentservice.database.models.Ticket + +class OfflineTicketRepository(private val ticketDao: TicketDao) : TicketRepository { + override suspend fun insertTicket(ticket: Ticket) = ticketDao.insert(ticket) + override suspend fun updateTicket(ticket: Ticket) = ticketDao.update(ticket) + override suspend fun deleteTicket(ticket: Ticket) = ticketDao.delete(ticket) + override suspend fun getAllTickets(): List = ticketDao.getAll() + override suspend fun getTicketById(ticketId: Int): Ticket = ticketDao.getTicketById(ticketId).first() + override fun getTickets(): Flow> = Pager( + config = PagingConfig( + pageSize = 4, + enablePlaceholders = false + ), + pagingSourceFactory = ticketDao::getTickets + ).flow + fun getAllTicketsPagingSource(): PagingSource = ticketDao.getTickets() + override suspend fun getFlightsTickets(flightId: Int): List = ticketDao.getFlightsTickets(flightId) + suspend fun clearTickets() = ticketDao.deleteAll() + suspend fun insertTickets(tickets: List) = + ticketDao.insert(*tickets.toTypedArray()) +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineUserRepository.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineUserRepository.kt new file mode 100644 index 0000000..ed04a68 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/OfflineUserRepository.kt @@ -0,0 +1,15 @@ +package ru.ulstu.`is`.airticketrentservice.database.repository + +import kotlinx.coroutines.flow.first +import ru.ulstu.`is`.airticketrentservice.database.dao.UserDao +import ru.ulstu.`is`.airticketrentservice.database.models.Rent +import ru.ulstu.`is`.airticketrentservice.database.models.User + +class OfflineUserRepository(private val userDao: UserDao) : UserRepository { + override suspend fun insertUser(user: User) = userDao.insert(user) + override suspend fun updateUser(user: User) = userDao.update(user) + override suspend fun deleteUser(user: User) = userDao.delete(user) + override suspend fun getAllUsers(): List = userDao.getAll() + override suspend fun getUserById(userId: Int): User = userDao.getUserById(userId).first() + override suspend fun getUserRents(id: Int): List = userDao.getUserRents(id) +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/RemoteKeysRepository.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/RemoteKeysRepository.kt new file mode 100644 index 0000000..3e3479e --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/RemoteKeysRepository.kt @@ -0,0 +1,10 @@ +package ru.ulstu.`is`.airticketrentservice.database.repository + +import ru.ulstu.`is`.airticketrentservice.database.models.RemoteKeyType +import ru.ulstu.`is`.airticketrentservice.database.models.RemoteKeys + +interface RemoteKeysRepository { + suspend fun getAllRemoteKeys(id: Int, type: RemoteKeyType): RemoteKeys? + suspend fun createRemoteKeys(remoteKeys: List) + suspend fun deleteRemoteKey(type: RemoteKeyType) +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/RentRepository.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/RentRepository.kt new file mode 100644 index 0000000..5969f77 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/RentRepository.kt @@ -0,0 +1,15 @@ +package ru.ulstu.`is`.airticketrentservice.database.repository + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.airticketrentservice.database.models.Rent + + +interface RentRepository { + suspend fun insertRent(rent: Rent) + suspend fun updateRent(rent: Rent) + suspend fun deleteRent(rent: Rent) +// suspend fun getAllRents(): List + suspend fun getRentById(rentId: Int): Rent + fun getRents(): Flow> +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/TicketRepository.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/TicketRepository.kt new file mode 100644 index 0000000..cdac5e4 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/TicketRepository.kt @@ -0,0 +1,15 @@ +package ru.ulstu.`is`.airticketrentservice.database.repository + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.airticketrentservice.database.models.Ticket + +interface TicketRepository { + suspend fun insertTicket(ticket: Ticket) + suspend fun updateTicket(ticket: Ticket) + suspend fun deleteTicket(ticket: Ticket) + suspend fun getAllTickets(): List + suspend fun getTicketById(ticketId: Int): Ticket + fun getTickets(): Flow> + suspend fun getFlightsTickets(flightId: Int): List +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/UserRepository.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/UserRepository.kt new file mode 100644 index 0000000..827b48b --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/database/repository/UserRepository.kt @@ -0,0 +1,13 @@ +package ru.ulstu.`is`.airticketrentservice.database.repository + +import ru.ulstu.`is`.airticketrentservice.database.models.Rent +import ru.ulstu.`is`.airticketrentservice.database.models.User + +interface UserRepository { + suspend fun insertUser(user: User) + suspend fun updateUser(user: User) + suspend fun deleteUser(user: User) + suspend fun getAllUsers(): List + suspend fun getUserById(userId: Int): User + suspend fun getUserRents(id: Int): List +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/elements/EmailTextInput.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/elements/EmailTextInput.kt new file mode 100644 index 0000000..8daa7d4 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/elements/EmailTextInput.kt @@ -0,0 +1,59 @@ +package ru.ulstu.`is`.airticketrentservice.elements + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EmailTextField(email: String, onEmailChange: (String) -> Unit) { + TextField( + value = email, + onValueChange = { + onEmailChange(it) + }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + modifier = Modifier.fillMaxWidth(), + label = { Text("Электронная почта") }, + placeholder = { Text("Электронная почта") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), + singleLine = true, + ) +} + +fun isValidEmail(email: String): Boolean { + val emailRegex = "[a-zA-Z0-9._-]+@[a-z]+\\.+[a-z]+".toRegex() + return email.matches(emailRegex) +} + +@Composable +fun ValidateEmail(email: String, onEmailChange: (String) -> Unit) { + + Column (modifier = Modifier.padding(16.dp)) { + EmailTextField(email = email, onEmailChange = { onEmailChange(it)}) + + if (email.isNotEmpty()) { + if (isValidEmail(email)) { + Text(text = "Корректно", color = Color(0xFF5D925E)) + } else { + Text(text = "Не корректно", color = Color(0xFF94474D)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/graphs/AuthNavGraph.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/graphs/AuthNavGraph.kt new file mode 100644 index 0000000..3a26e97 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/graphs/AuthNavGraph.kt @@ -0,0 +1,32 @@ +package ru.ulstu.`is`.airticketrentservice.graphs + +import android.window.SplashScreen +import androidx.compose.ui.Modifier +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.compose.composable +import androidx.navigation.navigation +import ru.ulstu.`is`.airticketrentservice.screen.auth.Login +import ru.ulstu.`is`.airticketrentservice.screen.auth.Registration +import ru.ulstu.`is`.airticketrentservice.viewModel.CurrentUserViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.LoginViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.RegistrationViewModel + +fun NavGraphBuilder.authNavGraph(navController: NavHostController, registrationViewModel: RegistrationViewModel, loginViewModel: LoginViewModel, currentUserViewModel: CurrentUserViewModel){ + navigation( + route=Graph.AUTHENTICATION, + startDestination = AuthScreen.Login.route + ){ + composable(route=AuthScreen.Login.route){ + Login(navController = navController, Modifier, loginViewModel, currentUserViewModel) + } + composable(route=AuthScreen.Registration.route){ + Registration(navController = navController, Modifier, registrationViewModel, currentUserViewModel) + } + } +} + +sealed class AuthScreen(val route: String){ + object Login: AuthScreen(route = "login") + object Registration: AuthScreen(route="registration") +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/graphs/HomeNavGraph.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/graphs/HomeNavGraph.kt new file mode 100644 index 0000000..bed34f2 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/graphs/HomeNavGraph.kt @@ -0,0 +1,135 @@ +package ru.ulstu.`is`.airticketrentservice.graphs + +import androidx.compose.runtime.Composable +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.compose.NavHost +import androidx.navigation.navArgument +import ru.ulstu.`is`.airticketrentservice.navigation.BottomBarScreen +import ru.ulstu.`is`.airticketrentservice.screen.Admin +import ru.ulstu.`is`.airticketrentservice.screen.FlightEdit +import ru.ulstu.`is`.airticketrentservice.screen.FlightList +import ru.ulstu.`is`.airticketrentservice.screen.FoundFlights +import ru.ulstu.`is`.airticketrentservice.screen.MainPage +import ru.ulstu.`is`.airticketrentservice.screen.MyRents +import ru.ulstu.`is`.airticketrentservice.screen.Profile +import ru.ulstu.`is`.airticketrentservice.screen.RentEdit +import ru.ulstu.`is`.airticketrentservice.screen.RentList +import ru.ulstu.`is`.airticketrentservice.screen.TicketEdit +import ru.ulstu.`is`.airticketrentservice.screen.UserEdit +import ru.ulstu.`is`.airticketrentservice.screen.UserList +import ru.ulstu.`is`.airticketrentservice.viewModel.AppViewModelProvider +import ru.ulstu.`is`.airticketrentservice.viewModel.CurrentUserViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.FindFlightsViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.FlightListViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.RentListViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.TicketListViewModel +//import ru.ulstu.`is`.airticketrentservice.viewModel.TicketEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.UserListViewModel + +@Composable +fun HomeNavGraph( + navController: NavHostController, + currentUserViewModel: CurrentUserViewModel = viewModel(factory = AppViewModelProvider.Factory), + flightListViewModel: FlightListViewModel = viewModel(factory = AppViewModelProvider.Factory), + userListViewModel: UserListViewModel = viewModel(factory = AppViewModelProvider.Factory), + ticketListViewModel: TicketListViewModel = viewModel(factory = AppViewModelProvider.Factory), + rentListViewModel: RentListViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + NavHost( + navController = navController, + route = Graph.MAIN, + startDestination = BottomBarScreen.MainPage.route + ){ + composable( + route = BottomBarScreen.MainPage.route + ){ + MainPage(navController, flightListViewModel) + } + composable( + route = BottomBarScreen.FlightList.route + ) { + FlightList(navController) + } + composable( + route = BottomBarScreen.Profile.route + ){ + Profile(navController, currentUserViewModel) + } + composable( + route = BottomBarScreen.UserList.route + ){ + UserList(navController) + } + composable( + route = BottomBarScreen.RentList.route + ){ + RentList(navController, rentListViewModel, ticketListViewModel) + } + composable( + route = BottomBarScreen.Admin.route + ){ + Admin(navController) + } + composable( + route = BottomBarScreen.FoundFlights.route, + arguments = listOf( + navArgument("direction_from") { type = NavType.StringType }, + navArgument("direction_to") { type = NavType.StringType }, + navArgument("departure_date") { type = NavType.StringType } + ) + ){ + FoundFlights(navController) + } + composable( + route = BottomBarScreen.UserEdit.route, + arguments = listOf( + navArgument("id") { + type = NavType.IntType + } + ) + ){ + UserEdit(navController) + } + composable( + route = BottomBarScreen.RentEdit.route, + arguments = listOf( + navArgument("id") { + type = NavType.IntType + } + ) + ){ + RentEdit(navController) + } + composable( + route = BottomBarScreen.FlightEdit.route, + arguments = listOf( + navArgument("id") { + type = NavType.IntType + } + ) + ){ + FlightEdit(navController) + } + composable( + route = BottomBarScreen.MyRents.route, + arguments = listOf( + navArgument("userId") { + type = NavType.IntType + } + ) + ) { + MyRents(navController) + } + composable( + route = BottomBarScreen.TicketEdit.route, + arguments = listOf( + navArgument("id") { type = NavType.IntType } + ) + ) { + TicketEdit(navController) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/graphs/RootNavGraph.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/graphs/RootNavGraph.kt new file mode 100644 index 0000000..b359a45 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/graphs/RootNavGraph.kt @@ -0,0 +1,59 @@ +package ru.ulstu.`is`.airticketrentservice.graphs + +import androidx.compose.runtime.Composable +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.compose.composable +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.navArgument +import ru.ulstu.`is`.airticketrentservice.screen.LoadScreen +import ru.ulstu.`is`.airticketrentservice.viewModel.AppViewModelProvider +import ru.ulstu.`is`.airticketrentservice.viewModel.CurrentUserViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.FlightListViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.LoginViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.RegistrationViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.RentListViewModel +//import ru.ulstu.`is`.airticketrentservice.viewModel.TicketEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.UserListViewModel + +const val USERID_ARGUMENT="userId" + +@Composable +fun RootNavigationGraph( + navController: NavHostController, + currentUserViewModel: CurrentUserViewModel = viewModel(factory = AppViewModelProvider.Factory), + registrationViewModel: RegistrationViewModel= viewModel(factory = AppViewModelProvider.Factory), + loginViewModel: LoginViewModel = viewModel(factory = AppViewModelProvider.Factory), + flightListViewModel: FlightListViewModel = viewModel(factory = AppViewModelProvider.Factory), + userListViewModel: UserListViewModel = viewModel(factory = AppViewModelProvider.Factory), + rentListViewModel: RentListViewModel = viewModel(factory = AppViewModelProvider.Factory) +){ + NavHost( + navController=navController, + route = Graph.ROOT, + startDestination = Graph.AUTHENTICATION + ){ + authNavGraph(navController = navController, registrationViewModel, loginViewModel, currentUserViewModel) + composable(route=Graph.MAIN, + arguments = listOf(navArgument(USERID_ARGUMENT){ + type= NavType.StringType + })){ + LoadScreen( + currentUserViewModel = currentUserViewModel, + flightListViewModel = flightListViewModel, + userListViewModel = userListViewModel, + rentListViewModel = rentListViewModel + ) + } + } +} + +object Graph{ + const val ROOT="root_graph" + const val AUTHENTICATION="auth_graph" + const val MAIN="main_graph/{$USERID_ARGUMENT}" + fun passUserId(userId: String): String{ + return "main_graph/$userId" + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/BottomBar.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/BottomBar.kt new file mode 100644 index 0000000..9bac344 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/BottomBar.kt @@ -0,0 +1,113 @@ +package ru.ulstu.`is`.airticketrentservice.navigation + +import android.content.res.Configuration +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.NavDestination +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import ru.ulstu.`is`.airticketrentservice.R +import ru.ulstu.`is`.airticketrentservice.ui.theme.AirTicketRentServiceTheme + +@Composable +fun BottomBar(navController: NavHostController){ + val screens = listOf( + BottomBarScreen.MainPage, + BottomBarScreen.Profile, + BottomBarScreen.Admin + ) + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination=navBackStackEntry?.destination + val bottomBarDestination=screens.any{ it.route==currentDestination?.route } + Row( + modifier = Modifier + .background(Color.Transparent), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + if (bottomBarDestination) { + NavigationBar( + containerColor = colorResource(R.color.lightlightBlue), + modifier = Modifier + .padding(10.dp) + .clip(shape = RoundedCornerShape(20.dp)) + ) { + screens.forEach { screen -> + AddItem( + screen = screen, + currentDestination = currentDestination, + navController = navController + ) + } + } + } + } +} + +@Composable +fun RowScope.AddItem( + screen: BottomBarScreen, + currentDestination: NavDestination?, + navController: NavController +){ + val selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true + + val background = if (selected) Color(0xFFFFFFFF) else Color.Transparent + + + Box( + modifier = Modifier + .height(40.dp) + .clip(CircleShape) + .background(background) + .clickable(onClick = { + navController.navigate(screen.route) { + popUpTo(navController.graph.findStartDestination().id) + launchSingleTop = true + } + }) + ) { + Row( + modifier = Modifier.padding(start = 47.dp, end = 46.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = screen.icon, + contentDescription = "icon", + tint = Color.DarkGray + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/BottomBarScreen.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/BottomBarScreen.kt new file mode 100644 index 0000000..0bedaa5 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/BottomBarScreen.kt @@ -0,0 +1,110 @@ +package ru.ulstu.`is`.airticketrentservice.navigation + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.AdminPanelSettings +import androidx.compose.material.icons.filled.AirplanemodeActive +import androidx.compose.material.icons.filled.FlightTakeoff +import androidx.compose.material.icons.filled.Payments +import androidx.compose.ui.graphics.vector.ImageVector +import ru.ulstu.`is`.airticketrentservice.R + +sealed class BottomBarScreen( + val route: String, + val title: String, + val icon: ImageVector, +){ + object MainPage: BottomBarScreen( + route = "main", + title ="", + icon = Icons.Filled.AirplanemodeActive + ) + object Profile: BottomBarScreen( + route = "profile", + title ="", + icon = Icons.Filled.AccountCircle + ) + object Admin: BottomBarScreen( + route = "admin", + title ="", + icon = Icons.Filled.AdminPanelSettings + ) + object FlightList: BottomBarScreen( + route = "flight-list", + title ="", + icon = Icons.Filled.FlightTakeoff + ) + object UserList: BottomBarScreen( + route = "user-list", + title ="", + icon = Icons.Filled.AccountCircle + ) + object RentList: BottomBarScreen( + route = "rent-list", + title ="", + icon = Icons.Filled.Payments + ) + object MyRents: BottomBarScreen( + route = "my-rents/{userId}", + title ="", + icon = Icons.Filled.Payments + ) { + fun passId(userId: String): String{ + return "my-rents/$userId" + } + } + object FoundFlights: BottomBarScreen( + route = "found-flights/{direction_from}-{direction_to}-{departure_date}", + title ="", + icon = Icons.Filled.FlightTakeoff + ) { + fun passText(direction_from: String, direction_to: String, departure_date: String): String { + return "found-flights/$direction_from-$direction_to-$departure_date" + } + } + object UserEdit: BottomBarScreen( + route = "user-edit/{id}", + title ="", + icon = Icons.Filled.AccountCircle + ) { + fun passId(id: String): String{ + return "user-edit/$id" + } + } + object FlightEdit: BottomBarScreen( + route = "flight-edit/{id}", + title ="", + icon = Icons.Filled.FlightTakeoff + ) { + fun passId(id: String): String{ + return "flight-edit/$id" + } + } + object FlightInfo: BottomBarScreen( + route = "flight-info/{id}", + title ="", + icon = Icons.Filled.AccountCircle + ) { + fun passId(id:String): String{ + return "flight-info/$id" + } + } + object RentEdit: BottomBarScreen( + route = "rent-edit/{id}", + title ="", + icon = Icons.Filled.AccountCircle + ) { + fun passId(id: String): String{ + return "rent-edit/$id" + } + } + object TicketEdit: BottomBarScreen( + route = "ticket-edit/{id}", + title ="", + icon = Icons.Filled.AccountCircle + ) { + fun passId(id:String): String{ + return "ticket-edit/$id" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/MainNavbar.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/MainNavbar.kt new file mode 100644 index 0000000..76ff5fa --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/MainNavbar.kt @@ -0,0 +1,362 @@ +package ru.ulstu.`is`.airticketrentservice.navigation + +import android.annotation.SuppressLint +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavDestination +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import androidx.navigation.navigation +import ru.ulstu.`is`.airticketrentservice.screen.RentList +import ru.ulstu.`is`.airticketrentservice.screen.UserEdit +import ru.ulstu.`is`.airticketrentservice.screen.FlightEdit +import ru.ulstu.`is`.airticketrentservice.R +import ru.ulstu.`is`.airticketrentservice.screen.Admin +import ru.ulstu.`is`.airticketrentservice.screen.FlightInfo +import ru.ulstu.`is`.airticketrentservice.screen.FlightList +import ru.ulstu.`is`.airticketrentservice.screen.FoundFlights +import ru.ulstu.`is`.airticketrentservice.screen.LoadScreen +import ru.ulstu.`is`.airticketrentservice.screen.MainPage +import ru.ulstu.`is`.airticketrentservice.screen.MyRents +import ru.ulstu.`is`.airticketrentservice.screen.Profile +import ru.ulstu.`is`.airticketrentservice.screen.RentEdit +import ru.ulstu.`is`.airticketrentservice.screen.TicketEdit +import ru.ulstu.`is`.airticketrentservice.screen.UserList +import ru.ulstu.`is`.airticketrentservice.screen.auth.Login +import ru.ulstu.`is`.airticketrentservice.screen.auth.Registration +import ru.ulstu.`is`.airticketrentservice.viewModel.AppViewModelProvider +import ru.ulstu.`is`.airticketrentservice.viewModel.CurrentUserViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.FlightEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.FlightListViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.LoginViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.RegistrationViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.RentEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.RentListViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.TicketEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.UserEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.UserListViewModel + + +//@OptIn(ExperimentalMaterial3Api::class) +//@Composable +//fun Topbar( +// navController: NavHostController, +// currentScreen: Screen? +//) { +// TopAppBar( +// colors = TopAppBarDefaults.topAppBarColors( +// containerColor = Color.Transparent, +// ), +// title = {}, +// navigationIcon = { +// if ( +// navController.previousBackStackEntry != null +// && (currentScreen == null || !currentScreen.showInBottomBar) +// ) { +// IconButton(onClick = { navController.navigateUp() }) { +// Icon( +// imageVector = Icons.Filled.ArrowBack, +// contentDescription = null, +// tint = colorResource(R.color.black) +// ) +// } +// } +// }, +// modifier = Modifier +// .padding(10.dp) +// .clip(RoundedCornerShape(20.dp)) +// ) +//} +// +//@Composable +//fun Navbar( +// navController: NavHostController, +// currentDestination: NavDestination? +//) { +// NavigationBar( +// Modifier +// .padding(10.dp) +// .clip(RoundedCornerShape(20.dp))) { +// Screen.bottomBarItems.forEach { screen -> +// NavigationBarItem( +// colors = NavigationBarItemDefaults.colors(colorResource(R.color.lightBlue)), +// icon = { Icon(screen.icon, contentDescription = null) }, +// label = { Text(stringResource(screen.resourceId)) }, +// selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true, +// onClick = { +// navController.navigate(screen.route) { +// popUpTo(navController.graph.findStartDestination().id) { +// saveState = true +// } +// launchSingleTop = true +// restoreState = true +// } +// } +// ) +// } +// } +//} +// +//const val USERID_ARGUMENT="userId" +//@Composable +//fun RootNavGraph( +// navController: NavHostController, +// registrationViewModel: RegistrationViewModel= viewModel(factory = AppViewModelProvider.Factory), +// loginViewModel: LoginViewModel = viewModel(factory = AppViewModelProvider.Factory), +// currentUserViewModel: CurrentUserViewModel = viewModel(factory = AppViewModelProvider.Factory), +//) { +// NavHost( +// navController = navController, +// route = Graph.ROOT, +// startDestination = Graph.AUTHENTICATION +// ) { +// authNavGraph(navController = navController, registrationViewModel, loginViewModel, currentUserViewModel) +// composable(route=Graph.MAIN, +// arguments = listOf(navArgument(USERID_ARGUMENT){ +// type= NavType.StringType +// })){ +// LoadScreen( +// navController = navController, +// currentUserViewModel = currentUserViewModel +// ) +// } +// } +//} +// +//fun NavGraphBuilder.authNavGraph(navController: NavHostController, registrationViewModel: RegistrationViewModel, loginViewModel: LoginViewModel, currentUserViewModel: CurrentUserViewModel){ +// navigation( +// route=Graph.AUTHENTICATION, +// startDestination = AuthScreen.Login.route +// ){ +// composable(route=AuthScreen.Login.route){ +// Login(navController = navController, Modifier, loginViewModel, currentUserViewModel) +// } +// composable(route=AuthScreen.Registration.route){ +// Registration(navController = navController, Modifier, registrationViewModel, currentUserViewModel) +// } +// } +//} +// +//sealed class AuthScreen(val route: String){ +// object Login : AuthScreen(route = "login") +// object Registration : AuthScreen(route="registration") +//} +// +//@Composable +//fun HomeNavGraph( +// navController: NavHostController, +// currentUserViewModel: CurrentUserViewModel +//) { +// NavHost( +// navController = navController, +// startDestination = Screen.MainPage.route +// ) { +// composable(Screen.MainPage.route) { MainPage(navController) } +// composable(Screen.RentList.route) { RentList(navController) } +// composable(Screen.FlightList.route) { FlightList(navController) } +// composable(Screen.Profile.route) { Profile(navController, currentUserViewModel) } +// composable(Screen.UserList.route) { UserList(navController) } +//// composable(Screen.Login.route) { Login(navController) } +// composable(Screen.MyRents.route) { MyRents(navController) } +//// composable(Screen.Registration.route) { Registration(navController) } +// composable(Screen.Admin.route) { Admin(navController) } +// composable( +// Screen.FlightInfo.route, +// arguments = listOf(navArgument("id") { type = NavType.IntType }) +// ) { +// FlightInfo(navController) +// } +// composable( +// Screen.FoundFlights.route, +// arguments = listOf( +// navArgument("from") { type = NavType.StringType }, +// navArgument("to") { type = NavType.StringType }, +// navArgument("departureDate") { type = NavType.StringType } +// ) +// ) { +// FoundFlights(navController) +// } +// composable( +// Screen.UserEdit.route, +// arguments = listOf(navArgument("id") { type = NavType.IntType }) +// ) { +// UserEdit(navController) +// } +// composable( +// Screen.FlightEdit.route, +// arguments = listOf(navArgument("id") { type = NavType.IntType }) +// ) { +// FlightEdit(navController) +// } +// composable( +// Screen.TicketEdit.route, +// arguments = listOf(navArgument("id") { type = NavType.IntType }) +// ) { +// TicketEdit(navController) +// } +//// composable( +//// Screen.MyRents.route, +//// arguments = listOf(navArgument("id") { type = NavType.IntType }) +//// ) { +//// MyRents(navController) +//// } +// composable( +// Screen.RentEdit.route, +// arguments = listOf(navArgument("id") { type = NavType.IntType }) +// ) { +// RentEdit(navController) +// } +// } +//} +// +//object Graph{ +// const val ROOT="root_graph" +// const val AUTHENTICATION="auth_graph" +// const val MAIN="main_graph/{$USERID_ARGUMENT}" +// fun passUserId(userId: String): String{ +// return "main_graph/$userId" +// } +//} +// +////@RequiresApi(Build.VERSION_CODES.O) +////@Composable +////fun Navhost( +//// navController: NavHostController, +//// innerPadding: PaddingValues, modifier: +//// Modifier = Modifier, +//// currentUserViewModel: CurrentUserViewModel = viewModel(factory = AppViewModelProvider.Factory), +//// loginViewModel: LoginViewModel = viewModel(factory = AppViewModelProvider.Factory), +//// registrationViewModel: RegistrationViewModel = viewModel(factory = AppViewModelProvider.Factory), +////) { +//// NavHost( +//// navController, +//// startDestination = Graph.AUTHENTICATION, +//// modifier.padding(innerPadding), +//// route = Graph.ROOT +//// ) { +//// authNavGraph(navController=navController,registrationViewModel,loginViewModel,currentUserViewModel) +//// composable(route=Graph.MAIN, +//// arguments = listOf(navArgument(USERID_ARGUMENT){ +//// type= NavType.StringType +//// }) +////// composable(Screen.RentList.route) { RentList(navController) } +////// composable(Screen.FlightList.route) { FlightList(navController) } +////// composable(Screen.Profile.route) { Profile(navController) } +////// composable(Screen.UserList.route) { UserList(navController) } +////// composable(Screen.Login.route) { Login(navController) } +////// composable(Screen.MyRents.route) { MyRents(navController) } +////// composable(Screen.Registration.route) { Registration(navController) } +////// composable(Screen.MainPage.route) { MainPage(navController) } +////// composable(Screen.Admin.route) { Admin(navController) } +////// composable( +////// Screen.TicketEdit.route, +////// arguments = listOf(navArgument("id") { type = NavType.IntType }) +////// ) { +////// TicketEdit(navController) +////// } +////// composable( +////// Screen.FlightInfo.route, +////// arguments = listOf(navArgument("id") { type = NavType.IntType }) +////// ) { +////// FlightInfo(navController) +////// } +////// composable( +////// Screen.FoundFlights.route, +////// arguments = listOf( +////// navArgument("from") { type = NavType.StringType }, +////// navArgument("to") { type = NavType.StringType }, +////// navArgument("departureDate") { type = NavType.StringType } +////// ) +////// ) { +////// FoundFlights(navController) +////// } +////// composable( +////// Screen.UserEdit.route, +////// arguments = listOf(navArgument("id") { type = NavType.IntType }) +////// ) { +////// UserEdit(navController) +////// } +////// composable( +////// Screen.FlightEdit.route, +////// arguments = listOf(navArgument("id") { type = NavType.IntType }) +////// ) { +////// FlightEdit(navController) +////// } +//////// composable( +//////// Screen.MyRents.route, +//////// arguments = listOf(navArgument("id") { type = NavType.IntType }) +//////// ) { +//////// MyRents(navController) +//////// } +////// composable( +////// Screen.RentEdit.route, +////// arguments = listOf(navArgument("id") { type = NavType.IntType }) +////// ) { +////// RentEdit(navController) +////// } +//// } +////} +// +//@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") +//@RequiresApi(Build.VERSION_CODES.O) +//@Composable +//fun MainNavbar() { +// val navController = rememberNavController() +// val navBackStackEntry by navController.currentBackStackEntryAsState() +// val currentDestination = navBackStackEntry?.destination +// val currentScreen = currentDestination?.route?.let { Screen.getItem(it) } +// val registrationViewModel: RegistrationViewModel= viewModel(factory = AppViewModelProvider.Factory) +// val loginViewModel: LoginViewModel = viewModel(factory = AppViewModelProvider.Factory) +// val currentUserViewModel: CurrentUserViewModel = viewModel(factory = AppViewModelProvider.Factory) +// +// Scaffold( +// topBar = { +// Topbar(navController, currentScreen) +// }, +// bottomBar = { +// if (currentScreen == null || currentScreen.showInBottomBar) { +// Navbar(navController, currentDestination) +// } +// } +// ) { +// RootNavGraph(navController, registrationViewModel, loginViewModel, currentUserViewModel) +// } +//} diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/Screen.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/Screen.kt new file mode 100644 index 0000000..34c6bef --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/Screen.kt @@ -0,0 +1,76 @@ +package ru.ulstu.`is`.airticketrentservice.navigation + +import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.AdminPanelSettings +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Search +import androidx.compose.ui.graphics.vector.ImageVector +import ru.ulstu.`is`.airticketrentservice.R + +//enum class Screen( +// val route: String, +// @StringRes val resourceId: Int, +// val icon: ImageVector = Icons.Filled.Favorite, +// val showInBottomBar: Boolean = true +//) { +// Admin( +// "admin", R.string.rent_view_title, Icons.Filled.AdminPanelSettings +// ), +// FlightEdit( +// "flight-edit/{id}", R.string.ticket_view_title, showInBottomBar = false +// ), +// FlightInfo( +// "flight-info/{id}", R.string.ticket_view_title, showInBottomBar = false +// ), +// FlightList( +// "flight-list", R.string.ticket_main_title, showInBottomBar = false +// ), +// FoundFlights( +// "found-flights/{from}-{to}-{departureDate}", R.string.ticket_main_title, showInBottomBar = false +// ), +// MainPage( +// "main", R.string.rent_view_title, Icons.Filled.Search +// ), +// MyRents( +// "my-rents", R.string.ticket_view_title, showInBottomBar = false +// ), +// Profile( +// "profile", R.string.rent_view_title, Icons.Filled.AccountCircle +// ), +// RentEdit( +// "rent-edit/{id}", R.string.ticket_view_title, showInBottomBar = false +// ), +// RentList( +// "rent-list", R.string.ticket_main_title, showInBottomBar = false +// ), +// TicketEdit( +// "ticket-edit/{id}", R.string.ticket_view_title, showInBottomBar = false +// ), +// UserEdit( +// "user-edit/{id}", R.string.ticket_view_title, showInBottomBar = false +// ), +// UserList( +// "user-list", R.string.ticket_main_title, showInBottomBar = false +// ), +// Login( +// "login", R.string.rent_view_title, showInBottomBar = false +// ), +// Registration( +// "registration", R.string.rent_view_title, showInBottomBar = false +// ); +// +// companion object { +// val bottomBarItems = listOf( +// MainPage, +// Profile, +// Admin +// ) +// +// fun getItem(route: String): Screen? { +// val findRoute = route.split("/").first() +// return values().find { value -> value.route.startsWith(findRoute) } +// } +// } +//} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/TopBar.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/TopBar.kt new file mode 100644 index 0000000..b03cf03 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/navigation/TopBar.kt @@ -0,0 +1,49 @@ +package ru.ulstu.`is`.airticketrentservice.navigation + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import ru.ulstu.`is`.airticketrentservice.R + +//@OptIn(ExperimentalMaterial3Api::class) +//@Composable +//fun TopBar(navController: NavHostController, +// currentScreen: BottomBarScreen?) { +// TopAppBar( +// colors = TopAppBarDefaults.topAppBarColors( +// containerColor = Color.Transparent, +// ), +// title = {}, +// actions = {}, +// navigationIcon = { +// if ( +// navController.previousBackStackEntry != null +// && (currentScreen == null) +// ) { +// IconButton(onClick = { navController.navigateUp() }) { +// Icon( +// imageVector = Icons.Filled.ArrowBack, +// contentDescription = null, +// tint = colorResource(R.color.black) +// ) +// } +// } +// }, +// modifier = Modifier +// .padding(10.dp) +// .clip(RoundedCornerShape(20.dp)) +// ) +//} diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/Admin.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/Admin.kt new file mode 100644 index 0000000..a0dfeda --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/Admin.kt @@ -0,0 +1,83 @@ +package ru.ulstu.`is`.airticketrentservice.screen + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import ru.ulstu.`is`.airticketrentservice.R +import ru.ulstu.`is`.airticketrentservice.navigation.BottomBarScreen + +@Composable +fun Admin ( + navController: NavController, +){ + Column ( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button( + onClick = { navController.navigate(BottomBarScreen.FlightList.route)}, + colors = ButtonDefaults.buttonColors( + containerColor = (colorResource(id = R.color.lightBlue)), + contentColor = Color.White + ), + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 10.dp, + pressedElevation = 6.dp + ), + shape = RoundedCornerShape(15.dp), + modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp), + ) { + Text("Рейсы") + } + Button( + onClick = { navController.navigate(BottomBarScreen.RentList.route)}, + colors = ButtonDefaults.buttonColors( + containerColor = (colorResource(id = R.color.lightBlue)), + contentColor = Color.White + ), + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 10.dp, + pressedElevation = 6.dp + ), + shape = RoundedCornerShape(15.dp), + modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp), + ) { + Text("Бронирования") + } + Button( + onClick = { navController.navigate(BottomBarScreen.UserList.route)}, + colors = ButtonDefaults.buttonColors( + containerColor = (colorResource(id = R.color.lightBlue)), + contentColor = Color.White + ), + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 10.dp, + pressedElevation = 6.dp + ), + shape = RoundedCornerShape(15.dp), + modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp), + ) { + Text("Пользователи") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/FlightEdit.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/FlightEdit.kt new file mode 100644 index 0000000..9ca9391 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/FlightEdit.kt @@ -0,0 +1,253 @@ +package ru.ulstu.`is`.airticketrentservice.screen + +import android.app.DatePickerDialog +import android.widget.DatePicker +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.viewModel.AppViewModelProvider +import ru.ulstu.`is`.airticketrentservice.viewModel.FlightDetails +import ru.ulstu.`is`.airticketrentservice.viewModel.FlightEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.FlightUiState +import ru.ulstu.`is`.airticketrentservice.R +import java.util.Calendar +import java.util.Date + +@Composable +fun FlightEdit( + navController: NavController, + viewModel: FlightEditViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + val coroutineScope = rememberCoroutineScope() + FlightEdit( + flightUiState = viewModel.flightUiState, + onClick = { + coroutineScope.launch { + viewModel.saveFlight() + navController.popBackStack() + } + }, + onUpdate = viewModel::updateUiState) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun FlightEdit( + flightUiState: FlightUiState, + onClick: () -> Unit, + onUpdate: (FlightDetails) -> Unit +) { + Column( + Modifier + .fillMaxWidth() + .padding(all = 10.dp) + .padding(horizontal = 24.dp) + .padding(vertical = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + value = flightUiState.flightDetails.direction_from, + onValueChange = { onUpdate(flightUiState.flightDetails.copy(direction_from = it)) }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { Text(stringResource(id = R.string.ticket_from)) }, + singleLine = true + ) + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + value = flightUiState.flightDetails.direction_to, + onValueChange = { onUpdate(flightUiState.flightDetails.copy(direction_to = it)) }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { Text(stringResource(id = R.string.ticket_to)) }, + singleLine = true + ) + + val mContext = LocalContext.current + val yearFrom: Int + val monthFrom: Int + val dayFrom: Int + + // Initializing a Calendar + val calendarFrom = Calendar.getInstance() + + // Fetching current year, month and day + yearFrom = calendarFrom.get(Calendar.YEAR) + monthFrom = calendarFrom.get(Calendar.MONTH) + dayFrom = calendarFrom.get(Calendar.DAY_OF_MONTH) + + calendarFrom.time = Date() + + // Declaring a string value to + // store date in string format + val dateFrom = remember { mutableStateOf("") } + + // Declaring DatePickerDialog and setting + // initial values as current values (present year, month and day) + val datePickerDialogFrom = DatePickerDialog( + mContext, + { _: DatePicker, year: Int, month: Int, dayOfMonth: Int -> + val selectedDateFrom = "$dayOfMonth-${month + 1}-$year" + dateFrom.value = selectedDateFrom + onUpdate(flightUiState.flightDetails.copy(departure_date = selectedDateFrom)) + }, yearFrom, monthFrom, dayFrom + ) + + Button( + colors = ButtonDefaults.buttonColors( + containerColor = (colorResource(id = R.color.lightBlue)), + contentColor = Color.White + ), + onClick = { + datePickerDialogFrom.show() + }, + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 10.dp, + pressedElevation = 6.dp + ), + shape = RoundedCornerShape(15.dp), + modifier = Modifier + .fillMaxWidth(), + ) { + Text("Выбрать дату вылета") + } + + Text(text = " ${flightUiState.flightDetails.departure_date}", fontSize = 15.sp, color = Color.DarkGray) + + // Declaring integer values + // for year, month and day + val yearTo: Int + val monthTo: Int + val dayTo: Int + + // Initializing a Calendar + val calendarTo = Calendar.getInstance() + + // Fetching current year, month and day + yearTo = calendarTo.get(Calendar.YEAR) + monthTo = calendarTo.get(Calendar.MONTH) + dayTo = calendarTo.get(Calendar.DAY_OF_MONTH) + + calendarTo.time = Date() + + // Declaring a string value to + // store date in string format + val dateTo = remember { mutableStateOf("") } + + // Declaring DatePickerDialog and setting + // initial values as current values (present year, month and day) + val datePickerDialogTo = DatePickerDialog( + mContext, + { _: DatePicker, year: Int, month: Int, dayOfMonth: Int -> + val selectedDateTo = "$dayOfMonth-${month + 1}-$year" + dateTo.value = selectedDateTo + onUpdate(flightUiState.flightDetails.copy(arrival_date = selectedDateTo)) + }, yearTo, monthTo, dayTo + ) + + Button( + colors = ButtonDefaults.buttonColors( + containerColor = (colorResource(id = R.color.lightBlue)), + contentColor = Color.White + ), + onClick = { + datePickerDialogTo.show() + }, + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 10.dp, + pressedElevation = 6.dp + ), + shape = RoundedCornerShape(15.dp), + modifier = Modifier + .fillMaxWidth(), + ) { + Text("Выбрать дату прилёта") + } + + Text(text = flightUiState.flightDetails.arrival_date, fontSize = 15.sp, color = Color.DarkGray) + + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + value = flightUiState.flightDetails.tickets_count.toString(), + onValueChange = { onUpdate(flightUiState.flightDetails.copy(tickets_count = it.toInt())) }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { Text("Количество билетов") }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) + ) + + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + value = flightUiState.flightDetails.one_ticket_cost.toString(), + onValueChange = { onUpdate(flightUiState.flightDetails.copy(one_ticket_cost = it.toDouble())) }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { Text("Стоимость одного билета") }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal) + ) + Button( + onClick = onClick, + enabled = flightUiState.isEntryValid, + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 10.dp, + pressedElevation = 6.dp + ), + shape = RoundedCornerShape(15.dp), + colors = ButtonDefaults.buttonColors(containerColor = colorResource(R.color.lightBlue)), + modifier = Modifier.fillMaxWidth() + ) { + Text(text = stringResource(R.string.ticket_save_button)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/FlightInfo.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/FlightInfo.kt new file mode 100644 index 0000000..d63788f --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/FlightInfo.kt @@ -0,0 +1,148 @@ +package ru.ulstu.`is`.airticketrentservice.screen + +import android.app.DatePickerDialog +import android.widget.DatePicker +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.R +import ru.ulstu.`is`.airticketrentservice.navigation.BottomBarScreen +import ru.ulstu.`is`.airticketrentservice.viewModel.AppViewModelProvider +import ru.ulstu.`is`.airticketrentservice.viewModel.FlightDetails +import ru.ulstu.`is`.airticketrentservice.viewModel.FlightEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.FlightUiState +import ru.ulstu.`is`.airticketrentservice.viewModel.TicketEditViewModel +import java.util.Calendar +import java.util.Date + +@Composable +fun FlightInfo( + navController: NavController, + flightViewModel: FlightEditViewModel = viewModel(factory = AppViewModelProvider.Factory) + ) { + FlightInfo( + flightUiState = flightViewModel.flightUiState, + onClick = { + val route = BottomBarScreen.TicketEdit.passId(0.toString()) + navController.navigate(route) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun FlightInfo( + flightUiState: FlightUiState, + onClick: () -> Unit +) { + Column( + Modifier + .fillMaxWidth() + .padding(all = 10.dp) + .padding(horizontal = 24.dp) + .padding(vertical = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + value = flightUiState.flightDetails.direction_from, + onValueChange = { }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { Text(stringResource(id = R.string.ticket_from)) }, + singleLine = true + ) + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + value = flightUiState.flightDetails.direction_to, + onValueChange = { }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { Text(stringResource(id = R.string.ticket_to)) }, + singleLine = true + ) + + Text(text = " ${flightUiState.flightDetails.departure_date}", fontSize = 15.sp, color = Color.DarkGray) + + Text(text = flightUiState.flightDetails.arrival_date, fontSize = 15.sp, color = Color.DarkGray) + + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + value = flightUiState.flightDetails.tickets_count.toString(), + onValueChange = { }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { Text("Количество билетов") }, + singleLine = true + ) + + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + value = flightUiState.flightDetails.one_ticket_cost.toString(), + onValueChange = { }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { Text("Стоимость одного билета") }, + singleLine = true + ) + Button( + onClick = onClick, + enabled = flightUiState.isEntryValid, + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 10.dp, + pressedElevation = 6.dp + ), + shape = RoundedCornerShape(15.dp), + colors = ButtonDefaults.buttonColors(containerColor = colorResource(R.color.lightBlue)), + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "Сформировать билет") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/FlightList.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/FlightList.kt new file mode 100644 index 0000000..3cc3f08 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/FlightList.kt @@ -0,0 +1,243 @@ +package ru.ulstu.`is`.airticketrentservice.screen + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DismissDirection +import androidx.compose.material3.DismissState +import androidx.compose.material3.DismissValue +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SwipeToDismiss +import androidx.compose.material3.Text +import androidx.compose.material3.rememberDismissState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemContentType +import androidx.paging.compose.itemKey +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.R +import ru.ulstu.`is`.airticketrentservice.database.models.Flight +import ru.ulstu.`is`.airticketrentservice.navigation.BottomBarScreen +import ru.ulstu.`is`.airticketrentservice.viewModel.AppViewModelProvider +import ru.ulstu.`is`.airticketrentservice.viewModel.FlightListViewModel + +@Composable +fun FlightList( + navController: NavController, + viewModel: FlightListViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + val coroutineScope = rememberCoroutineScope() + val flightListUiState = viewModel.flightsListUiState.collectAsLazyPagingItems() + Scaffold( + topBar = {}, + floatingActionButton = { + FloatingActionButton( + onClick = { + val route = BottomBarScreen.FlightEdit.passId(0.toString()) + navController.navigate(route) + }, + containerColor = colorResource(R.color.lightlightBlue) + ) { + Icon(Icons.Filled.Add, "Добавить") + } + } + ) { innerPadding -> + FlightList( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + flightList = flightListUiState, + onClick = { id: Int -> + val route = BottomBarScreen.FlightEdit.passId(id.toString()) + navController.navigate(route) + }, + onSwipe = { flight: Flight -> + coroutineScope.launch { + viewModel.deleteFlight(flight) + } + } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun DismissBackground(dismissState: DismissState) { + val color = when (dismissState.dismissDirection) { + DismissDirection.StartToEnd -> Color.Transparent + DismissDirection.EndToStart -> Color(0xFFFF1744) + null -> Color.Transparent + } + val direction = dismissState.dismissDirection + + Row( + modifier = Modifier + .fillMaxSize() + .background(color) + .padding(12.dp, 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + if (direction == DismissDirection.EndToStart) { + Icon( + Icons.Default.Delete, + contentDescription = "delete", + tint = Color.White + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SwipeToDelete( + dismissState: DismissState, + flight: Flight, + onClick: (id: Int) -> Unit +) { + SwipeToDismiss( + modifier = Modifier.zIndex(1f), + state = dismissState, + directions = setOf( + DismissDirection.EndToStart + ), + background = { + DismissBackground(dismissState) + }, + dismissContent = { + FlightListItem(flight = flight, + modifier = Modifier + .padding(vertical = 7.dp) + .clickable { onClick(flight.id) }) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) +@Composable +private fun FlightList( + modifier: Modifier = Modifier, + flightList: LazyPagingItems, + onClick: (id: Int) -> Unit, + onSwipe: (flight: Flight) -> Unit +) { + val refreshScope = rememberCoroutineScope() + var refreshing by remember { mutableStateOf(false) } + fun refresh() = refreshScope.launch { + refreshing = true + flightList.refresh() + refreshing = false + } + + val state = rememberPullRefreshState(refreshing, ::refresh) + Box( + modifier = modifier.pullRefresh(state) + ) { + Column( + modifier = modifier.fillMaxSize() + ) { + LazyColumn(modifier = Modifier.padding(all = 10.dp)) { + items( + count = flightList.itemCount, + key = flightList.itemKey(), + contentType = flightList.itemContentType() + ) { index -> + val flight = flightList[index] + flight?.let { + var show by remember { mutableStateOf(true) } + val dismissState = rememberDismissState( + confirmValueChange = { + if (it == DismissValue.DismissedToStart || + it == DismissValue.DismissedToEnd + ) { + show = false + true + } else false + }, positionalThreshold = { 200.dp.toPx() } + ) + + AnimatedVisibility( + show, exit = fadeOut(spring()) + ) { + SwipeToDelete( + dismissState = dismissState, + flight = flight, + onClick = onClick + ) + } + + LaunchedEffect(show) { + if (!show) { + delay(800) + onSwipe(flight) + } + } + } + } + } + PullRefreshIndicator( + refreshing, state, + Modifier + .align(CenterHorizontally) + .zIndex(100f) + ) + } + } +} + +@Composable +private fun FlightListItem( + flight: Flight, modifier: Modifier = Modifier +) { + Card( + modifier = modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = modifier.padding(all = 10.dp) + ) { + Text( + text = String.format("%s %s", flight.direction_from, flight.direction_to) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/FoundFlights.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/FoundFlights.kt new file mode 100644 index 0000000..45cffe8 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/FoundFlights.kt @@ -0,0 +1,109 @@ +package ru.ulstu.`is`.airticketrentservice.screen + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DismissDirection +import androidx.compose.material3.DismissState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SwipeToDismiss +import androidx.compose.material3.Text +import androidx.compose.material3.rememberDismissState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import ru.ulstu.`is`.airticketrentservice.R +import ru.ulstu.`is`.airticketrentservice.database.models.Flight +import ru.ulstu.`is`.airticketrentservice.navigation.BottomBarScreen +import ru.ulstu.`is`.airticketrentservice.viewModel.AppViewModelProvider +import ru.ulstu.`is`.airticketrentservice.viewModel.FindFlightsViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.FoundFlightsUiState + +@Composable +fun FoundFlights( + navController: NavController, + viewModel: FindFlightsViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + val foundFlightsUiState = viewModel.foundFlightsUiState + + Scaffold( + topBar = {} + ) { innerPadding -> + FoundFlights( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + flightList = foundFlightsUiState.flightList, + onClick = { uid: Int -> + val route = BottomBarScreen.FlightInfo.passId(uid.toString()) + navController.navigate(route) + } + ) + } +} + +@Composable +private fun FoundFlights( + modifier: Modifier = Modifier, + flightList: List, + onClick: (uid: Int) -> Unit +) { + Column( + modifier = modifier.fillMaxSize() + ) { + if (flightList.isEmpty()) { + Text( + text = "Подходящие рейсы не найдены", + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge + ) + } else { + LazyColumn(modifier = Modifier.padding(all = 10.dp)) { + items(items = flightList, key = { it.id }) { flight -> + FlightListItem(flight = flight, modifier = Modifier.clickable{ onClick(flight.id) }) + } + } + } + } +} + +@Composable +private fun FlightListItem( + flight: Flight, modifier: Modifier = Modifier +) { + Card( + modifier = modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), + colors = CardDefaults.cardColors(containerColor = colorResource(id = R.color.lightlightBlue)) + ) { + Column( + modifier = modifier.padding(all = 10.dp) + ) { + Text(text = "${flight.direction_from} --> ${flight.direction_to}") + Text(text = "${flight.one_ticket_cost}") + } + } +} diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/MainPage.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/MainPage.kt new file mode 100644 index 0000000..9c3b455 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/MainPage.kt @@ -0,0 +1,245 @@ +package ru.ulstu.`is`.airticketrentservice.screen + +import android.annotation.SuppressLint +import android.app.DatePickerDialog +import android.widget.DatePicker +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.imageResource +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.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.navigation.NavDestination +import androidx.navigation.NavHostController +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import ru.ulstu.`is`.airticketrentservice.R +import ru.ulstu.`is`.airticketrentservice.graphs.HomeNavGraph +import ru.ulstu.`is`.airticketrentservice.navigation.BottomBar +import ru.ulstu.`is`.airticketrentservice.navigation.BottomBarScreen +import ru.ulstu.`is`.airticketrentservice.viewModel.AppViewModelProvider +import ru.ulstu.`is`.airticketrentservice.viewModel.CurrentUserViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.FlightEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.FlightListViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.LoginViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.RegistrationViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.RentEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.RentListViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.TicketEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.TicketListViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.UserEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.UserListViewModel +import java.util.Calendar +import java.util.Date + +@Composable +fun MainPage( + navController: NavController, + flightListViewModel: FlightListViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + Scaffold( + ) { innerPadding -> + MainPage( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + navController = navController + ) + } +} + +@Composable +fun MainPage( + modifier: Modifier = Modifier, + navController: NavController +) { + val from = remember { mutableStateOf("") } + val to = remember { mutableStateOf("") } + val departureDate = remember { mutableStateOf("") } + + + val state = rememberScrollState() + LaunchedEffect(Unit) { state.animateScrollTo(100) } + Column( + Modifier + .padding(all = 10.dp) + .fillMaxSize() + .navigationBarsPadding() + .padding(horizontal = 24.dp) + .padding(vertical = 32.dp) + .verticalScroll(state) + .padding(top = 50.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + bitmap = ImageBitmap.imageResource(R.drawable.logo), + contentDescription = "Логотип", + modifier = Modifier.size(100.dp, 100.dp) + ) + Text( + text = stringResource(id = R.string.main_heading), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + fontSize = 30.sp, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 10.dp), + ) + Divider( + color = Color.Black, thickness = 2.dp, modifier = Modifier + .padding(bottom = 24.dp) + .fillMaxWidth() + ) + TextField(modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp), + value = from.value, onValueChange = { from.value = it }, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.LightGray.copy(.2f), + unfocusedContainerColor = Color.LightGray.copy(.2f), + disabledContainerColor = Color.LightGray.copy(.2f), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(15.dp), + label = { + Text(stringResource(id = R.string.ticket_from)) + } + ) + TextField(modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp), + value = to.value, onValueChange = { to.value = it }, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.LightGray.copy(.2f), + unfocusedContainerColor = Color.LightGray.copy(.2f), + disabledContainerColor = Color.LightGray.copy(.2f), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(15.dp), + label = { + Text(stringResource(id = R.string.ticket_to)) + } + ) + + val mContext = LocalContext.current + val yearFrom: Int + val monthFrom: Int + val dayFrom: Int + + val calendarFrom = Calendar.getInstance() + + yearFrom = calendarFrom.get(Calendar.YEAR) + monthFrom = calendarFrom.get(Calendar.MONTH) + dayFrom = calendarFrom.get(Calendar.DAY_OF_MONTH) + + calendarFrom.time = Date() + + val dateFrom = remember { mutableStateOf("") } + + val datePickerDialogFrom = DatePickerDialog( + mContext, + { _: DatePicker, year: Int, month: Int, dayOfMonth: Int -> + val selectedDateFrom = "$dayOfMonth-${month + 1}-$year" + dateFrom.value = selectedDateFrom + departureDate.value = selectedDateFrom + }, yearFrom, monthFrom, dayFrom + ) + + Button( + colors = ButtonDefaults.buttonColors( + containerColor = (colorResource(id = R.color.lightBlue)), + contentColor = Color.White + ), + onClick = { + datePickerDialogFrom.show() + }, + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 10.dp, + pressedElevation = 6.dp + ), + shape = RoundedCornerShape(15.dp), + modifier = Modifier + .fillMaxWidth(), + ) { + Text("Выбрать дату вылета") + } + + Text(text = " ${departureDate.value}", fontSize = 15.sp, color = Color.DarkGray) + + Button( + modifier = Modifier + .fillMaxWidth(), + onClick = { + navController.navigate(BottomBarScreen.FoundFlights.passText(from.value, to.value, departureDate.value)) + }, + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 10.dp, + pressedElevation = 6.dp + ), + shape = RoundedCornerShape(15.dp), + colors = ButtonDefaults.buttonColors(containerColor = colorResource(R.color.lightBlue)), + content = { + Text(stringResource(id = R.string.button_search)) + } + ) + } +} + +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") +@Composable +fun LoadScreen(navController: NavHostController = rememberNavController(), + currentUserViewModel: CurrentUserViewModel = viewModel(factory = AppViewModelProvider.Factory), + flightListViewModel: FlightListViewModel = viewModel(factory = AppViewModelProvider.Factory), + userListViewModel: UserListViewModel = viewModel(factory = AppViewModelProvider.Factory), + ticketListViewModel: TicketListViewModel = viewModel(factory = AppViewModelProvider.Factory), + rentListViewModel: RentListViewModel = viewModel(factory = AppViewModelProvider.Factory) +){ + Scaffold( + bottomBar = {BottomBar(navController = navController) }, + ) { + Modifier + .padding(it) + HomeNavGraph( + navController = navController, + currentUserViewModel = currentUserViewModel, + flightListViewModel = flightListViewModel, + userListViewModel = userListViewModel, + ticketListViewModel = ticketListViewModel, + rentListViewModel = rentListViewModel + ) + } +} diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/MyRents.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/MyRents.kt new file mode 100644 index 0000000..1984af8 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/MyRents.kt @@ -0,0 +1,219 @@ +package ru.ulstu.`is`.airticketrentservice.screen + +import android.util.Log +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DismissDirection +import androidx.compose.material3.DismissState +import androidx.compose.material3.DismissValue +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SwipeToDismiss +import androidx.compose.material3.Text +import androidx.compose.material3.rememberDismissState +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.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemContentType +import androidx.paging.compose.itemKey +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.database.models.Rent +import ru.ulstu.`is`.airticketrentservice.viewModel.AppViewModelProvider +import ru.ulstu.`is`.airticketrentservice.viewModel.CurrentUserViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.RentListViewModel +import ru.ulstu.`is`.airticketrentservice.R +import ru.ulstu.`is`.airticketrentservice.database.models.Flight +import ru.ulstu.`is`.airticketrentservice.navigation.BottomBarScreen +import ru.ulstu.`is`.airticketrentservice.viewModel.UsersRentsViewModel + +@Composable +fun MyRents( + navController: NavController, + viewModel: UsersRentsViewModel = viewModel(factory = AppViewModelProvider.Factory), + currentUserViewModel: CurrentUserViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + val coroutineScope = rememberCoroutineScope() + + val getUser by remember { mutableStateOf(currentUserViewModel.user) } + val userRentsUiState = viewModel.userRentsUiState + Log.d("RentListViewModel", "мои бронирования: ${userRentsUiState.rentList}") + + Scaffold( + ) { innerPadding -> + MyRents( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + rentList = userRentsUiState.rentList, + onClick = { uid: Int -> + val route = BottomBarScreen.RentEdit.passId(uid.toString()) + navController.navigate(route) + }, + onSwipe = { rent: Rent -> + coroutineScope.launch { + viewModel.deleteRent(rent) + } + } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun DismissBackground(dismissState: DismissState) { + val color = when (dismissState.dismissDirection) { + DismissDirection.StartToEnd -> Color.Transparent + DismissDirection.EndToStart -> Color(0xFFFF1744) + null -> Color.Transparent + } + val direction = dismissState.dismissDirection + + Row( + modifier = Modifier + .fillMaxSize() + .background(color) + .padding(12.dp, 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + if (direction == DismissDirection.EndToStart) { + Icon( + Icons.Default.Delete, + contentDescription = "delete", + tint = Color.White + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SwipeToDelete( + dismissState: DismissState, + rent: Rent, + onClick: (id: Int) -> Unit +) { + SwipeToDismiss( + modifier = Modifier.zIndex(1f), + state = dismissState, + directions = setOf( + DismissDirection.EndToStart + ), + background = { + DismissBackground(dismissState) + }, + dismissContent = { + RentListItem(rent = rent, + modifier = Modifier + .padding(vertical = 7.dp) + .clip(shape = RoundedCornerShape(15.dp)) + .clickable { onClick(rent.id) }) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) +@Composable +private fun MyRents( + modifier: Modifier = Modifier, + rentList: List, + onClick: (id: Int) -> Unit, + onSwipe: (rent: Rent) -> Unit +) { + Column( + modifier = modifier.fillMaxSize() + ) { + if (rentList.isEmpty()) { + Text( + text = "Бронирований пока нет", + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge + ) + } else { + LazyColumn(modifier = Modifier.padding(all = 10.dp)) { + items(items = rentList, key = { it.id }) { rent -> + val dismissState: DismissState = rememberDismissState( + positionalThreshold = { 200.dp.toPx() } + ) + + if (dismissState.isDismissed(direction = DismissDirection.EndToStart)) { + onSwipe(rent) + } + + SwipeToDelete( + dismissState = dismissState, + rent = rent, + onClick = onClick + ) + } + } + } + } +} + +@Composable +private fun RentListItem( + modifier: Modifier = Modifier, rent: Rent +) { + Card( + modifier = modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = modifier.padding(all = 10.dp) + ) { + Text( + text = "Бронирование ${rent.id}" + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/Profile.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/Profile.kt new file mode 100644 index 0000000..0b518ae --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/Profile.kt @@ -0,0 +1,189 @@ +package ru.ulstu.`is`.airticketrentservice.screen + +import android.util.Log +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.graphics.ImageBitmap +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.imageResource +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.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import ru.ulstu.`is`.airticketrentservice.viewModel.AppViewModelProvider +import ru.ulstu.`is`.airticketrentservice.viewModel.CurrentUserViewModel +import ru.ulstu.`is`.airticketrentservice.R +import ru.ulstu.`is`.airticketrentservice.graphs.AuthScreen +import ru.ulstu.`is`.airticketrentservice.navigation.BottomBarScreen +import ru.ulstu.`is`.airticketrentservice.viewModel.toUiState + +@Composable +fun Profile( + navController: NavController, + currentUserViewModel: CurrentUserViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + val getUser by remember { mutableStateOf(currentUserViewModel.user) } + val userUiState = getUser?.toUiState(true) + Log.d("CurrentUserViewModel1", "Текущий пользователь: $getUser") + val state = rememberScrollState() + LaunchedEffect(Unit) { state.animateScrollTo(100) } + Column( + Modifier + .padding(all = 10.dp) + .fillMaxSize() + .navigationBarsPadding() + .padding(horizontal = 24.dp) + .padding(vertical = 32.dp) + .verticalScroll(state) + .padding(top = 50.dp), + horizontalAlignment = Alignment.CenterHorizontally) { + Image( + bitmap = ImageBitmap.imageResource(R.drawable.logo), + contentDescription = "Логотип", + modifier = Modifier.size(100.dp, 100.dp) + ) + Text( + text = stringResource(id = R.string.profile_heading), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + fontSize = 30.sp, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 10.dp), + ) + Divider(color = Color.Black, thickness = 2.dp,modifier = Modifier + .padding(bottom = 24.dp) + .fillMaxWidth()) + TextField(modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp), + value = "${getUser?.surname} ${getUser?.name} ${getUser?.patronymic}", onValueChange = {}, readOnly = true, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.LightGray.copy(.2f), + unfocusedContainerColor = Color.LightGray.copy(.2f), + disabledContainerColor = Color.LightGray.copy(.2f), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(15.dp), + label = { + Text(stringResource(id = R.string.name)) + } + ) + TextField(modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp), + value = "${getUser?.email}", onValueChange = {}, readOnly = true, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.LightGray.copy(.2f), + unfocusedContainerColor = Color.LightGray.copy(.2f), + disabledContainerColor = Color.LightGray.copy(.2f), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(15.dp), + label = { + Text(stringResource(id = R.string.login_label)) + } + ) + TextField(modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp), + value = "${getUser?.date_of_birth}", onValueChange = {}, readOnly = true, + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.LightGray.copy(.2f), + unfocusedContainerColor = Color.LightGray.copy(.2f), + disabledContainerColor = Color.LightGray.copy(.2f), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(15.dp), + label = { + Text(stringResource(id = R.string.dateOfBirth)) + } + ) + Button( + modifier = Modifier + .fillMaxWidth() + .padding(5.dp), + onClick = { + navController.navigate(BottomBarScreen.UserEdit.passId(getUser?.id.toString())) + }, + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 10.dp, + pressedElevation = 6.dp + ), + shape = RoundedCornerShape(15.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color.LightGray), + content = { + Text("Изменить") + } + ) +// Button( +// modifier = Modifier +// .fillMaxWidth() +// .padding(5.dp), +// onClick = { +// val route = BottomBarScreen.MyRents.passId(getUser?.id.toString()) +// navController.navigate(route) +// }, +// elevation = ButtonDefaults.buttonElevation( +// defaultElevation = 10.dp, +// pressedElevation = 6.dp +// ), +// shape = RoundedCornerShape(15.dp), +// colors = ButtonDefaults.buttonColors(containerColor = colorResource(R.color.lightBlue)), +// content = { +// Text("Мои бронирования") +// } +// ) +// Button( +// modifier = Modifier +// .fillMaxWidth() +// .padding(5.dp), +// onClick = { +// navController.navigate(AuthScreen.Login.route) +// }, +// elevation = ButtonDefaults.buttonElevation( +// defaultElevation = 10.dp, +// pressedElevation = 6.dp +// ), +// shape = RoundedCornerShape(15.dp), +// colors = ButtonDefaults.buttonColors(containerColor = colorResource(R.color.lightBlue)), +// content = { +// Text("Выйти") +// } +// ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/RentEdit.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/RentEdit.kt new file mode 100644 index 0000000..381826f --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/RentEdit.kt @@ -0,0 +1,248 @@ +package ru.ulstu.`is`.airticketrentservice.screen + +import android.annotation.SuppressLint +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults.TrailingIcon +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.R +import ru.ulstu.`is`.airticketrentservice.database.models.Ticket +import ru.ulstu.`is`.airticketrentservice.database.models.User +import ru.ulstu.`is`.airticketrentservice.viewModel.AppViewModelProvider +import ru.ulstu.`is`.airticketrentservice.viewModel.CurrentUserViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.FlightEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.FlightUiState +import ru.ulstu.`is`.airticketrentservice.viewModel.RentDetails +import ru.ulstu.`is`.airticketrentservice.viewModel.RentEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.RentUiState +import ru.ulstu.`is`.airticketrentservice.viewModel.TicketEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.TicketUiState +import ru.ulstu.`is`.airticketrentservice.viewModel.TicketsListUiState +import ru.ulstu.`is`.airticketrentservice.viewModel.UserEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.UserUiState +import ru.ulstu.`is`.airticketrentservice.viewModel.UsersListUiState + +@SuppressLint("UnrememberedMutableState") +@Composable +fun RentEdit( + navController: NavController, + viewModel: RentEditViewModel = viewModel(factory = AppViewModelProvider.Factory), + userViewModel: UserEditViewModel = viewModel(factory = AppViewModelProvider.Factory), + ticketViewModel: TicketEditViewModel = viewModel(factory = AppViewModelProvider.Factory) + ) { + val coroutineScope = rememberCoroutineScope() + userViewModel.setCurrentUser(viewModel.rentUiState.rentDetails.userId) + ticketViewModel.setCurrentTicket(viewModel.rentUiState.rentDetails.ticketId) + RentEdit( + rentUiState = viewModel.rentUiState, + userUiState = userViewModel.userUiState, + ticketUiState = ticketViewModel.ticketUiState, + onClick = { + coroutineScope.launch { + viewModel.saveRent() + navController.popBackStack() + } + }, + onUpdate = viewModel::updateUiState + ) +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun RentEdit( + rentUiState: RentUiState, + userUiState: UserUiState, + ticketUiState: TicketUiState, + onClick: () -> Unit, + onUpdate: (RentDetails) -> Unit +) { + Column( + Modifier + .fillMaxWidth() + .padding(all = 10.dp) + ) { + TextField( + value = "${userUiState.userDetails.surname} ${userUiState.userDetails.name} ${userUiState.userDetails.patronymic}", + onValueChange = {}, + readOnly = true, + modifier = Modifier.fillMaxWidth(), + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.LightGray.copy(.2f), + unfocusedContainerColor = Color.LightGray.copy(.2f), + disabledContainerColor = Color.LightGray.copy(.2f), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(15.dp), + label = { + Text("Пользователь") + } + ) + + TextField( + value = "${ticketUiState.ticketDetails.ticket_cost}", + onValueChange = {}, + readOnly = true, + modifier = Modifier.fillMaxWidth(), + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.LightGray.copy(.2f), + unfocusedContainerColor = Color.LightGray.copy(.2f), + disabledContainerColor = Color.LightGray.copy(.2f), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(15.dp), + label = { + Text("Стоимость билета") + } + ) + + TextField( + value = "${ticketUiState.ticketDetails.passengers_count}", + onValueChange = {}, + readOnly = true, + modifier = Modifier.fillMaxWidth(), + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.LightGray.copy(.2f), + unfocusedContainerColor = Color.LightGray.copy(.2f), + disabledContainerColor = Color.LightGray.copy(.2f), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(15.dp), + label = { + Text("Количество пассажиров") + } + ) + + TextField( + value = "Ожидает подтверждения", + onValueChange = { onUpdate(rentUiState.rentDetails.copy(status = it)) }, + readOnly = true, + modifier = Modifier.fillMaxWidth(), + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.LightGray.copy(.2f), + unfocusedContainerColor = Color.LightGray.copy(.2f), + disabledContainerColor = Color.LightGray.copy(.2f), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + shape = RoundedCornerShape(15.dp), + label = { + Text(stringResource(id = R.string.rent_status)) + } + ) + +// var expanded by remember { mutableStateOf(false) } +// +// Box( +// modifier = Modifier.fillMaxWidth().padding(all = 5.dp).clip(RoundedCornerShape(15.dp)) +// ) { +// ExposedDropdownMenuBox( +// modifier = Modifier.fillMaxWidth(), +// expanded = expanded, +// onExpandedChange = { expanded = !expanded }, +// ) +// { +// TextField( +// value = rentUiState.rentDetails.status, +// onValueChange = {}, +// readOnly = true, +// trailingIcon = { +// TrailingIcon(expanded = expanded) +// }, +// modifier = Modifier.menuAnchor().fillMaxWidth(), +// colors = TextFieldDefaults.colors( +// focusedContainerColor = Color.LightGray.copy(.2f), +// unfocusedContainerColor = Color.LightGray.copy(.2f), +// disabledContainerColor = Color.LightGray.copy(.2f), +// focusedIndicatorColor = Color.Transparent, +// unfocusedIndicatorColor = Color.Transparent, +// ), +// shape = RoundedCornerShape(15.dp), +// label = { +// Text(stringResource(id = R.string.rent_status)) +// } +// ) +// ExposedDropdownMenu( +// expanded = expanded, +// onDismissRequest = { expanded = false }, +// modifier = Modifier +// .background(Color.LightGray.copy(.2f)) +// .exposedDropdownSize() +// .fillMaxWidth() +// ) { +// DropdownMenuItem( +// modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(15.dp)), +// text = { Text("Подтверждено") }, +// onClick = { +// onUpdate(rentUiState.rentDetails.copy(status = "Подтверждено")) +// expanded = false +// } +// ) +// DropdownMenuItem( +// modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(15.dp)), +// text = { Text("Ожидает подтверждения") }, +// onClick = { +// onUpdate(rentUiState.rentDetails.copy(status = "Ожидает подтверждения")) +// expanded = false +// } +// ) +// DropdownMenuItem( +// modifier = Modifier.fillMaxWidth().clip(RoundedCornerShape(15.dp)), +// text = { Text("Отклонено") }, +// onClick = { +// onUpdate(rentUiState.rentDetails.copy(status = "Отклонено")) +// expanded = false +// } +// ) +// } +// } +// } + + Button( + enabled = rentUiState.isEntryValid, + modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp), + onClick = onClick, + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 10.dp, + pressedElevation = 6.dp + ), + shape = RoundedCornerShape(15.dp), + colors = ButtonDefaults.buttonColors(containerColor = colorResource(R.color.lightBlue)), + content = { + Text(text = stringResource(R.string.rent_save_button)) + } + ) + + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/RentList.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/RentList.kt new file mode 100644 index 0000000..6341908 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/RentList.kt @@ -0,0 +1,255 @@ +package ru.ulstu.`is`.airticketrentservice.screen + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DismissDirection +import androidx.compose.material3.DismissState +import androidx.compose.material3.DismissValue +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.SwipeToDismiss +import androidx.compose.material3.Text +import androidx.compose.material3.rememberDismissState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemContentType +import androidx.paging.compose.itemKey +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.database.models.Rent +import ru.ulstu.`is`.airticketrentservice.navigation.BottomBarScreen +import ru.ulstu.`is`.airticketrentservice.viewModel.AppViewModelProvider +import ru.ulstu.`is`.airticketrentservice.viewModel.RentListViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.TicketListViewModel + +@Composable +fun RentList( + navController: NavController, + viewModel: RentListViewModel = viewModel(factory = AppViewModelProvider.Factory), + ticketListViewModel: TicketListViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + val coroutineScope = rememberCoroutineScope() + val rentListUiState = viewModel.rentListUiState.collectAsLazyPagingItems() + + Scaffold( + topBar = {}, + floatingActionButton = { + FloatingActionButton( + onClick = { + val route = BottomBarScreen.RentEdit.passId(0.toString()) + navController.navigate(route) + }, + ) { + Icon(Icons.Filled.Add, "Добавить") + } + } + ) { innerPadding -> + RentList( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + rentList = rentListUiState, + onClick = { uid: Int -> + val route = BottomBarScreen.RentEdit.passId(uid.toString()) + navController.navigate(route) + }, + onSwipe = { rent: Rent -> + coroutineScope.launch { + viewModel.deleteRent(rent) + } + } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun DismissBackground(dismissState: DismissState) { + val color = when (dismissState.dismissDirection) { + DismissDirection.StartToEnd -> Color.Transparent + DismissDirection.EndToStart -> Color(0xFFFF1744) + null -> Color.Transparent + } + val direction = dismissState.dismissDirection + + Row( + modifier = Modifier + .fillMaxSize() + .background(color) + .padding(12.dp, 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + if (direction == DismissDirection.EndToStart) { + Icon( + Icons.Default.Delete, + contentDescription = "delete", + tint = Color.White + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SwipeToDelete( + dismissState: DismissState, + rent: Rent, + onClick: (uid: Int) -> Unit +) { + SwipeToDismiss( + modifier = Modifier.zIndex(1f), + state = dismissState, + directions = setOf( + DismissDirection.EndToStart + ), + background = { + DismissBackground(dismissState) + }, + dismissContent = { + RentListItem(rent = rent, + modifier = Modifier + .padding(vertical = 7.dp) + .clickable { onClick(rent.id) }) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) +@Composable +private fun RentList( + modifier: Modifier = Modifier, + rentList: LazyPagingItems, + onClick: (uid: Int) -> Unit, + onSwipe: (rent: Rent) -> Unit +) { + val refreshScope = rememberCoroutineScope() + var refreshing by remember { mutableStateOf(false) } + fun refresh() = refreshScope.launch { + refreshing = true + rentList.refresh() + refreshing = false + } + + val state = rememberPullRefreshState(refreshing, ::refresh) + Box( + modifier = modifier.pullRefresh(state) + ) { + Column( + modifier = modifier.fillMaxSize() + ) { + LazyColumn(modifier = Modifier.padding(all = 10.dp)) { + items( + count = rentList.itemCount, + key = rentList.itemKey(), + contentType = rentList.itemContentType() + ) { index -> + val rent = rentList[index] + rent?.let { + var show by remember { mutableStateOf(true) } + val dismissState = rememberDismissState( + confirmValueChange = { + if (it == DismissValue.DismissedToStart || + it == DismissValue.DismissedToEnd + ) { + show = false + true + } else false + }, positionalThreshold = { 200.dp.toPx() } + ) + + AnimatedVisibility( + show, exit = fadeOut(spring()) + ) { + SwipeToDelete( + dismissState = dismissState, + rent = rent, + onClick = onClick + ) + } + + LaunchedEffect(show) { + if (!show) { + delay(800) + onSwipe(rent) + } + } + } + } + } + PullRefreshIndicator( + refreshing, state, + Modifier + .align(CenterHorizontally) + .zIndex(100f) + ) + } + } +} + +@Composable +private fun RentListItem( + rent: Rent, modifier: Modifier = Modifier +) { + Card( + modifier = modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = modifier.padding(all = 10.dp) + ) { + Text( + text = "Бронирование ${rent.id}" + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/TicketEdit.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/TicketEdit.kt new file mode 100644 index 0000000..a8a9581 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/TicketEdit.kt @@ -0,0 +1,198 @@ +package ru.ulstu.`is`.airticketrentservice.screen + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.R +import ru.ulstu.`is`.airticketrentservice.navigation.BottomBarScreen +import ru.ulstu.`is`.airticketrentservice.viewModel.AppViewModelProvider +import ru.ulstu.`is`.airticketrentservice.viewModel.FlightEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.FlightUiState +import ru.ulstu.`is`.airticketrentservice.viewModel.RentEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.TicketDetails +import ru.ulstu.`is`.airticketrentservice.viewModel.TicketEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.TicketUiState + +@Composable +fun TicketEdit( + navController: NavController, + viewModel: TicketEditViewModel = viewModel(factory = AppViewModelProvider.Factory), + flightViewModel: FlightEditViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + val coroutineScope = rememberCoroutineScope() + TicketEdit( + ticketUiState = viewModel.ticketUiState, + flightUiState = flightViewModel.flightUiState, + onClick = { + coroutineScope.launch { + viewModel.saveTicket() + navController.navigate(BottomBarScreen.RentEdit.passId(0.toString())) + } + }, + onUpdate = viewModel::updateUiState, + totalCost = flightViewModel.flightUiState.flightDetails.one_ticket_cost * viewModel.ticketUiState.ticketDetails.passengers_count.toDouble() + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TicketEdit( + ticketUiState: TicketUiState, + flightUiState: FlightUiState, + onClick: () -> Unit, + onUpdate: (TicketDetails) -> Unit, + totalCost: Double +) { + //ticketUiState.ticketDetails.ticket_cost = totalCost + Column( + Modifier + .fillMaxWidth() + .padding(all = 10.dp) + .padding(horizontal = 24.dp) + .padding(vertical = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + value = flightUiState.flightDetails.direction_from, + onValueChange = {}, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { Text(stringResource(id = R.string.ticket_from)) }, + singleLine = true, + readOnly = true + ) + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + value = flightUiState.flightDetails.direction_to, + onValueChange = {}, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { Text(stringResource(id = R.string.ticket_to)) }, + singleLine = true, + readOnly = true + ) + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + value = flightUiState.flightDetails.departure_date, + onValueChange = {}, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { Text(stringResource(id = R.string.ticket_arrivalDate)) }, + singleLine = true, + readOnly = true + ) + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + value = flightUiState.flightDetails.arrival_date, + onValueChange = {}, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { Text(stringResource(id = R.string.ticket_departureDate)) }, + singleLine = true, + readOnly = true + ) + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + value = totalCost.toString(), + onValueChange = { onUpdate(ticketUiState.ticketDetails.copy(ticket_cost = it.toDouble())) }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { Text("Стоимость билета") }, + singleLine = true, + readOnly = true + ) + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + value = ticketUiState.ticketDetails.passengers_count.toString(), + onValueChange = { onUpdate(ticketUiState.ticketDetails.copy(passengers_count = it.toInt())) }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { Text("Количество пассажиров") }, + singleLine = true + ) +// TextField( +// modifier = Modifier +// .fillMaxWidth() +// .padding(10.dp), +// value = totalCost.toString(), +// onValueChange = {}, +// colors = TextFieldDefaults.textFieldColors( +// containerColor = Color.LightGray.copy(.2f), +// unfocusedIndicatorColor = Color.Transparent, +// focusedIndicatorColor = Color.Transparent +// ), +// shape = RoundedCornerShape(15.dp), +// label = { Text("Стоимость за всех пассажиров") }, +// singleLine = true, +// readOnly = true +// ) + Button( + onClick = onClick, + enabled = ticketUiState.isEntryValid, + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 10.dp, + pressedElevation = 6.dp + ), + shape = RoundedCornerShape(15.dp), + colors = ButtonDefaults.buttonColors(containerColor = colorResource(R.color.lightBlue)), + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "Забронировать") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/UserEdit.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/UserEdit.kt new file mode 100644 index 0000000..9dbd430 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/UserEdit.kt @@ -0,0 +1,206 @@ +package ru.ulstu.`is`.airticketrentservice.screen + +import android.app.DatePickerDialog +import android.widget.DatePicker +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.currentCompositionErrors +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.viewModel.AppViewModelProvider +import ru.ulstu.`is`.airticketrentservice.viewModel.UserDetails +import ru.ulstu.`is`.airticketrentservice.viewModel.UserEditViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.UserUiState +import ru.ulstu.`is`.airticketrentservice.R +import ru.ulstu.`is`.airticketrentservice.viewModel.CurrentUserViewModel +import java.util.Calendar +import java.util.Date + +@Composable +fun UserEdit( + navController: NavController, + viewModel: UserEditViewModel = viewModel(factory = AppViewModelProvider.Factory), + currentUserViewModel: CurrentUserViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + val coroutineScope = rememberCoroutineScope() + UserEdit( + userUiState = viewModel.userUiState, + onClick = { + coroutineScope.launch { + viewModel.saveUser() + currentUserViewModel.user = viewModel.userUiState.user + navController.popBackStack() + } + }, + onUpdate = viewModel::updateUiState + ) +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun UserEdit( + userUiState: UserUiState, + onClick: () -> Unit, + onUpdate: (UserDetails) -> Unit, +) { + Column( + Modifier + .fillMaxWidth() + .padding(all = 10.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + TextField( + modifier = Modifier.fillMaxWidth().padding(10.dp), + value = userUiState.userDetails.surname, + onValueChange = { onUpdate(userUiState.userDetails.copy(surname = it)) }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { Text("Фамилия") }, + singleLine = true + ) + TextField( + modifier = Modifier.fillMaxWidth().padding(10.dp), + value = userUiState.userDetails.name, + onValueChange = { onUpdate(userUiState.userDetails.copy(name = it)) }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { Text("Имя") }, + singleLine = true + ) + TextField( + modifier = Modifier.fillMaxWidth().padding(10.dp), + value = userUiState.userDetails.patronymic, + onValueChange = { onUpdate(userUiState.userDetails.copy(patronymic = it)) }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { Text("Отчество") }, + singleLine = true + ) + val mContext = LocalContext.current + val year: Int + val month: Int + val day: Int + + // Initializing a Calendar + val calendar = Calendar.getInstance() + + // Fetching current year, month and day + year = calendar.get(Calendar.YEAR) + month = calendar.get(Calendar.MONTH) + day = calendar.get(Calendar.DAY_OF_MONTH) + + calendar.time = Date() + + // Declaring a string value to + // store date in string format + val date = remember { mutableStateOf("") } + + // Declaring DatePickerDialog and setting + // initial values as current values (present year, month and day) + val datePickerDialog = DatePickerDialog( + mContext, + { _: DatePicker, year: Int, month: Int, dayOfMonth: Int -> + val selectedDate = "$dayOfMonth-${month + 1}-$year" + date.value = selectedDate + onUpdate(userUiState.userDetails.copy(date_of_birth = selectedDate)) + }, year, month, day + ) + + Button( + colors = ButtonDefaults.buttonColors( + containerColor = (colorResource(id = R.color.lightBlue)), + contentColor = Color.White + ), + onClick = { + datePickerDialog.show() + }, + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 10.dp, + pressedElevation = 6.dp + ), + shape = RoundedCornerShape(15.dp), + modifier = Modifier + .fillMaxWidth(), + ) { + Text("Выбрать дату рождения") + } + + Text(text = " ${userUiState.userDetails.date_of_birth}", fontSize = 15.sp, color = Color.DarkGray, textAlign = TextAlign.Center) + + TextField( + modifier = Modifier.fillMaxWidth().padding(10.dp), + value = userUiState.userDetails.email, + onValueChange = { onUpdate(userUiState.userDetails.copy(email = it)) }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { Text("Электронная почта") }, + singleLine = true + ) + TextField( + modifier = Modifier.fillMaxWidth().padding(10.dp), + value = userUiState.userDetails.password, + onValueChange = { onUpdate(userUiState.userDetails.copy(password = it)) }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { Text(stringResource(id = R.string.user_password)) }, + singleLine = true + ) + Button( + onClick = onClick, + enabled = userUiState.isEntryValid, + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 10.dp, + pressedElevation = 6.dp + ), + shape = RoundedCornerShape(15.dp), + colors = ButtonDefaults.buttonColors(containerColor = colorResource(R.color.lightBlue)), + modifier = Modifier.fillMaxWidth().padding(10.dp) + ) { + Text(text = stringResource(R.string.ticket_save_button)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/UserList.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/UserList.kt new file mode 100644 index 0000000..2a4707d --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/UserList.kt @@ -0,0 +1,208 @@ +package ru.ulstu.`is`.airticketrentservice.screen + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DismissDirection +import androidx.compose.material3.DismissState +import androidx.compose.material3.DismissValue +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.SwipeToDismiss +import androidx.compose.material3.Text +import androidx.compose.material3.rememberDismissState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemContentType +import androidx.paging.compose.itemKey +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.database.models.User +import ru.ulstu.`is`.airticketrentservice.navigation.BottomBarScreen +import ru.ulstu.`is`.airticketrentservice.viewModel.AppViewModelProvider +import ru.ulstu.`is`.airticketrentservice.viewModel.UserListViewModel + +@Composable +fun UserList( + navController: NavController, + viewModel: UserListViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + val coroutineScope = rememberCoroutineScope() + val userListUiState = viewModel.userListUiState + Scaffold( + ) { innerPadding -> + UserList( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + userList = userListUiState.userList, + onClick = { uid: Int -> + navController.navigate(BottomBarScreen.UserEdit.passId(uid.toString())) + }, + onSwipe = { user: User -> + coroutineScope.launch { + viewModel.deleteUser(user) + } + } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun DismissBackground(dismissState: DismissState) { + val color = when (dismissState.dismissDirection) { + DismissDirection.StartToEnd -> Color.Transparent + DismissDirection.EndToStart -> Color(0xFFFF1744) + null -> Color.Transparent + } + val direction = dismissState.dismissDirection + + Row( + modifier = Modifier + .fillMaxSize() + .background(color) + .padding(12.dp, 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + if (direction == DismissDirection.EndToStart) { + Icon( + Icons.Default.Delete, + contentDescription = "delete", + tint = Color.White + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SwipeToDelete( + dismissState: DismissState, + user: User, + onClick: (id: Int) -> Unit +) { + SwipeToDismiss( + modifier = Modifier.zIndex(1f), + state = dismissState, + directions = setOf( + DismissDirection.EndToStart + ), + background = { + DismissBackground(dismissState) + }, + dismissContent = { + UserListItem(user = user, + modifier = Modifier + .padding(vertical = 7.dp) + .clickable { onClick(user.id) }) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) +@Composable +private fun UserList( + modifier: Modifier = Modifier, + userList: List, + onClick: (uid: Int) -> Unit, + onSwipe: (user: User) -> Unit +) { + Column( + modifier = modifier.fillMaxSize() + ) { + if (userList.isEmpty()) { + Text( + text = "Пользователи не найдены", + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge + ) + } else { + LazyColumn(modifier = Modifier.padding(all = 10.dp)) { + items(items = userList, key = { it.id }) { user -> + val dismissState: DismissState = rememberDismissState( + positionalThreshold = { 200.dp.toPx() } + ) + + if (dismissState.isDismissed(direction = DismissDirection.EndToStart)) { + onSwipe(user) + } + + SwipeToDelete( + dismissState = dismissState, + user = user, + onClick = onClick + ) + } + } + } + } +} + +@Composable +private fun UserListItem( + user: User, modifier: Modifier = Modifier +) { + Card( + modifier = modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = modifier.padding(all = 10.dp) + ) { + Text( + text = String.format("%s %s %s", user.surname, user.name, user.patronymic?: "") + ) + } + } +} diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/auth/Login.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/auth/Login.kt new file mode 100644 index 0000000..0e20790 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/auth/Login.kt @@ -0,0 +1,179 @@ +package ru.ulstu.`is`.airticketrentservice.screen.auth + +import android.annotation.SuppressLint +import android.util.Log +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +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.graphics.ImageBitmap +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.imageResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import ru.ulstu.`is`.airticketrentservice.database.models.User +import ru.ulstu.`is`.airticketrentservice.elements.ValidateEmail +import ru.ulstu.`is`.airticketrentservice.elements.isValidEmail +import ru.ulstu.`is`.airticketrentservice.viewModel.AppViewModelProvider +import ru.ulstu.`is`.airticketrentservice.viewModel.CurrentUserViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.LoginViewModel +import ru.ulstu.`is`.airticketrentservice.R +import ru.ulstu.`is`.airticketrentservice.api.repository.RestUserRepository +import ru.ulstu.`is`.airticketrentservice.graphs.AuthScreen +import ru.ulstu.`is`.airticketrentservice.graphs.Graph + +@SuppressLint("UnrememberedMutableState") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Login(navController: NavController, modifier: Modifier = Modifier, loginViewModel: LoginViewModel = viewModel(factory = AppViewModelProvider.Factory), currentUserViewModel: CurrentUserViewModel = viewModel(factory = AppViewModelProvider.Factory)) { + + var emailValue by rememberSaveable { mutableStateOf("") } + var passwordValue by rememberSaveable { mutableStateOf("") } + loginViewModel.setUserList() + val users = mutableStateOf>(loginViewModel.userList) + val argument = currentUserViewModel.argument.value + var passwordVisibility by rememberSaveable { mutableStateOf(false) } + val emailRegex = "[a-zA-Z0-9._-]+@[a-z]+\\.+[a-z]+".toRegex() + val coroutineScope = rememberCoroutineScope() + + Column( + Modifier + .padding(all = 10.dp) + .fillMaxSize() + .navigationBarsPadding() + .padding(horizontal = 24.dp) + .padding(vertical = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally) { + Image( + bitmap = ImageBitmap.imageResource(R.drawable.logo), + contentDescription = "Логотип", + modifier = Modifier.size(100.dp, 100.dp) + ) + Text( + text = stringResource(id = R.string.login_heading_text), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + fontSize = 40.sp, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 10.dp), + ) + Divider(color = Color.Black, thickness = 2.dp,modifier = Modifier + .padding(bottom = 24.dp) + .fillMaxWidth()) + ValidateEmail(emailValue) { emailValue = it } + TextField(modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp), + value = passwordValue, onValueChange = { passwordValue = it }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { + Text(stringResource(id = R.string.password_label)) + }, + placeholder = { Text("Пароль") }, + singleLine = true, + visualTransformation = if (passwordVisibility) VisualTransformation.None else PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + trailingIcon = { + val image = if (passwordVisibility) + Icons.Filled.Visibility + else Icons.Filled.VisibilityOff + + val description = + if (passwordVisibility) "Скрыть" else "Показать" + + IconButton(onClick = { passwordVisibility = !passwordVisibility }) { + Icon(imageVector = image, description) + } + } + ) + Button( + modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp), + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 10.dp, + pressedElevation = 6.dp + ), + shape = RoundedCornerShape(15.dp), + onClick = { + if (passwordValue.isNotEmpty() && isValidEmail(emailValue)) { + users.value.forEach { user -> + if (user.password == passwordValue && user.email == emailValue) { + currentUserViewModel.setArgument(user.id.toString()) + navController.navigate(route = Graph.passUserId(user.id.toString())) + Log.d("CurrentUserViewModel", "Текущий пользователь: $user") + } + } + } + }, + colors = ButtonDefaults.buttonColors(containerColor = colorResource(R.color.lightBlue)), + content = { + Text(stringResource(id = R.string.login_button)) + } + ) + Text( + text = "Ещё не зарегистрированы?", + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth(), + ) + TextButton( + modifier = Modifier + .fillMaxWidth(), + onClick = { + navController.navigate(route = AuthScreen.Registration.route) + }, + content = { + Text( + stringResource(id = R.string.registration), + color = colorResource(R.color.lightBlue), textDecoration = TextDecoration.Underline) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/auth/Registration.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/auth/Registration.kt new file mode 100644 index 0000000..7edfc99 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/screen/auth/Registration.kt @@ -0,0 +1,343 @@ +package ru.ulstu.`is`.airticketrentservice.screen.auth + +import android.annotation.SuppressLint +import android.app.DatePickerDialog +import android.util.Log +import android.widget.DatePicker +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +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.graphics.ImageBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.imageResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.room.ColumnInfo +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.database.models.User +import ru.ulstu.`is`.airticketrentservice.elements.isValidEmail +import ru.ulstu.`is`.airticketrentservice.viewModel.AppViewModelProvider +import ru.ulstu.`is`.airticketrentservice.viewModel.CurrentUserViewModel +import ru.ulstu.`is`.airticketrentservice.viewModel.RegistrationViewModel +import ru.ulstu.`is`.airticketrentservice.R +import ru.ulstu.`is`.airticketrentservice.elements.ValidateEmail +import ru.ulstu.`is`.airticketrentservice.graphs.AuthScreen +import ru.ulstu.`is`.airticketrentservice.graphs.Graph +import java.util.Calendar +import java.util.Date + +@SuppressLint("UnrememberedMutableState") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Registration(navController: NavController, modifier: Modifier = Modifier, registrationViewModel: RegistrationViewModel = viewModel(factory = AppViewModelProvider.Factory), currentUserViewModel: CurrentUserViewModel = viewModel(factory = AppViewModelProvider.Factory)) { + + var emailValue by rememberSaveable { mutableStateOf("") } + var passwordValue by rememberSaveable { mutableStateOf("") } + var surnameValue by rememberSaveable { mutableStateOf("") } + var nameValue by rememberSaveable { mutableStateOf("") } + var patronymicValue by rememberSaveable { mutableStateOf("") } + var dateOfBirthValue by rememberSaveable { mutableStateOf("") } + val coroutineScope = rememberCoroutineScope() + registrationViewModel.setUserList() + val users = mutableStateOf>(emptyList()) + registrationViewModel.users.observeForever { userList -> + users.value = userList + } + + var isValid = "Данные корректны" + + val state = rememberScrollState() + LaunchedEffect(Unit) { state.animateScrollTo(100) } + var passwordVisibility by rememberSaveable { mutableStateOf(false) } + val emailRegex = "[a-zA-Z0-9._-]+@[a-z]+\\.+[a-z]+".toRegex() + + Column( + Modifier + .padding(all = 10.dp) + .fillMaxSize() + .navigationBarsPadding() + .padding(horizontal = 24.dp) + .padding(vertical = 32.dp) + .verticalScroll(state), + horizontalAlignment = Alignment.CenterHorizontally) { + Image( + bitmap = ImageBitmap.imageResource(R.drawable.logo), + contentDescription = "Логотип", + modifier = Modifier.size(100.dp, 100.dp) + ) + Text( + text = stringResource(id = R.string.registration), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + fontSize = 30.sp, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 10.dp), + ) + Divider(color = Color.Black, thickness = 2.dp,modifier = Modifier + .padding(bottom = 24.dp) + .fillMaxWidth()) + + TextField(modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp), + value = surnameValue, onValueChange = { surnameValue = it }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { + Text(stringResource(id = R.string.surname)) + } + ) + TextField(modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp), + value = nameValue, onValueChange = { nameValue = it }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { + Text(stringResource(id = R.string.name)) + } + ) + TextField(modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp), + value = patronymicValue, onValueChange = { patronymicValue = it }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { + Text("Отчество") + } + ) + val mContext = LocalContext.current + val year: Int + val month: Int + val day: Int + + val calendar = Calendar.getInstance() + + year = calendar.get(Calendar.YEAR) + month = calendar.get(Calendar.MONTH) + day = calendar.get(Calendar.DAY_OF_MONTH) + + calendar.time = Date() + + val date = remember { mutableStateOf("") } + + val datePickerDialog = DatePickerDialog( + mContext, + { _: DatePicker, year: Int, month: Int, dayOfMonth: Int -> + val selectedDate = "$dayOfMonth-${month + 1}-$year" + date.value = selectedDate + dateOfBirthValue = selectedDate + }, year, month, day + ) + + Button( + colors = ButtonDefaults.buttonColors( + containerColor = (colorResource(id = R.color.lightBlue)), + contentColor = Color.White + ), + onClick = { + datePickerDialog.show() + }, + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 10.dp, + pressedElevation = 6.dp + ), + shape = RoundedCornerShape(15.dp), + modifier = Modifier + .fillMaxWidth(), + ) { + Text("Выбрать дату рождения") + } + + TextField(modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp), + value = dateOfBirthValue, onValueChange = { dateOfBirthValue = it }, readOnly = true, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { + Text(stringResource(id = R.string.dateOfBirth)) + } + ) + ValidateEmail(emailValue) { emailValue = it } +// TextField(modifier = Modifier +// .fillMaxWidth() +// .padding(all = 10.dp), +// value = emailValue, onValueChange = { emailValue = it }, +// colors = TextFieldDefaults.textFieldColors( +// containerColor = Color.LightGray.copy(.2f), +// unfocusedIndicatorColor = Color.Transparent, +// focusedIndicatorColor = Color.Transparent +// ), +// shape = RoundedCornerShape(15.dp), +// label = { +// Text("Электронная почта") +// } +// ) + TextField(modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp), + value = passwordValue, onValueChange = { passwordValue = it }, + colors = TextFieldDefaults.textFieldColors( + containerColor = Color.LightGray.copy(.2f), + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(15.dp), + label = { + Text(stringResource(id = R.string.password_label)) + }, + singleLine = true, + placeholder = { Text(stringResource(id = R.string.password_label)) }, + visualTransformation = if (passwordVisibility) VisualTransformation.None else PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + trailingIcon = { + val image = if (passwordVisibility) + Icons.Filled.Visibility + else Icons.Filled.VisibilityOff + + val description = + if (passwordVisibility) "Скрыть" else "Показать" + + IconButton(onClick = { passwordVisibility = !passwordVisibility }) { + Icon(imageVector = image, description) + } + } + ) + Text( + text = isValid, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth(), + ) + Button( + modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp), + onClick = { + var isExist = false; + if (passwordValue.isNotEmpty() && isValidEmail(emailValue) && surnameValue.isNotEmpty() && nameValue.isNotEmpty() && patronymicValue.isNotEmpty() && dateOfBirthValue.isNotEmpty()) { + users.value.forEach { user -> + if (user.email == emailValue) { + Log.d("User already exist. User id: ", user.id.toString()) + isExist = true + } + } + if (!isExist) { + val newUser = User(0, surnameValue, nameValue, patronymicValue, dateOfBirthValue, emailValue, passwordValue, "user") + coroutineScope.launch { + val insertResult = async { + registrationViewModel.insertUser(newUser) + } + + insertResult.await() + + registrationViewModel.setUserList() + registrationViewModel.users.observeForever { userList -> + users.value = userList + Log.println(Log.ASSERT, "UsersList", users.value.toString()) + users.value.forEach { user -> + if (user.password == passwordValue && user.email == emailValue) { + currentUserViewModel.setArgument(user.id.toString()) + navController.navigate(route = Graph.passUserId(user.id.toString())) + } + } + } + } + } + } else { + isValid = "Данные не корректны" + } + }, + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 10.dp, + pressedElevation = 6.dp + ), + shape = RoundedCornerShape(15.dp), + colors = ButtonDefaults.buttonColors(containerColor = colorResource(R.color.lightBlue)), + content = { + Text(stringResource(id = R.string.registration_button)) + } + ) + Text( + text = "Уже зарегистрированы?", + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth(), + ) + TextButton( + modifier = Modifier + .fillMaxWidth(), + onClick = { + navController.navigate(route = AuthScreen.Login.route) + }, + content = { + Text( + stringResource(id = R.string.login_heading_text), + color = colorResource(R.color.lightBlue), textDecoration = TextDecoration.Underline) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/ui/theme/Color.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/ui/theme/Color.kt new file mode 100644 index 0000000..7cde7d0 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package ru.ulstu.`is`.airticketrentservice.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/ui/theme/Theme.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/ui/theme/Theme.kt new file mode 100644 index 0000000..78ca2fa --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/ui/theme/Theme.kt @@ -0,0 +1,70 @@ +package ru.ulstu.`is`.airticketrentservice.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun AirTicketRentServiceTheme( + 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 + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/ui/theme/Type.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/ui/theme/Type.kt new file mode 100644 index 0000000..ca1718e --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package ru.ulstu.`is`.airticketrentservice.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/AppViewModelProvider.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/AppViewModelProvider.kt new file mode 100644 index 0000000..d85512e --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/AppViewModelProvider.kt @@ -0,0 +1,74 @@ +package ru.ulstu.`is`.airticketrentservice.viewModel + +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.createSavedStateHandle +import androidx.lifecycle.viewmodel.CreationExtras +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import ru.ulstu.`is`.airticketrentservice.TicketApplication + +object AppViewModelProvider { + val Factory = viewModelFactory { + initializer { + CurrentUserViewModel(ticketApplication().container.userRestRepository) + } + initializer { + LoginViewModel(ticketApplication().container.userRestRepository) + } + initializer { + RegistrationViewModel(ticketApplication().container.userRestRepository) + } + initializer { + FlightListViewModel(ticketApplication().container.flightRestRepository) + } + initializer { + UserListViewModel(ticketApplication().container.userRestRepository) + } + initializer { + TicketListViewModel(ticketApplication().container.ticketRestRepository) + } + initializer { + RentListViewModel(ticketApplication().container.rentRestRepository) + } + initializer { + TicketEditViewModel( + this.createSavedStateHandle(), + ticketApplication().container.ticketRestRepository + ) + } + initializer { + RentEditViewModel( + this.createSavedStateHandle(), + ticketApplication().container.rentRestRepository + ) + } + initializer { + UsersRentsViewModel( + this.createSavedStateHandle(), + ticketApplication().container.userRestRepository, + ticketApplication().container.rentRestRepository + ) + } + initializer { + FindFlightsViewModel( + this.createSavedStateHandle(), + ticketApplication().container.flightRestRepository + ) + } + initializer { + UserEditViewModel( + this.createSavedStateHandle(), + ticketApplication().container.userRestRepository + ) + } + initializer { + FlightEditViewModel( + this.createSavedStateHandle(), + ticketApplication().container.flightRestRepository + ) + } + } +} + +fun CreationExtras.ticketApplication(): TicketApplication = + (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as TicketApplication) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/CurrentUserViewModel.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/CurrentUserViewModel.kt new file mode 100644 index 0000000..858c3af --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/CurrentUserViewModel.kt @@ -0,0 +1,29 @@ +package ru.ulstu.`is`.airticketrentservice.viewModel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.database.models.User +import ru.ulstu.`is`.airticketrentservice.database.repository.UserRepository + +class CurrentUserViewModel(private val userRepository: UserRepository) : ViewModel(){ + val argument = mutableStateOf(null) + private val userid = mutableIntStateOf(0) + var user by mutableStateOf(null) + + fun setArgument(arg: String) { + argument.value = arg + userid.intValue = arg.toInt() + viewModelScope.launch { + user = userRepository.getUserById(userid.intValue) + } + } + + suspend fun updateUser(user: User) { + userRepository.updateUser(user) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/FindFlightsViewModel.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/FindFlightsViewModel.kt new file mode 100644 index 0000000..431a690 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/FindFlightsViewModel.kt @@ -0,0 +1,36 @@ +package ru.ulstu.`is`.airticketrentservice.viewModel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.database.AppContainer +import ru.ulstu.`is`.airticketrentservice.database.models.Flight +import ru.ulstu.`is`.airticketrentservice.database.repository.FlightRepository + +class FindFlightsViewModel( + savedStateHandle: SavedStateHandle, + private val flightRepository: FlightRepository +) : ViewModel() { + private val from: String = checkNotNull(savedStateHandle["direction_from"]) + private val to: String = checkNotNull(savedStateHandle["direction_to"]) + private val departureDate: String = checkNotNull(savedStateHandle["departure_date"]) + + var foundFlightsUiState by mutableStateOf(FoundFlightsUiState()) + private set + + init { + viewModelScope.launch { + foundFlightsUiState = FoundFlightsUiState(flightRepository.findFlights(from, to, departureDate)) + } + } +} + +data class FoundFlightsUiState(val flightList: List = listOf()) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/FlightEditViewModel.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/FlightEditViewModel.kt new file mode 100644 index 0000000..bb45bcb --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/FlightEditViewModel.kt @@ -0,0 +1,104 @@ +package ru.ulstu.`is`.airticketrentservice.viewModel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.database.models.Flight +import ru.ulstu.`is`.airticketrentservice.database.repository.FlightRepository + +class FlightEditViewModel( + savedStateHandle: SavedStateHandle, + private val flightRepository: FlightRepository +) : ViewModel() { + + var flightUiState by mutableStateOf(FlightUiState()) + private set + + private val flightUid: Int = checkNotNull(savedStateHandle["id"]) + + init { + viewModelScope.launch { + if (flightUid > 0) { + flightUiState = flightRepository.getFlightById(flightUid) + .toUiState(true) + } + } + } + + fun updateUiState(flightDetails: FlightDetails) { + flightUiState = FlightUiState( + flightDetails = flightDetails, + isEntryValid = validateInput(flightDetails) + ) + } + + suspend fun saveFlight() { + if (validateInput()) { + if (flightUid > 0) { + flightRepository.updateFlight( + flightUiState.flightDetails.toFlight(flightUid) + ) + } else { + flightRepository.insertFlight( + flightUiState.flightDetails.toFlight() + ) + } + } + } + + private fun validateInput(uiState: FlightDetails = flightUiState.flightDetails): Boolean { + return with(uiState) { + direction_from.isNotBlank() + && direction_to.isNotBlank() + && departure_date.isNotBlank() + && arrival_date.isNotBlank() + && tickets_count > 0 + && one_ticket_cost > 0 + } + } +} + +data class FlightUiState( + val flightDetails: FlightDetails = FlightDetails(), + val isEntryValid: Boolean = false +) + +data class FlightDetails( + val direction_from: String = "", + val direction_to: String = "", + val departure_date: String = "", + val arrival_date: String = "", + val tickets_count: Int = 0, + val one_ticket_cost: Double = 0.0 +) + +fun FlightDetails.toFlight(id: Int = 0): Flight = Flight( + id = id, + direction_from = direction_from, + direction_to = direction_to, + departure_date = departure_date, + arrival_date = arrival_date, + tickets_count = tickets_count, + one_ticket_cost = one_ticket_cost +) + +fun Flight.toDetails(): FlightDetails = FlightDetails( + direction_from = direction_from, + direction_to = direction_to, + departure_date = departure_date, + arrival_date = arrival_date, + tickets_count = tickets_count, + one_ticket_cost = one_ticket_cost +) + +fun Flight.toUiState(isEntryValid: Boolean = false): FlightUiState = FlightUiState( + flightDetails = this.toDetails(), + isEntryValid = isEntryValid +) + diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/FlightListViewModel.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/FlightListViewModel.kt new file mode 100644 index 0000000..222046a --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/FlightListViewModel.kt @@ -0,0 +1,40 @@ +package ru.ulstu.`is`.airticketrentservice.viewModel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.database.models.Flight +import ru.ulstu.`is`.airticketrentservice.database.repository.FlightRepository + +class FlightListViewModel( + private val flightRepository: FlightRepository +): ViewModel() { + + //val foundFlightsUiState: Flow> = flightRepository.findPagingFlights(from, to, departureDate) + var flightsListUiState: Flow> = flightRepository.getFlights() + +// val flightListUiState: StateFlow = flightRepository.getAllFlights().map { +// FlightListUiState(it) +// }.stateIn( +// scope = viewModelScope, +// started = SharingStarted.WhileSubscribed(stopTimeoutMillis = AppContainer.TIMEOUT), +// initialValue = FlightListUiState() +// ) + + +// val _searchResults = MutableStateFlow>(PagingData.empty()) +// val searchResults: MutableStateFlow> = _searchResults +// +// fun searchFlights(from: String, to: String, departureDate: String) { +// viewModelScope.launch { +// val results = flightRepository.findPagingFlights(from, to, departureDate).single() +// _searchResults.value = results +// } +// } + + fun deleteFlight(flight: Flight) = viewModelScope.launch { + flightRepository.deleteFlight(flight) + } +} diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/LoginViewModel.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/LoginViewModel.kt new file mode 100644 index 0000000..cb6e098 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/LoginViewModel.kt @@ -0,0 +1,20 @@ +package ru.ulstu.`is`.airticketrentservice.viewModel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.database.models.User +import ru.ulstu.`is`.airticketrentservice.database.repository.UserRepository + +class LoginViewModel(private val userRepository: UserRepository) : ViewModel() { + + var userList by mutableStateOf>(emptyList()) + fun setUserList() { + viewModelScope.launch { + userList=userRepository.getAllUsers() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/RegistrationViewModel.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/RegistrationViewModel.kt new file mode 100644 index 0000000..165ba0a --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/RegistrationViewModel.kt @@ -0,0 +1,25 @@ +package ru.ulstu.`is`.airticketrentservice.viewModel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.database.models.User +import ru.ulstu.`is`.airticketrentservice.database.repository.UserRepository + +class RegistrationViewModel(private val userRepository: UserRepository) : ViewModel() { + + private val _users = MutableLiveData>() + val users: LiveData> get() = _users + + fun setUserList() { + viewModelScope.launch { + _users.value = userRepository.getAllUsers() + } + } + + suspend fun insertUser(user: User) { + userRepository.insertUser(user) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/RentEditViewModel.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/RentEditViewModel.kt new file mode 100644 index 0000000..d06c5ed --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/RentEditViewModel.kt @@ -0,0 +1,85 @@ +package ru.ulstu.`is`.airticketrentservice.viewModel + +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.database.models.Rent +import ru.ulstu.`is`.airticketrentservice.database.repository.RentRepository + +class RentEditViewModel( + savedStateHandle: SavedStateHandle, + private val rentRepository: RentRepository +) : ViewModel() { + var rentUiState by mutableStateOf(RentUiState()) + private set + + private val rentUid: Int = checkNotNull(savedStateHandle["id"]) + + init { + viewModelScope.launch { + if (rentUid > 0) { + rentUiState = rentRepository.getRentById(rentUid) + .toUiState(true) + } + } + } + + fun updateUiState(rentDetails: RentDetails) { + rentUiState = RentUiState( + rentDetails = rentDetails, + isEntryValid = validateInput(rentDetails) + ) + } + suspend fun saveRent() { + if (validateInput()) { + if (rentUid > 0) { + rentRepository.updateRent(rentUiState.rentDetails.toRent(rentUid)) + } else { + rentRepository.insertRent(rentUiState.rentDetails.toRent()) + } + } + } + + private fun validateInput(uiState: RentDetails = rentUiState.rentDetails): Boolean { + return with(uiState) { + status.isNotBlank() + && userId > 0 + && ticketId > 0 + } + } +} + +data class RentUiState( + val rentDetails: RentDetails = RentDetails(), + val isEntryValid: Boolean = false +) + +data class RentDetails( + val status: String = "Ожидает подтверждения", + val userId: Int = 0, + val ticketId: Int = 0 +) + +fun RentDetails.toRent(id: Int = 0): Rent = Rent( + id = id, + status = status, + userId = userId, + ticketId = ticketId +) + +fun Rent.toDetails(): RentDetails = RentDetails( + status = status, + userId = userId, + ticketId = ticketId +) + +fun Rent.toUiState(isEntryValid: Boolean = false): RentUiState = RentUiState( + rentDetails = this.toDetails(), + isEntryValid = isEntryValid +) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/RentListViewModel.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/RentListViewModel.kt new file mode 100644 index 0000000..130a3a8 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/RentListViewModel.kt @@ -0,0 +1,44 @@ +package ru.ulstu.`is`.airticketrentservice.viewModel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.database.AppContainer +import ru.ulstu.`is`.airticketrentservice.database.models.Rent +import ru.ulstu.`is`.airticketrentservice.database.repository.RentRepository +import ru.ulstu.`is`.airticketrentservice.database.repository.UserRepository + +class RentListViewModel ( + private val rentRepository: RentRepository +) : ViewModel() { + val rentListUiState: Flow> = rentRepository.getRents() + +// private var userId: Int = 0 +// fun setUserId(userId: Int) { +// this.userId = userId +// } +// var userRentsUiState by mutableStateOf(UserRentsUiState()) +// private set +// +// init { +// viewModelScope.launch { +// if (userId > 0) { +// userRentsUiState = UserRentsUiState(userRepository.getUserRents(userId)) +// } +// } +// } + + suspend fun deleteRent(rent: Rent) { + rentRepository.deleteRent(rent) + } +} + diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/TicketEditViewModel.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/TicketEditViewModel.kt new file mode 100644 index 0000000..a60dc4e --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/TicketEditViewModel.kt @@ -0,0 +1,108 @@ +package ru.ulstu.`is`.airticketrentservice.viewModel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.database.models.Ticket +import ru.ulstu.`is`.airticketrentservice.database.repository.TicketRepository + +class TicketEditViewModel( + savedStateHandle: SavedStateHandle, + private val ticketRepository: TicketRepository, +) : ViewModel() { + + var ticketsListUiState by mutableStateOf(TicketsListUiState()) + private set + + var ticketUiState by mutableStateOf(TicketUiState()) + private set + + private val ticketUid: Int = checkNotNull(savedStateHandle["id"]) + + init { + viewModelScope.launch { + ticketsListUiState = TicketsListUiState(ticketRepository.getAllTickets()) + if (ticketUid > 0) { + ticketUiState = ticketRepository.getTicketById(ticketUid) + .toUiState(true) + } + } + } + + fun setCurrentTicket(ticketId: Int) { + val ticket: Ticket? = + ticketsListUiState.ticketList.firstOrNull { ticket -> ticket.id == ticketId } + ticket?.let { updateTicketUiState(it) } + } + + fun updateTicketUiState(ticket: Ticket) { + ticketUiState = TicketUiState( + ticket = ticket + ) + } + + fun updateUiState(ticketDetails: TicketDetails) { + ticketUiState = TicketUiState( + ticketDetails = ticketDetails, + isEntryValid = validateInput(ticketDetails) + ) + } + + suspend fun saveTicket() { + if (validateInput()) { + if (ticketUid > 0) { + ticketRepository.updateTicket( + ticketUiState.ticketDetails.toTicket(ticketUid) + ) + } else { + ticketRepository.insertTicket( + ticketUiState.ticketDetails.toTicket() + ) + } + } + } + + private fun validateInput(uiState: TicketDetails = ticketUiState.ticketDetails): Boolean { + return with(uiState) { + passengers_count > 0 + && ticket_cost > 0 + && flightId > 0 + } + } +} + +data class TicketUiState( + val ticketDetails: TicketDetails = TicketDetails(), + val isEntryValid: Boolean = false, + val ticket: Ticket? = null +) + +data class TicketDetails( + val passengers_count: Int = 0, + var ticket_cost: Double = 0.0, + var flightId: Int = 0, +) + +fun TicketDetails.toTicket(uid: Int = 0): Ticket = Ticket( + id = uid, + passengers_count = passengers_count, + ticket_cost = ticket_cost, + flightId = flightId +) + +fun Ticket.toDetails(): TicketDetails = TicketDetails( + passengers_count = passengers_count, + ticket_cost = ticket_cost, + flightId = flightId +) + +fun Ticket.toUiState(isEntryValid: Boolean = false): TicketUiState = TicketUiState( + ticketDetails = this.toDetails(), + isEntryValid = isEntryValid +) + +data class TicketsListUiState(val ticketList: List = listOf()) diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/TicketListViewModel.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/TicketListViewModel.kt new file mode 100644 index 0000000..1da8709 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/TicketListViewModel.kt @@ -0,0 +1,34 @@ +package ru.ulstu.`is`.airticketrentservice.viewModel + +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.database.models.Ticket +import ru.ulstu.`is`.airticketrentservice.database.repository.TicketRepository + +class TicketListViewModel(private val ticketRepository: TicketRepository): ViewModel() { + val ticketListUiState: Flow> = ticketRepository.getTickets() + +// private var userId: Int = 0 +// fun setUserId(userId: Int) { +// this.userId = userId +// } +// var userRentsUiState by mutableStateOf(UserRentsUiState()) +// private set +// +// init { +// viewModelScope.launch { +// if (userId > 0) { +// userRentsUiState = UserRentsUiState(userRepository.getUserRents(userId)) +// } +// } +// } + + suspend fun deleteTicket(ticket: Ticket) { + ticketRepository.deleteTicket(ticket) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/UserEditViewModel.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/UserEditViewModel.kt new file mode 100644 index 0000000..fa0ae9c --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/UserEditViewModel.kt @@ -0,0 +1,126 @@ +package ru.ulstu.`is`.airticketrentservice.viewModel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.database.models.User +import ru.ulstu.`is`.airticketrentservice.database.repository.UserRepository + +class UserEditViewModel( + savedStateHandle: SavedStateHandle, + private val userRepository: UserRepository +) : ViewModel() { + + var usersListUiState by mutableStateOf(UsersListUiState()) + private set + + var userUiState by mutableStateOf(UserUiState()) + private set + + private val userUid: Int = checkNotNull(savedStateHandle["id"]) + + init { + viewModelScope.launch { + usersListUiState = UsersListUiState(userRepository.getAllUsers()) + if (userUid > 0) { + userUiState = userRepository.getUserById(userUid) + .toUiState(true)!! + } + } + } + + fun setCurrentUser(userId: Int) { + val user: User? = + usersListUiState.userList.firstOrNull { user -> user.id == userId } + user?.let { updateUserUiState(it) } + } + + fun updateUserUiState(user: User) { + userUiState = UserUiState( + user = user + ) + } + + fun updateUiState(userDetails: UserDetails) { + userUiState = UserUiState( + userDetails = userDetails, + isEntryValid = validateInput(userDetails) + ) + } + + suspend fun saveUser() { + if (validateInput()) { + if (userUid > 0) { + userRepository.updateUser(userUiState.userDetails.toUser(userUid)) + } else { + userRepository.insertUser(userUiState.userDetails.toUser()) + } + } + } + + private fun validateInput(uiState: UserDetails = userUiState.userDetails): Boolean { + return with(uiState) { + surname.isNotBlank() + && name.isNotBlank() + && patronymic.isNotBlank() + && date_of_birth.isNotBlank() + && email.isNotBlank() + && password.isNotBlank() + && role.isNotBlank() + } + } +} + +data class UserUiState( + val userDetails: UserDetails = UserDetails(), + val isEntryValid: Boolean = false, + val user: User? = null +) + +data class UserDetails( + val surname: String = "", + val name: String = "", + val patronymic: String = "", + val date_of_birth: String = "", + val email: String = "", + val password: String = "", + val role: String = "" +) + +fun UserDetails.toUser(id: Int = 0): User = User( + id = id, + surname = surname, + name = name, + patronymic = patronymic, + date_of_birth = date_of_birth, + email = email, + password = password, + role = role +) + +fun User.toDetails(): UserDetails? = patronymic?.let { + UserDetails( + surname = surname, + name = name, + patronymic = it, + date_of_birth = date_of_birth, + email = email, + password = password, + role = role + ) +} + +fun User.toUiState(isEntryValid: Boolean = false): UserUiState? = this.toDetails()?.let { + UserUiState( + userDetails = it, + isEntryValid = isEntryValid + ) +} + +data class UsersListUiState(val userList: List = listOf()) diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/UserListViewModel.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/UserListViewModel.kt new file mode 100644 index 0000000..1df9e37 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/UserListViewModel.kt @@ -0,0 +1,33 @@ +package ru.ulstu.`is`.airticketrentservice.viewModel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.database.models.Flight +import ru.ulstu.`is`.airticketrentservice.database.models.User +import ru.ulstu.`is`.airticketrentservice.database.repository.UserRepository + +class UserListViewModel( + private val userRepository: UserRepository +) : ViewModel() { + + var userListUiState by mutableStateOf(UserListUiState()) + private set + + init { + viewModelScope.launch { + userListUiState = UserListUiState(userRepository.getAllUsers()) + } + } + + suspend fun deleteUser(user: User) { + userRepository.deleteUser(user) + } +} + +data class UserListUiState(val userList: List = listOf()) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/UsersRentsViewModel.kt b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/UsersRentsViewModel.kt new file mode 100644 index 0000000..f0ba511 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/airticketrentservice/viewModel/UsersRentsViewModel.kt @@ -0,0 +1,38 @@ +package ru.ulstu.`is`.airticketrentservice.viewModel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import ru.ulstu.`is`.airticketrentservice.database.models.Rent +import ru.ulstu.`is`.airticketrentservice.database.repository.RentRepository +import ru.ulstu.`is`.airticketrentservice.database.repository.UserRepository + +class UsersRentsViewModel( + savedStateHandle: SavedStateHandle, + private val userRepository: UserRepository, + private val rentRepository: RentRepository +) : ViewModel() { + + var userRentsUiState by mutableStateOf(UserRentsUiState()) + private set + + private val userUid: Int = checkNotNull(savedStateHandle["userId"]) + + init { + viewModelScope.launch { + if (userUid > 0) { + userRentsUiState = UserRentsUiState(userRepository.getUserRents(userUid)) + } + } + } + + suspend fun deleteRent(rent: Rent) { + rentRepository.deleteRent(rent) + } +} + +data class UserRentsUiState(val rentList: List = listOf()) \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/logo.png b/app/src/main/res/drawable/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7635bfbea8f6aef7703d4c92ba3a5b91831191f7 GIT binary patch literal 56284 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 + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..c209e78ecd372343283f4157dcfd918ec5165bb3 GIT binary patch literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9 GIT binary patch literal 2898 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..4f0f1d64e58ba64d180ce43ee13bf9a17835fbca GIT binary patch literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..948a3070fe34c611c42c0d3ad3013a0dce358be0 GIT binary patch literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f GIT binary patch literal 3918 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�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} 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

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..3d6cee7 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,13 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + #6AABC8 + #e7f0f5 + #fcfcff + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..f5bee6d --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,65 @@ + + AirTicketRentService + --> + Редактировать + Бронирование + Нет бронирования + Пользователь + Дата бронирования + Статус бронирования + Билет + Сохранить + Сохранить + Рейс не указан + Пользователь не указан + Записи о пользователях отсутствуют + Записи о билетах отсутствуют + Записи о бронированиях отсутствуют + Откуда + Куда + Дата вылета + Дата прилёта + Стоимость + ФИО + Дата рождения + Имя пользователя + Пароль + Место + Редактировать + Подтвердить + Вход + user@mail.ru + Password + Фамилия + Имя + Петров + Павел + Подтверждение пароля + Войти + Дата рождения + 2000-01-01 + Зарегистрироваться + Логин + Пароль + Билеты + Бронирования + Поиск авиабилетов + Регистрация + Выбрать даты + Фильтр + Поиск + Личный кабинет + Мои документы + Мои бронирования + Создать бронь + Написать в поддержку + "Главная" + "Профиль" + "Для администратора" + "" + "" + "" + "" + "" + "" + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..fe0620b --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +