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 0000000..7635bfb
Binary files /dev/null and b/app/src/main/res/drawable/logo.png differ
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ 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 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
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 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
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 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
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 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
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 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
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 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..fa0f996
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml
new file mode 100644
index 0000000..402382a
--- /dev/null
+++ b/app/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,6 @@
+
+
+
+ 192.168.1.100
+
+
\ No newline at end of file
diff --git a/app/src/test/java/ru/ulstu/is/airticketrentservice/ExampleUnitTest.kt b/app/src/test/java/ru/ulstu/is/airticketrentservice/ExampleUnitTest.kt
new file mode 100644
index 0000000..aa7eaa8
--- /dev/null
+++ b/app/src/test/java/ru/ulstu/is/airticketrentservice/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package ru.ulstu.`is`.airticketrentservice
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..04a426d
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,7 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ id("com.android.application") version "8.1.1" apply false
+ id("org.jetbrains.kotlin.android") version "1.8.10" apply false
+ id("com.google.devtools.ksp") version "1.8.20-1.0.11" apply false
+ id("org.jetbrains.kotlin.plugin.serialization") version "1.8.20" apply false
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..3c5031e
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..a793f91
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sun Dec 17 20:08:07 GMT+04:00 2023
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/server/data.json b/server/data.json
new file mode 100644
index 0000000..abb9337
--- /dev/null
+++ b/server/data.json
@@ -0,0 +1,134 @@
+{
+ "tickets": [
+ {
+ "id": 1,
+ "passengers_count": 1,
+ "ticket_cost": 1000,
+ "flightId": 2
+ }
+ ],
+ "flights": [
+ {
+ "id": 2,
+ "direction_from": "a",
+ "direction_to": "b",
+ "departure_date": "1-1-2024",
+ "arrival_date": "1-1-2024",
+ "tickets_count": 100,
+ "one_ticket_cost": 1000
+ },
+ {
+ "direction_from": "ульск",
+ "direction_to": "мск",
+ "departure_date": "24-12-2023",
+ "arrival_date": "24-12-2023",
+ "tickets_count": 20,
+ "one_ticket_cost": 2000,
+ "id": 3
+ },
+ {
+ "direction_from": "Санкт-Петербург ",
+ "direction_to": "Сочи",
+ "departure_date": "17-12-2023",
+ "arrival_date": "17-12-2023",
+ "tickets_count": 320,
+ "one_ticket_cost": 2435,
+ "id": 5
+ },
+ {
+ "direction_from": "Ульяновск ",
+ "direction_to": "Москва ",
+ "departure_date": "26-12-2023",
+ "arrival_date": "26-12-2023",
+ "tickets_count": 40,
+ "one_ticket_cost": 2680,
+ "id": 6
+ },
+ {
+ "direction_from": "Хабаровск ",
+ "direction_to": "Кострома ",
+ "departure_date": "5-1-2024",
+ "arrival_date": "5-1-2024",
+ "tickets_count": 200,
+ "one_ticket_cost": 6870,
+ "id": 7
+ },
+ {
+ "direction_from": "Астрахань ",
+ "direction_to": "Нижний Новгород",
+ "departure_date": "28-1-2024",
+ "arrival_date": "28-1-2024",
+ "tickets_count": 68,
+ "one_ticket_cost": 8720,
+ "id": 8
+ },
+ {
+ "direction_from": "Крым",
+ "direction_to": "Мурманск",
+ "departure_date": "27-1-2024",
+ "arrival_date": "28-1-2024",
+ "tickets_count": 100,
+ "one_ticket_cost": 17800,
+ "id": 9
+ },
+ {
+ "direction_from": "Вологда",
+ "direction_to": "Астана",
+ "departure_date": "14-1-2024",
+ "arrival_date": "15-1-2024",
+ "tickets_count": 200,
+ "one_ticket_cost": 10800,
+ "id": 10
+ }
+ ],
+ "users": [
+ {
+ "id": 2,
+ "surname": "Артамонова",
+ "name": "Татьяна",
+ "patronymic": "Валерьевна",
+ "date_of_birth": "7-11-2003",
+ "email": "usertt@mail.ru",
+ "password": "usertt",
+ "role": "user"
+ },
+ {
+ "surname": "а",
+ "name": "а",
+ "patronymic": "а",
+ "date_of_birth": "17-5-2000",
+ "email": "user@mail.ru",
+ "password": "user",
+ "role": "user",
+ "id": 3
+ },
+ {
+ "surname": "ыщылы",
+ "name": "шчшчгч",
+ "patronymic": "шчшчшч",
+ "date_of_birth": "10-12-2015",
+ "email": "aaa@mail.ru",
+ "password": "aaa",
+ "role": "user",
+ "id": 4
+ },
+ {
+ "surname": "ыщылы",
+ "name": "шчшчгч",
+ "patronymic": "шчшчшч",
+ "date_of_birth": "10-12-2015",
+ "email": "aa@mail.ru",
+ "password": "aaa",
+ "role": "user",
+ "id": 5
+ }
+ ],
+ "rents": [
+ {
+ "id": 1,
+ "status": "Ожидает подтверждения",
+ "userId": 2,
+ "ticketId": 1
+ }
+ ]
+}
\ No newline at end of file
diff --git a/server/data.json.bak b/server/data.json.bak
new file mode 100644
index 0000000..38fafe5
--- /dev/null
+++ b/server/data.json.bak
@@ -0,0 +1,134 @@
+{
+ "tickets": [
+ {
+ "id": 1,
+ "passengers_count": 1,
+ "ticket_cost": 1000,
+ "flightId": 2
+ }
+ ],
+ "flights": [
+ {
+ "id": 2,
+ "direction_from": "a",
+ "direction_to": "b",
+ "departure_date": "1-1-2024",
+ "arrival_date": "1-1-2024",
+ "tickets_count": 100,
+ "one_ticket_cost": 1000
+ },
+ {
+ "direction_from": "ульск",
+ "direction_to": "мск",
+ "departure_date": "24-12-2023",
+ "arrival_date": "24-12-2023",
+ "tickets_count": 20,
+ "one_ticket_cost": 2000,
+ "id": 3
+ },
+ {
+ "direction_from": "Санкт-Петербург ",
+ "direction_to": "Сочи",
+ "departure_date": "17-12-2023",
+ "arrival_date": "17-12-2023",
+ "tickets_count": 320,
+ "one_ticket_cost": 2435,
+ "id": 5
+ },
+ {
+ "direction_from": "Ульяновск ",
+ "direction_to": "Москва ",
+ "departure_date": "26-12-2023",
+ "arrival_date": "26-12-2023",
+ "tickets_count": 40,
+ "one_ticket_cost": 2680,
+ "id": 6
+ },
+ {
+ "direction_from": "Хабаровск ",
+ "direction_to": "Кострома ",
+ "departure_date": "5-1-2024",
+ "arrival_date": "5-1-2024",
+ "tickets_count": 200,
+ "one_ticket_cost": 6870,
+ "id": 7
+ },
+ {
+ "direction_from": "Астрахань ",
+ "direction_to": "Нижний Новгород",
+ "departure_date": "28-1-2024",
+ "arrival_date": "28-1-2024",
+ "tickets_count": 68,
+ "one_ticket_cost": 8720,
+ "id": 8
+ },
+ {
+ "direction_from": "Крым",
+ "direction_to": "Мурманск",
+ "departure_date": "27-1-2024",
+ "arrival_date": "28-1-2024",
+ "tickets_count": 100,
+ "one_ticket_cost": 17800,
+ "id": 9
+ },
+ {
+ "direction_from": "Вологда",
+ "direction_to": "Астана",
+ "departure_date": "14-1-2024",
+ "arrival_date": "15-1-2024",
+ "tickets_count": 200,
+ "one_ticket_cost": 10800,
+ "id": 10
+ }
+ ],
+ "users": [
+ {
+ "id": 2,
+ "surname": "Артамонова",
+ "name": "Татьяна",
+ "patronymic": "Валерьевна",
+ "date_of_birth": "7-11-2003",
+ "email": "usertt@mail.ru",
+ "password": "usertt",
+ "role": "user"
+ },
+ {
+ "surname": "а",
+ "name": "а",
+ "patronymic": "а",
+ "date_of_birth": "17-5-2000",
+ "email": "user@mail.ru",
+ "password": "user",
+ "role": "user",
+ "id": 3
+ },
+ {
+ "surname": "ыщылы",
+ "name": "шчшчгч",
+ "patronymic": "шчшчшч",
+ "date_of_birth": "10-12-2015",
+ "email": "aaa@mail.ru",
+ "password": "aaa",
+ "role": "user",
+ "id": 4
+ },
+ {
+ "surname": "ыщылы",
+ "name": "шчшчгч",
+ "patronymic": "шчшчшч",
+ "date_of_birth": "10-12-2015",
+ "email": "aaa@mail.ru",
+ "password": "aaa",
+ "role": "user",
+ "id": 5
+ }
+ ],
+ "rents": [
+ {
+ "id": 1,
+ "status": "Ожидает подтверждения",
+ "userId": 2,
+ "ticketId": 1
+ }
+ ]
+}
\ No newline at end of file
diff --git a/server/package-lock.json b/server/package-lock.json
new file mode 100644
index 0000000..0203f7f
--- /dev/null
+++ b/server/package-lock.json
@@ -0,0 +1,1335 @@
+{
+ "name": "fake-db",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "fake-db",
+ "version": "1.0.0",
+ "devDependencies": {
+ "json-server": "0.17.4"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "dev": true,
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "dev": true
+ },
+ "node_modules/basic-auth": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "5.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
+ "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
+ "dev": true,
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.11.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
+ "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.1",
+ "set-function-length": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "dev": true,
+ "dependencies": {
+ "mime-db": ">= 1.43.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/compression": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
+ "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
+ "dev": true,
+ "dependencies": {
+ "accepts": "~1.3.5",
+ "bytes": "3.0.0",
+ "compressible": "~2.0.16",
+ "debug": "2.6.9",
+ "on-headers": "~1.0.2",
+ "safe-buffer": "5.1.2",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/compression/node_modules/bytes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+ "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/connect-pause": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/connect-pause/-/connect-pause-0.1.1.tgz",
+ "integrity": "sha512-a1gSWQBQD73krFXdUEYJom2RTFrWUL3YvXDCRkyv//GVXc79cdW9MngtRuN9ih4FDKBtfJAJId+BbDuX+1rh2w==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-disposition/node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+ "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "dev": true
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "dev": true,
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
+ "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.1",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "dev": true
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/errorhandler": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.5.1.tgz",
+ "integrity": "sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A==",
+ "dev": true,
+ "dependencies": {
+ "accepts": "~1.3.7",
+ "escape-html": "~1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "dev": true
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.18.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
+ "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
+ "dev": true,
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.1",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.5.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.2.0",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.11.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.18.0",
+ "serve-static": "1.15.0",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/express-urlrewrite": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/express-urlrewrite/-/express-urlrewrite-1.4.0.tgz",
+ "integrity": "sha512-PI5h8JuzoweS26vFizwQl6UTF25CAHSggNv0J25Dn/IKZscJHWZzPrI5z2Y2jgOzIaw2qh8l6+/jUcig23Z2SA==",
+ "dev": true,
+ "dependencies": {
+ "debug": "*",
+ "path-to-regexp": "^1.0.3"
+ }
+ },
+ "node_modules/express-urlrewrite/node_modules/path-to-regexp": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
+ "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
+ "dev": true,
+ "dependencies": {
+ "isarray": "0.0.1"
+ }
+ },
+ "node_modules/express/node_modules/body-parser": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
+ "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
+ "dev": true,
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.11.0",
+ "raw-body": "2.5.1",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/express/node_modules/raw-body": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
+ "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
+ "dev": true,
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/express/node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/finalhandler": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
+ "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
+ "dev": true,
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
+ "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
+ "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+ "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
+ "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "dev": true,
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
+ "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
+ "dev": true
+ },
+ "node_modules/isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
+ "dev": true
+ },
+ "node_modules/jju": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz",
+ "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==",
+ "dev": true
+ },
+ "node_modules/json-parse-helpfulerror": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz",
+ "integrity": "sha512-XgP0FGR77+QhUxjXkwOMkC94k3WtqEBfcnjWqhRd82qTat4SWKRE+9kUnynz/shm3I4ea2+qISvTIeGTNU7kJg==",
+ "dev": true,
+ "dependencies": {
+ "jju": "^1.1.0"
+ }
+ },
+ "node_modules/json-server": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/json-server/-/json-server-0.17.4.tgz",
+ "integrity": "sha512-bGBb0WtFuAKbgI7JV3A864irWnMZSvBYRJbohaOuatHwKSRFUfqtQlrYMrB6WbalXy/cJabyjlb7JkHli6dYjQ==",
+ "dev": true,
+ "dependencies": {
+ "body-parser": "^1.19.0",
+ "chalk": "^4.1.2",
+ "compression": "^1.7.4",
+ "connect-pause": "^0.1.1",
+ "cors": "^2.8.5",
+ "errorhandler": "^1.5.1",
+ "express": "^4.17.1",
+ "express-urlrewrite": "^1.4.0",
+ "json-parse-helpfulerror": "^1.0.3",
+ "lodash": "^4.17.21",
+ "lodash-id": "^0.14.1",
+ "lowdb": "^1.0.0",
+ "method-override": "^3.0.0",
+ "morgan": "^1.10.0",
+ "nanoid": "^3.1.23",
+ "please-upgrade-node": "^3.2.0",
+ "pluralize": "^8.0.0",
+ "server-destroy": "^1.0.1",
+ "yargs": "^17.0.1"
+ },
+ "bin": {
+ "json-server": "lib/cli/bin.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true
+ },
+ "node_modules/lodash-id": {
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/lodash-id/-/lodash-id-0.14.1.tgz",
+ "integrity": "sha512-ikQPBTiq/d5m6dfKQlFdIXFzvThPi2Be9/AHxktOnDSfSxE1j9ICbBT5Elk1ke7HSTgM38LHTpmJovo9/klnLg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/lowdb": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz",
+ "integrity": "sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.3",
+ "is-promise": "^2.1.0",
+ "lodash": "4",
+ "pify": "^3.0.0",
+ "steno": "^0.4.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==",
+ "dev": true
+ },
+ "node_modules/method-override": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz",
+ "integrity": "sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==",
+ "dev": true,
+ "dependencies": {
+ "debug": "3.1.0",
+ "methods": "~1.1.2",
+ "parseurl": "~1.3.2",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/method-override/node_modules/debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "dev": true,
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/morgan": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
+ "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
+ "dev": true,
+ "dependencies": {
+ "basic-auth": "~2.0.1",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-finished": "~2.3.0",
+ "on-headers": "~1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/morgan/node_modules/on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
+ "dev": true,
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+ "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
+ "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dev": true,
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
+ "dev": true
+ },
+ "node_modules/pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/please-upgrade-node": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
+ "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==",
+ "dev": true,
+ "dependencies": {
+ "semver-compare": "^1.0.0"
+ }
+ },
+ "node_modules/pluralize": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
+ "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "dev": true,
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+ "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+ "dev": true,
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "dev": true,
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "node_modules/semver-compare": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
+ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
+ "dev": true
+ },
+ "node_modules/send": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+ "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "dev": true,
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
+ },
+ "node_modules/serve-static": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
+ "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+ "dev": true,
+ "dependencies": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.18.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/server-destroy": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz",
+ "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==",
+ "dev": true
+ },
+ "node_modules/set-function-length": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
+ "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
+ "dev": true,
+ "dependencies": {
+ "define-data-property": "^1.1.1",
+ "get-intrinsic": "^1.2.1",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "dev": true
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/steno": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz",
+ "integrity": "sha512-EEHMVYHNXFHfGtgjNITnka0aHhiAlo93F7z2/Pwd+g0teG9CnM3JIINM7hVVB5/rhw9voufD7Wukwgtw2uqh6w==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.3"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dev": true,
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ }
+ }
+}
diff --git a/server/package.json b/server/package.json
new file mode 100644
index 0000000..0fb3100
--- /dev/null
+++ b/server/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "fake-db",
+ "version": "1.0.0",
+ "scripts": {
+ "start": "json-server --watch data.json --host 0.0.0.0 -p 8079"
+ },
+ "dependencies": {
+ },
+ "devDependencies": {
+ "json-server": "0.17.4"
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..25def7a
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,18 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "AirTicketRentService"
+include(":app")
+
\ No newline at end of file