diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index 47f8e00..d9f8f30 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -10,6 +10,14 @@ diff --git a/.idea/misc.xml b/.idea/misc.xml index 0ad17cb..8978d23 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a69613c..4e4ba3b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,7 +1,9 @@ plugins { alias(libs.plugins.android.application) + id("com.google.gms.google-services") alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.kotlinx.serialization) + id("com.apollographql.apollo3").version("3.8.3") } android { @@ -50,8 +52,13 @@ android { } } -dependencies { +apollo { + service("service") { + packageName.set("com.example.mpdriver") + } +} +dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) @@ -76,4 +83,8 @@ dependencies { implementation(libs.kotlinx.datetime) implementation(libs.yandex.maps) implementation(libs.kotlin.coroutines) + implementation(platform("com.google.firebase:firebase-bom:33.1.1")) + implementation("com.apollographql.apollo3:apollo-runtime:3.8.3") + implementation("ru.gildor.coroutines:kotlin-coroutines-okhttp:1.0") + } \ No newline at end of file diff --git a/app/src/main/graphql/fetchApplicationData.graphql b/app/src/main/graphql/fetchApplicationData.graphql new file mode 100644 index 0000000..15c1fff --- /dev/null +++ b/app/src/main/graphql/fetchApplicationData.graphql @@ -0,0 +1,58 @@ +query fetchApplicationData ($userID: String!) { + tasks(userId: $userID) { + id + startPln + endPln + startFact + endFact + status + taskType + text + + events { + id + } + + subtasks { + id + } + + route { + temperatureProperty + name + + trailer { + gost + } + + truck { + gost + } + } + } + subtasks(userId: $userID) { + id + startPln + endPln + startFact + endFact + status + taskType + text + station { + name + location { + lat + lon + } + } + } + + notes(userId: $userID) { + id + taskId + noteStatus + tip + text + } +} \ No newline at end of file diff --git a/app/src/main/graphql/getActiveSubtaskID.graphql b/app/src/main/graphql/getActiveSubtaskID.graphql new file mode 100644 index 0000000..b81b5d9 --- /dev/null +++ b/app/src/main/graphql/getActiveSubtaskID.graphql @@ -0,0 +1,7 @@ +query GetActiveSubtaskID($userID: String!) { + task(userId: $userID, isActive: true) { + activeSubtask { + id + } + } +} \ No newline at end of file diff --git a/app/src/main/graphql/getActiveTaskId.graphql b/app/src/main/graphql/getActiveTaskId.graphql new file mode 100644 index 0000000..f44ee01 --- /dev/null +++ b/app/src/main/graphql/getActiveTaskId.graphql @@ -0,0 +1,15 @@ +query GetActiveTaskId($userID: String!) { + task(isActive: true, userId: $userID) { + id + } + completedTasks: tasks(userId: $userID, isCompleted: true) { + id + startPln + } + + plannedTasks: tasks(userId: $userID, isPlanned: true) { + id, + startPln + } + +} \ No newline at end of file diff --git a/app/src/main/graphql/getPlannedTasksIDs.graphql b/app/src/main/graphql/getPlannedTasksIDs.graphql new file mode 100644 index 0000000..9605f26 --- /dev/null +++ b/app/src/main/graphql/getPlannedTasksIDs.graphql @@ -0,0 +1,8 @@ +query GetPlannedTasksIDs($userID: String!) { + tasks(userId: $userID, isPlanned:true) { + id + subtasks { + id + } + } +} \ No newline at end of file diff --git a/app/src/main/graphql/getSubtaskByID.graphql b/app/src/main/graphql/getSubtaskByID.graphql new file mode 100644 index 0000000..57ef600 --- /dev/null +++ b/app/src/main/graphql/getSubtaskByID.graphql @@ -0,0 +1,9 @@ +query GetSubtaskByID($userID: String!, $subtaskID: String!) { + subtask(userId: $userID, subtaskId: $subtaskID) { + id + text + startPln + endPln + status + } +} \ No newline at end of file diff --git a/app/src/main/graphql/getTaskById.graphql b/app/src/main/graphql/getTaskById.graphql new file mode 100644 index 0000000..8c603dd --- /dev/null +++ b/app/src/main/graphql/getTaskById.graphql @@ -0,0 +1,20 @@ +query GetTaskById($taskID: String!, $userID: String!) { + task(userId: $userID, taskId: $taskID) { + status + route { + name + temperatureProperty + truck { + gost + } + trailer { + gost + } + } + text + startPln + endPln + startFact + endFact + } +} \ No newline at end of file diff --git a/app/src/main/graphql/getTasksFeedScreen.graphql b/app/src/main/graphql/getTasksFeedScreen.graphql new file mode 100644 index 0000000..531d09b --- /dev/null +++ b/app/src/main/graphql/getTasksFeedScreen.graphql @@ -0,0 +1,7 @@ +query GetTasksFeedScreen($userID: String!){ + tasks(userId: $userID) { + id + text + + } +} \ No newline at end of file diff --git a/app/src/main/graphql/schema.graphqls b/app/src/main/graphql/schema.graphqls new file mode 100644 index 0000000..48f3d54 --- /dev/null +++ b/app/src/main/graphql/schema.graphqls @@ -0,0 +1,453 @@ +type Query { + tasks(userId: String!, isPlanned: Boolean = false, isCompleted: Boolean = false): [AppTaskQL!]! + + task(userId: String!, taskId: String = null, isActive: Boolean = null): AppTaskQL + + countPlannedTasks(userId: String!): Int! + + countCompletedTasks(userId: String!): Int! + + notes(userId: String!): [AppNoteQL!]! + + subtask(userId: String!, subtaskId: String!): SubtaskQL + + subtasks(userId: String!): [SubtaskQL!]! +} + +type AppTaskQL { + activeSubtask: SubtaskQL + + id: String! + + profileId: Int! + + startPln: DateTime! + + endPln: DateTime! + + startFact: DateTime + + endFact: DateTime + + status: StatusEnumQl! + + taskType: String! + + text: String! + + events: [AppEventQL!]! + + subtasks: [SubtaskQL!]! + + route: AppRouteQL! +} + +type SubtaskQL { + id: String! + + startPln: DateTime! + + endPln: DateTime! + + startFact: DateTime + + endFact: DateTime + + status: StatusEnumQl! + + taskType: String! + + text: String! + + station: MSTQL +} + +""" +The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. +""" +scalar String + +""" +Date with time (isoformat) +""" +scalar DateTime + +enum StatusEnumQl { + CANCELLED + + IN_PROGRESS + + COMPLETED + + NOT_DEFINED +} + +type MSTQL { + name: String! + + location: LocationQL! +} + +type LocationQL { + lat: Float! + + lon: Float! +} + +""" +The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point). +""" +scalar Float + +""" +The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. +""" +scalar Int + +type AppEventQL { + id: String! + + type: String! + + text: String! + + eventDatetime: DateTime! +} + +type AppRouteQL { + temperatureProperty: MarshTemperaturePropertyQL! + + name: String! + + trailer: TRSQL + + truck: TRSQL +} + +enum MarshTemperaturePropertyQL { + HOT + + COLD + + UNDEFINED +} + +type TRSQL { + gost: String +} + +""" +The `Boolean` scalar type represents `true` or `false`. +""" +scalar Boolean + +type AppNoteQL { + id: String! + + userId: String! + + taskId: String! + + noteStatus: Int! + + tip: Int! + + text: String! +} + +""" +A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations. +""" +type __Schema { + description: String + + """ + A list of all types supported by this server. + """ + types: [__Type!]! + + """ + The type that query operations will be rooted at. + """ + queryType: __Type! + + """ + If this server supports mutation, the type that mutation operations will be rooted at. + """ + mutationType: __Type + + """ + If this server support subscription, the type that subscription operations will be rooted at. + """ + subscriptionType: __Type + + """ + A list of all directives supported by this server. + """ + directives: [__Directive!]! +} + +""" +The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum. + +Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByURL`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types. +""" +type __Type { + kind: __TypeKind! + + name: String + + description: String + + specifiedByURL: String + + fields(includeDeprecated: Boolean = false): [__Field!] + + interfaces: [__Type!] + + possibleTypes: [__Type!] + + enumValues(includeDeprecated: Boolean = false): [__EnumValue!] + + inputFields(includeDeprecated: Boolean = false): [__InputValue!] + + ofType: __Type + + isOneOf: Boolean +} + +""" +An enum describing what kind of type a given `__Type` is. +""" +enum __TypeKind { + """ + Indicates this type is a scalar. + """ + SCALAR + + """ + Indicates this type is an object. `fields` and `interfaces` are valid fields. + """ + OBJECT + + """ + Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields. + """ + INTERFACE + + """ + Indicates this type is a union. `possibleTypes` is a valid field. + """ + UNION + + """ + Indicates this type is an enum. `enumValues` is a valid field. + """ + ENUM + + """ + Indicates this type is an input object. `inputFields` is a valid field. + """ + INPUT_OBJECT + + """ + Indicates this type is a list. `ofType` is a valid field. + """ + LIST + + """ + Indicates this type is a non-null. `ofType` is a valid field. + """ + NON_NULL +} + +""" +Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type. +""" +type __Field { + name: String! + + description: String + + args(includeDeprecated: Boolean = false): [__InputValue!]! + + type: __Type! + + isDeprecated: Boolean! + + deprecationReason: String +} + +""" +Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value. +""" +type __InputValue { + name: String! + + description: String + + type: __Type! + + """ + A GraphQL-formatted string representing the default value for this input value. + """ + defaultValue: String + + isDeprecated: Boolean! + + deprecationReason: String +} + +""" +One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string. +""" +type __EnumValue { + name: String! + + description: String + + isDeprecated: Boolean! + + deprecationReason: String +} + +""" +A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. + +In some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor. +""" +type __Directive { + name: String! + + description: String + + isRepeatable: Boolean! + + locations: [__DirectiveLocation!]! + + args(includeDeprecated: Boolean = false): [__InputValue!]! +} + +""" +A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies. +""" +enum __DirectiveLocation { + """ + Location adjacent to a query operation. + """ + QUERY + + """ + Location adjacent to a mutation operation. + """ + MUTATION + + """ + Location adjacent to a subscription operation. + """ + SUBSCRIPTION + + """ + Location adjacent to a field. + """ + FIELD + + """ + Location adjacent to a fragment definition. + """ + FRAGMENT_DEFINITION + + """ + Location adjacent to a fragment spread. + """ + FRAGMENT_SPREAD + + """ + Location adjacent to an inline fragment. + """ + INLINE_FRAGMENT + + """ + Location adjacent to a variable definition. + """ + VARIABLE_DEFINITION + + """ + Location adjacent to a schema definition. + """ + SCHEMA + + """ + Location adjacent to a scalar definition. + """ + SCALAR + + """ + Location adjacent to an object type definition. + """ + OBJECT + + """ + Location adjacent to a field definition. + """ + FIELD_DEFINITION + + """ + Location adjacent to an argument definition. + """ + ARGUMENT_DEFINITION + + """ + Location adjacent to an interface definition. + """ + INTERFACE + + """ + Location adjacent to a union definition. + """ + UNION + + """ + Location adjacent to an enum definition. + """ + ENUM + + """ + Location adjacent to an enum value definition. + """ + ENUM_VALUE + + """ + Location adjacent to an input object type definition. + """ + INPUT_OBJECT + + """ + Location adjacent to an input object field definition. + """ + INPUT_FIELD_DEFINITION +} + +""" +Directs the executor to include this field or fragment only when the `if` argument is true. +""" +directive @include ("Included when true." if: Boolean!) on FIELD|FRAGMENT_SPREAD|INLINE_FRAGMENT + +""" +Directs the executor to skip this field or fragment when the `if` argument is true. +""" +directive @skip ("Skipped when true." if: Boolean!) on FIELD|FRAGMENT_SPREAD|INLINE_FRAGMENT + +""" +Marks an element of a GraphQL schema as no longer supported. +""" +directive @deprecated ("Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https:\/\/commonmark.org\/)." reason: String = "No longer supported") on FIELD_DEFINITION|ARGUMENT_DEFINITION|INPUT_FIELD_DEFINITION|ENUM_VALUE + +""" +Exposes a URL that specifies the behaviour of this scalar. +""" +directive @specifiedBy ("The URL that specifies the behaviour of this scalar." url: String!) on SCALAR + +schema { + query: Query +} diff --git a/app/src/main/java/com/example/mpdriver/MainActivity.kt b/app/src/main/java/com/example/mpdriver/MainActivity.kt index 9aaca82..5920f19 100644 --- a/app/src/main/java/com/example/mpdriver/MainActivity.kt +++ b/app/src/main/java/com/example/mpdriver/MainActivity.kt @@ -3,6 +3,7 @@ package com.example.mpdriver import android.content.Intent import android.content.IntentFilter import android.os.Bundle +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -22,6 +23,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -30,6 +32,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.sharp.Settings import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -37,9 +40,11 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton 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.Modifier @@ -49,6 +54,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource 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.navigation.NavHostController @@ -58,9 +64,16 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.example.mpdriver.api.Api +import com.example.mpdriver.api.TaskApi +import com.example.mpdriver.api.apolloClient +import com.example.mpdriver.api.fetchAppDataToDB + + +import com.example.mpdriver.api.toJson import com.example.mpdriver.recievers.TimeTickReciever import com.example.mpdriver.screens.Feed import com.example.mpdriver.screens.MapScreen +import com.example.mpdriver.screens.NoteScreen import com.example.mpdriver.screens.PhoneCodeInputScreen import com.example.mpdriver.screens.PhoneInputScreen import com.example.mpdriver.screens.SubtaskScreen @@ -68,6 +81,8 @@ import com.example.mpdriver.screens.TasksList import com.example.mpdriver.storage.Database import com.tencent.mmkv.MMKV import com.yandex.mapkit.MapKitFactory +import kotlinx.coroutines.launch +import okhttp3.Dispatcher class MainActivity : ComponentActivity() { @@ -79,6 +94,8 @@ class MainActivity : ComponentActivity() { MapKitFactory.setApiKey("f4385b18-0740-454a-a71f-d20da7e8fc3b") MapKitFactory.initialize(this) val rootDir = MMKV.initialize(this) + Log.d("MMKV.KEYS", Database.allKeys!!.joinToString()) + println("mmkv root: $rootDir") registerReceiver(timeTickReciever, IntentFilter(Intent.ACTION_TIME_TICK)) @@ -123,7 +140,32 @@ fun MainNavigator() { mutableStateOf(false) } + var loadingData by remember { + mutableStateOf(true) + } + + val bottomNavController = rememberNavController() + + val api = TaskApi(LocalContext.current) + + LaunchedEffect(Unit) { + api.send_task_status_chains(onResponse = { + Database.dropUpdates() + }) + apolloClient.fetchAppDataToDB() + loadingData = false + } + + if (loadingData) + return Row(Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) { + Column(Modifier.fillMaxWidth().padding(horizontal = 16.dp), horizontalAlignment = Alignment.CenterHorizontally) { + CircularProgressIndicator(color = Color(0xFFE5332A)) + Text( text = "Происходит обновление даннных, Пожалуйста, подождите.", fontSize = 16.sp, textAlign = TextAlign.Center) + } + } + + Scaffold( topBar = { Header( @@ -132,9 +174,11 @@ fun MainNavigator() { backLink = backLink ) }, - bottomBar = { Footer( - hostController = bottomNavController - ) }, + bottomBar = { + Footer( + hostController = bottomNavController + ) + }, ) { @@ -145,14 +189,19 @@ fun MainNavigator() { ) { composable("feed", exitTransition = { - slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Left, tween(200)) + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Left, + tween(200) + ) } - ) { + ) { backLink = false headerTitle = "Лента" - Box(modifier = Modifier - .fillMaxWidth() - .fillMaxHeight()) { + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { Feed(hostController = bottomNavController) } } @@ -160,15 +209,18 @@ fun MainNavigator() { enterTransition = { slideIntoContainer( - AnimatedContentTransitionScope.SlideDirection.Left, tween(200)) + AnimatedContentTransitionScope.SlideDirection.Left, tween(200) + ) } - ) { + ) { backLink = true headerTitle = "Задачи" - Box(modifier = Modifier - .fillMaxWidth() - .fillMaxHeight()) { + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { TasksList(hostController = bottomNavController) } } @@ -176,7 +228,11 @@ fun MainNavigator() { backLink = false headerTitle = "События" - Box(modifier = Modifier.fillMaxWidth().fillMaxHeight()) { + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { Text(text = "События") } @@ -184,31 +240,43 @@ fun MainNavigator() { composable("settings") { backLink = false headerTitle = "Настройки" - Box(modifier = Modifier.fillMaxWidth().fillMaxHeight()) { + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { Text(text = "Profile") } } composable("notifications") { backLink = false headerTitle = "Уведомления" - Box(modifier = Modifier.fillMaxWidth().fillMaxHeight()) { - Text(text = "Notifications") + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { + NoteScreen() } } composable("chat") { backLink = false headerTitle = "Чат" - Box(modifier = Modifier.fillMaxWidth().fillMaxHeight()) { + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { Text(text = "Chat") } } composable("map", enterTransition = { - fadeIn() + fadeIn() }, exitTransition = { - ExitTransition.None - }) { + ExitTransition.None + }) { backLink = false headerTitle = "Карта" MapScreen() @@ -218,20 +286,23 @@ fun MainNavigator() { }), enterTransition = { slideIntoContainer( - AnimatedContentTransitionScope.SlideDirection.Left, tween(200)) + AnimatedContentTransitionScope.SlideDirection.Left, tween(200) + ) } - ) {bse -> - bse.arguments?.let {args -> + ) { bse -> + bse.arguments?.let { args -> headerTitle = "Детали задачи" backLink = true - Box(modifier = Modifier - .fillMaxWidth() - .fillMaxHeight()){ + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { SubtaskScreen(args.getLong("taskId")) } return@composable } - + } } @@ -300,14 +371,19 @@ data class FooterNavMenuLabel( ) @Composable -fun Footer(modifier: Modifier = Modifier, - hostController: NavHostController +fun Footer( + modifier: Modifier = Modifier, + hostController: NavHostController ) { val menuItems = mapOf( "feed" to FooterNavMenuLabel("Лента", R.drawable.home_default, R.drawable.home), "events" to FooterNavMenuLabel("События", R.drawable.calendar_default, R.drawable.calendar), - "notifications" to FooterNavMenuLabel("Уведомления", R.drawable.bell_default, R.drawable.bell), + "notifications" to FooterNavMenuLabel( + "Уведомления", + R.drawable.bell_default, + R.drawable.bell + ), "chat" to FooterNavMenuLabel("Чат", R.drawable.chat_default, R.drawable.chat), "map" to FooterNavMenuLabel("Карта", R.drawable.location_default, R.drawable.location) ) @@ -337,17 +413,23 @@ fun Footer(modifier: Modifier = Modifier, onClick = { activeRoute = it.key hostController.navigate(it.key) - }, + }, colors = ButtonDefaults.textButtonColors( contentColor = Color.Black, ) ) { - Column (horizontalAlignment = Alignment.CenterHorizontally) { - if (activeRoute== it.key) { - Image(painter = painterResource(id = it.value.imageActive), contentDescription = "") + Column(horizontalAlignment = Alignment.CenterHorizontally) { + if (activeRoute == it.key) { + Image( + painter = painterResource(id = it.value.imageActive), + contentDescription = "" + ) Text(text = it.value.title, fontSize = 11.sp, color = Color.Black) } else { - Image(painter = painterResource(id = it.value.defaultImage), contentDescription = "") + Image( + painter = painterResource(id = it.value.defaultImage), + contentDescription = "" + ) Text(text = it.value.title, fontSize = 11.sp, color = Color.Gray) } diff --git a/app/src/main/java/com/example/mpdriver/api/Api.kt b/app/src/main/java/com/example/mpdriver/api/Api.kt index 21cd6d8..8208dbe 100644 --- a/app/src/main/java/com/example/mpdriver/api/Api.kt +++ b/app/src/main/java/com/example/mpdriver/api/Api.kt @@ -18,6 +18,7 @@ import okhttp3.Response import okhttp3.internal.EMPTY_REQUEST import java.io.IOException import java.lang.reflect.Type +import java.util.concurrent.TimeUnit data class Langs( @@ -41,11 +42,10 @@ inline fun Response.parseList(): List { open class Api(val ctx: Context) { - val kv = MMKV.defaultMMKV() val clientCheckAuth = OkHttpClient.Builder().build() - val client = OkHttpClient.Builder() + val client = OkHttpClient.Builder().connectTimeout(60, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS).writeTimeout(30 ,TimeUnit.SECONDS) .addInterceptor { chain -> val req = chain.request().newBuilder() .addHeader("Authorization", "Bearer ${Database.access_token}") @@ -55,7 +55,7 @@ open class Api(val ctx: Context) { .build() - val BASE_URL = "http://147.45.107.119:8000/api/v1" + val BASE_URL = "http://192.168.0.101:8000/api/v1" fun performRequest(clientReq: Request, errorHandler: (Exception) -> Unit = {}, handler: (Response) -> Unit) { client.newCall(clientReq).enqueue(object : Callback { diff --git a/app/src/main/java/com/example/mpdriver/api/Task.kt b/app/src/main/java/com/example/mpdriver/api/Task.kt index e836f3a..65a2a85 100644 --- a/app/src/main/java/com/example/mpdriver/api/Task.kt +++ b/app/src/main/java/com/example/mpdriver/api/Task.kt @@ -1,7 +1,9 @@ package com.example.mpdriver.api import android.content.Context +import android.provider.ContactsContract.Data import android.util.Log +import com.example.mpdriver.storage.CreateUpdateTaskData import com.example.mpdriver.storage.Database import com.google.gson.annotations.SerializedName import kotlinx.coroutines.Dispatchers @@ -9,9 +11,15 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import okhttp3.Call +import okhttp3.Callback import okhttp3.MediaType.Companion.toMediaType import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import ru.gildor.coroutines.okhttp.await +import java.io.IOException +import kotlin.math.log data class SetTaskStatusRequest( @@ -20,80 +28,110 @@ data class SetTaskStatusRequest( data class TaskResponse( - @SerializedName("id" ) var id: Long? = null, - @SerializedName("startPln" ) var startPln: String? = null, - @SerializedName("endPln" ) var endPln: String? = null, - @SerializedName("startFact" ) var startFact: String? = null, - @SerializedName("endFact" ) var endFact: String? = null, - @SerializedName("status" ) var status: String? = null, - @SerializedName("taskType" ) var taskType: String? = null, - @SerializedName("text" ) var text: String? = null, - @SerializedName("events" ) var events: ArrayList = arrayListOf(), - @SerializedName("subtasks" ) var subtasks: ArrayList = arrayListOf(), - @SerializedName("route" ) var route: Route? = Route() + @SerializedName("id") var id: Long? = null, + @SerializedName("startPln") var startPln: String? = null, + @SerializedName("endPln") var endPln: String? = null, + @SerializedName("startFact") var startFact: String? = null, + @SerializedName("endFact") var endFact: String? = null, + @SerializedName("status") var status: String? = null, + @SerializedName("taskType") var taskType: String? = null, + @SerializedName("text") var text: String? = null, + @SerializedName("events") var events: ArrayList = arrayListOf(), + @SerializedName("subtasks") var subtasks: ArrayList = arrayListOf(), + @SerializedName("route") var route: Route? = Route() ) -data class Events ( +data class Events( - @SerializedName("id" ) var id : Long? = null, - @SerializedName("type" ) var type : String? = null, - @SerializedName("text" ) var text : String? = null, - @SerializedName("eventDatetime" ) var eventDatetime : String? = null + @SerializedName("id") var id: Long? = null, + @SerializedName("type") var type: String? = null, + @SerializedName("text") var text: String? = null, + @SerializedName("eventDatetime") var eventDatetime: String? = null ) -data class Location ( +data class Location( - @SerializedName("lat" ) var lat : Double? = null, - @SerializedName("lon" ) var lon : Double? = null + @SerializedName("lat") var lat: Double? = null, + @SerializedName("lon") var lon: Double? = null ) -data class Station ( +data class Station( - @SerializedName("id" ) var id : Long? = null, - @SerializedName("name" ) var name : String? = null, - @SerializedName("location" ) var location : Location? = Location() + @SerializedName("id") var id: Long? = null, + @SerializedName("name") var name: String? = null, + @SerializedName("location") var location: Location? = Location() ) -data class Subtasks ( +data class Subtasks( - @SerializedName("id" ) var id : Long? = null, - @SerializedName("parentId" ) var parentId : Long? = null, - @SerializedName("startPln" ) var startPln : String? = null, - @SerializedName("endPln" ) var endPln : String? = null, - @SerializedName("startFact" ) var startFact : String? = null, - @SerializedName("endFact" ) var endFact : String? = null, - @SerializedName("status" ) var status : String? = null, - @SerializedName("taskType" ) var taskType : String? = null, - @SerializedName("text" ) var text : String? = null, - @SerializedName("station" ) var station : Station? = Station() + @SerializedName("id") var id: Long? = null, + @SerializedName("parentId") var parentId: Long? = null, + @SerializedName("startPln") var startPln: String? = null, + @SerializedName("endPln") var endPln: String? = null, + @SerializedName("startFact") var startFact: String? = null, + @SerializedName("endFact") var endFact: String? = null, + @SerializedName("status") var status: String? = null, + @SerializedName("taskType") var taskType: String? = null, + @SerializedName("text") var text: String? = null, + @SerializedName("station") var station: Station? = Station() ) -data class TRS ( +data class TRS( - @SerializedName("id" ) var id : Long? = null, - @SerializedName("gost" ) var gost : String? = null + @SerializedName("id") var id: Long? = null, + @SerializedName("gost") var gost: String? = null ) -data class Route ( +data class Route( - @SerializedName("id" ) var id : Long? = null, - @SerializedName("temperatureProperty" ) var temperatureProperty : Int? = null, - @SerializedName("name" ) var name : String? = null, - @SerializedName("trailer" ) var trailer : TRS? = null, - @SerializedName("truck" ) var truck : TRS? = null + @SerializedName("id") var id: Long? = null, + @SerializedName("temperatureProperty") var temperatureProperty: Int? = null, + @SerializedName("name") var name: String? = null, + @SerializedName("trailer") var trailer: TRS? = null, + @SerializedName("truck") var truck: TRS? = null ) -class TaskApi(ctx: Context): Api(ctx) { +class TaskApi(ctx: Context) : Api(ctx) { - suspend fun getPlannedTasksApiCall(errorHandler: (Exception)-> Unit = {}, handler: (List) -> Unit) { + data class SendTaskStatusReq( + val data: List + ) + + fun send_task_status_chains(onResponse: (Response) -> Unit = {}, onFailure: () -> Unit = {}) { + val chainBody = SendTaskStatusReq(Database.updateTasks).toJson().toRequestBody() + val req = Request.Builder().url("$BASE_URL/tasks").post(chainBody).build() + client.newCall(req).enqueue( + object : Callback { + override fun onFailure(call: Call, e: IOException) { + Log.d("taskAPI", "${e}") + onFailure() + } + + override fun onResponse(call: Call, response: Response) { + Log.d("taskAPI", response.body.toString()) + if (response.isSuccessful) { + return onResponse(response) + } + return onFailure() + + } + + } + ) + } + + suspend fun getPlannedTasksApiCall( + errorHandler: (Exception) -> Unit = {}, + handler: (List) -> Unit + ) { val req = Request.Builder() .url("$BASE_URL/tasks/planned") .build() @@ -103,21 +141,24 @@ class TaskApi(ctx: Context): Api(ctx) { } } - fun getPlannedTasks(errorHandler: (Exception)-> Unit = {}, handler: (List) -> Unit) { - runBlocking { - launch(Dispatchers.IO) { - getPlannedTasksApiCall { - println(Database.planned_tasks) - println(it) - Database.planned_tasks = it - println(Database.planned_tasks) - } - } - handler(Database.planned_tasks) - } - } +// fun getPlannedTasks(errorHandler: (Exception)-> Unit = {}, handler: (List) -> Unit) { +// runBlocking { +// launch(Dispatchers.IO) { +// getPlannedTasksApiCall { +// println(Database.planned_tasks) +// println(it) +// Database.planned_tasks = it +// println(Database.planned_tasks) +// } +// } +// handler(Database.planned_tasks) +// } +// } - suspend fun getActiveTaskApiCall(errorHandler: (Exception)-> Unit = {}, handler: (TaskResponse) -> Unit) { + suspend fun getActiveTaskApiCall( + errorHandler: (Exception) -> Unit = {}, + handler: (TaskResponse) -> Unit + ) { val req = Request.Builder() .url("$BASE_URL/tasks/active") .build() @@ -126,21 +167,24 @@ class TaskApi(ctx: Context): Api(ctx) { handler(it.parse()) } } - fun getActiveTask(errorHandler: (Exception)-> Unit = {}, handler: (TaskResponse) -> Unit) { - runBlocking { - launch(Dispatchers.IO) { - getAllTasksForUser() - } +// fun getActiveTask(errorHandler: (Exception)-> Unit = {}, handler: (TaskResponse) -> Unit) { +// runBlocking { +// launch(Dispatchers.IO) { +// getAllTasksForUser() +// } +// +// val data = Database.tasks.filter { it.status == "InProgress" } +// if (data.count() >= 1) { +// return@runBlocking handler(data[0]) +// } +// handler(TaskResponse()) +// } +// } - val data = Database.tasks.filter { it.status == "InProgress" } - if (data.count() >= 1) { - return@runBlocking handler(data[0]) - } - handler(TaskResponse()) - } - } - - fun getCompletedTask(errorHandler: (Exception)-> Unit = {}, handler: (List) -> Unit) { + fun getCompletedTask( + errorHandler: (Exception) -> Unit = {}, + handler: (List) -> Unit + ) { val req = Request.Builder() .url("$BASE_URL/tasks/completed") .build() @@ -149,7 +193,12 @@ class TaskApi(ctx: Context): Api(ctx) { handler(it.parseList()) } } - fun getSubtasks(taskId: Long, errorHandler: (Exception)-> Unit = {}, handler: (List) -> Unit) { + + fun getSubtasks( + taskId: Long, + errorHandler: (Exception) -> Unit = {}, + handler: (List) -> Unit + ) { val req = Request.Builder() .url("$BASE_URL/tasks/$taskId/subtasks") .build() @@ -158,7 +207,12 @@ class TaskApi(ctx: Context): Api(ctx) { handler(it.parseList()) } } - fun getEvents(taskId: Long, errorHandler: (Exception)-> Unit = {}, handler: (List) -> Unit) { + + fun getEvents( + taskId: Long, + errorHandler: (Exception) -> Unit = {}, + handler: (List) -> Unit + ) { val req = Request.Builder() .url("$BASE_URL/tasks/$taskId/events") .build() @@ -168,24 +222,24 @@ class TaskApi(ctx: Context): Api(ctx) { } } - fun getAllTasksForUser() { - val req = Request.Builder().url("$BASE_URL/tasks").build() - performRequest(req) {res -> - val data = res.parseList() - Database.tasks = data - } - } +// fun getAllTasksForUser() { +// val req = Request.Builder().url("$BASE_URL/tasks").build() +// performRequest(req) {res -> +// val data = res.parseList() +// Database.tasks = data +// } +// } - fun setTaskStatusInProgress(taskId: Long, errorHandler: (Exception)-> Unit = {}, handler: (TaskResponse) -> Unit) { - - val body = SetTaskStatusRequest(taskId).toJson().toRequestBody("application/json".toMediaType()) - - val req = Request.Builder() - .url("$BASE_URL/tasks/active") - .post(body) - - performRequest(req.build(), errorHandler) { - handler(it.parse()) - } - } +// fun setTaskStatusInProgress(taskId: Long, errorHandler: (Exception)-> Unit = {}, handler: (TaskResponse) -> Unit) { +// +// val body = SetTaskStatusRequest(taskId).toJson().toRequestBody("application/json".toMediaType()) +// +// val req = Request.Builder() +// .url("$BASE_URL/tasks/active") +// .post(body) +// +// performRequest(req.build(), errorHandler) { +// handler(it.parse()) +// } +// } } \ No newline at end of file diff --git a/app/src/main/java/com/example/mpdriver/api/graphql.kt b/app/src/main/java/com/example/mpdriver/api/graphql.kt new file mode 100644 index 0000000..d2280de --- /dev/null +++ b/app/src/main/java/com/example/mpdriver/api/graphql.kt @@ -0,0 +1,31 @@ +package com.example.mpdriver.api +import androidx.compose.ui.platform.LocalContext +import com.apollographql.apollo3.ApolloClient +import com.apollographql.apollo3.network.http.DefaultHttpEngine +import com.example.mpdriver.FetchApplicationDataQuery +import com.example.mpdriver.storage.Database +import com.tencent.mmkv.MMKV + + +val apolloClient = ApolloClient.Builder().serverUrl("http://192.168.0.101:8000/graphql").httpEngine(DefaultHttpEngine(timeoutMillis = 60000)).build() + + +suspend fun ApolloClient.fetchAppDataToDB(): Unit { + val kv = Database.kv + + val req = this.query(FetchApplicationDataQuery("1125904232173609")).execute().data + + req?.let { + for (task in req.tasks) { + kv.encode("task:${task.id}", task.toJson()) + } + for (subtask in req.subtasks) { + kv.encode("subtask:${subtask.id}", subtask.toJson()) + } + for (note in req.notes) { + kv.encode("note:${note.id}", note.toJson()) + } + } + +} + diff --git a/app/src/main/java/com/example/mpdriver/components/Subtask.kt b/app/src/main/java/com/example/mpdriver/components/Subtask.kt index bcba9ce..9ac92d8 100644 --- a/app/src/main/java/com/example/mpdriver/components/Subtask.kt +++ b/app/src/main/java/com/example/mpdriver/components/Subtask.kt @@ -1,5 +1,6 @@ package com.example.mpdriver.components +import android.util.Log import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -17,6 +18,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Notifications @@ -39,13 +41,20 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -54,9 +63,17 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import com.apollographql.apollo3.api.ApolloResponse +import com.apollographql.apollo3.api.toInput +import com.example.mpdriver.GetSubtaskByIDQuery +import com.example.mpdriver.GetTaskByIdQuery import com.example.mpdriver.R import com.example.mpdriver.api.Subtasks +import com.example.mpdriver.api.apolloClient import com.example.mpdriver.recievers.TimeTickReciever +import com.example.mpdriver.storage.CreateUpdateTaskData +import com.example.mpdriver.storage.Database +import com.example.mpdriver.type.StatusEnumQl import kotlinx.coroutines.MainScope import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch @@ -64,10 +81,13 @@ import kotlinx.coroutines.runBlocking import kotlinx.datetime.Clock import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone import kotlinx.datetime.UtcOffset import kotlinx.datetime.asTimeZone import kotlinx.datetime.format.byUnicodePattern +import kotlinx.datetime.format.char import kotlinx.datetime.toInstant +import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.until import kotlin.math.abs @@ -76,7 +96,7 @@ import kotlin.math.abs fun Subtask( modifier: Modifier = Modifier, children: @Composable () -> Unit = {}, - subtask: Subtasks = Subtasks(1, startPln = "2024-06-01T00:00", endPln = "2024-06-10T00:00"), + subtaskID: Long = 0, footerButton: @Composable () -> Unit = {} ) { @@ -91,13 +111,39 @@ fun Subtask( var nowTime by remember { mutableStateOf(Clock.System.now()) } + var subtaskResponse by remember { + mutableStateOf(null) + } + + LaunchedEffect(Unit) { + + val sbtDB = Database.subtasks.find { it.id == subtaskID.toString() } + + sbtDB?.let { + subtaskResponse = GetSubtaskByIDQuery.Subtask( + sbtDB.id, + sbtDB.text, + startPln = sbtDB.startPln, + endPln = sbtDB.endPln, + status = sbtDB.status + ) + + } +// subtaskResponse = apolloClient.query(GetSubtaskByIDQuery("1125904232173609", subtaskID.toString())).execute() + } TimeTickReciever.registerHandler { nowTime = Clock.System.now() } - val startPln = LocalDateTime.parse(subtask.startPln!!) - val endPln = LocalDateTime.parse(subtask.endPln!!) + var startPln: LocalDateTime = Clock.System.now().toLocalDateTime(TimeZone.UTC) + var endPln: LocalDateTime = Clock.System.now().toLocalDateTime(TimeZone.UTC) + + subtaskResponse?.let { + startPln = LocalDateTime.parse(subtaskResponse?.startPln.toString()) + endPln = LocalDateTime.parse(subtaskResponse?.endPln.toString()) + } + val dateFormat = LocalDateTime.Format { byUnicodePattern("d.MM.yyyy") @@ -119,10 +165,10 @@ fun Subtask( val status = when { - subtask.status == "Completed" && !isDelay -> TaskStatus.SUCCESS - subtask.status == "Completed" && isDelay -> TaskStatus.WARNING - subtask.status == "Cancelled" -> TaskStatus.WARNING - subtask.status == "InProgress" || subtask.status == "NotDefined" && isDelay -> TaskStatus.DANGER + subtaskResponse?.status == StatusEnumQl.COMPLETED && !isDelay -> TaskStatus.SUCCESS + subtaskResponse?.status == StatusEnumQl.COMPLETED && isDelay -> TaskStatus.WARNING + subtaskResponse?.status == StatusEnumQl.CANCELLED -> TaskStatus.WARNING + (subtaskResponse?.status == StatusEnumQl.IN_PROGRESS || subtaskResponse?.status == StatusEnumQl.NOT_DEFINED) && isDelay -> TaskStatus.DANGER else -> TaskStatus.DEFAULT } @@ -151,7 +197,7 @@ fun Subtask( ) { Text( modifier = Modifier.weight(1f), - text = "${subtask.text}", + text = "${subtaskResponse?.text}", fontWeight = FontWeight.Bold, fontSize = 20.sp ) @@ -259,8 +305,8 @@ fun Subtask( } } - if (isActionVisible) { - IsSubtaskCompletedAction(setStateAction = { isActionVisible = false }, subtask) + if (isActionVisible && subtaskResponse != null) { + IsSubtaskCompletedAction(setStateAction = { isActionVisible = false }, subtaskResponse!!) } } @@ -268,7 +314,10 @@ fun Subtask( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun IsSubtaskCompletedAction(setStateAction: (Boolean) -> Unit = { }, subtask: Subtasks) { +fun IsSubtaskCompletedAction( + setStateAction: (Boolean) -> Unit = { }, + subtask: GetSubtaskByIDQuery.Subtask +) { var currentStep by remember { mutableStateOf(0) } @@ -318,6 +367,10 @@ fun IsSubtaskCompletedAction(setStateAction: (Boolean) -> Unit = { }, subtask: S composable("success") { SuccessStep(subtask = subtask, controller = actionController) title = "Когда вы выполнили подзадачу?" + isFullScreen = true + LaunchedEffect(sheetState) { + sheetState.expand() + } } composable("failure") { isFullScreen = true @@ -339,13 +392,13 @@ fun IsSubtaskCompletedAction(setStateAction: (Boolean) -> Unit = { }, subtask: S @Composable -fun InitialStep(subtask: Subtasks, controller: NavHostController) { +fun InitialStep(subtask: GetSubtaskByIDQuery.Subtask, controller: NavHostController) { Column { JDEButton(type = ButtonType.SUCCESS, onClick = { controller.navigate("success") }) { Text(text = "Подзадача выполнена", fontSize = 15.sp, fontWeight = FontWeight.Bold) } Spacer(modifier = Modifier.height(10.dp)) - JDEButton(type = ButtonType.WARNING, onClick = {controller.navigate("failure")}) { + JDEButton(type = ButtonType.WARNING, onClick = { controller.navigate("failure") }) { Text(text = "У меня возникла проблема", fontSize = 15.sp, fontWeight = FontWeight.Bold) } Spacer(modifier = Modifier.height(40.dp)) @@ -354,10 +407,9 @@ fun InitialStep(subtask: Subtasks, controller: NavHostController) { } -@Preview(showBackground = true) @Composable fun SuccessStep( - subtask: Subtasks = Subtasks(), + subtask: GetSubtaskByIDQuery.Subtask, controller: NavHostController = rememberNavController() ) { @@ -402,26 +454,96 @@ fun SuccessStep( Spacer(modifier = Modifier.height(10.dp)) Row(Modifier.fillMaxWidth()) { - TextInput(modifier = Modifier.weight(.66f), date, { date = it }, "Дата") + TextInput(modifier = Modifier.weight(.66f), date, { + date = it.filter { it.isDigit() } + }, "Дата") Spacer(modifier = Modifier.width(10.dp)) - TextInput(modifier = Modifier.weight(.33f), time, { date = time }, "Время") + TextInput( + modifier = Modifier.weight(.33f), + time, + { time = it.filter { it.isDigit() } }, + "Время" + ) } } Spacer(modifier = Modifier.height(20.dp)) - ActiveButton(onClick = { /*TODO*/ }, text = "Сохранить", modifier = Modifier.fillMaxWidth()) + ActiveButton(onClick = { + val task = Database.tasks.find { + it.subtasks.find { sbt -> + sbt.id == subtask.id + } != null + } + + val dt = LocalDateTime.parse(date + "T" + time, LocalDateTime.Format { + dayOfMonth() + monthNumber() + year() + char('T') + hour() + minute() + }).toInstant(TimeZone.UTC).toEpochMilliseconds() + + task?.let { + Database.createUpdateTaskDataLocally( + CreateUpdateTaskData( + subtask.id.toLong(), + dt = dt, + status = "Completed" + ) + ) + + if (subtask.id == task.subtasks.last().id) { + Database.createUpdateTaskDataLocally( + CreateUpdateTaskData( + task.id.toLong(), + dt = dt, + status = "Completed" + ) + ) + return@let + } + val curr_sbt_idx = task.subtasks.indexOf(task.subtasks.find { it.id == subtask.id }) + + Database.createUpdateTaskDataLocally( + CreateUpdateTaskData( + task.subtasks[curr_sbt_idx + 1].id.toLong(), + dt, + status = "InProgress" + ) + ) + + } + + + }, text = "Сохранить", modifier = Modifier.fillMaxWidth()) } } +fun transformText(it: String): String { + var digits = it.filter { it.isDigit() } + if (digits.length > 8) { + digits = digits.slice(0..<8) + } + + var data = when (digits.length) { + in 0..2 -> "${digits}" + in 3..4 -> "${digits.substring(0..<2)}.${digits.substring(2)}" + in 5..8 -> "${digits.substring(0..<2)}.${digits.substring(2..<4)}.${digits.substring(4)}" + else -> digits + } + println(data) + return data + +} @OptIn(ExperimentalMaterial3Api::class) -@Preview(showBackground = true) @Composable fun FailureStep( - subtask: Subtasks = Subtasks(), + subtask: GetSubtaskByIDQuery.Subtask, controller: NavHostController = rememberNavController() ) { @@ -440,7 +562,8 @@ fun FailureStep( color = Color.Gray ) Spacer(modifier = Modifier.height(40.dp)) - TextField(value = failureDesk, + TextField( + value = failureDesk, onValueChange = { failureDesk = it }, shape = RoundedCornerShape(10.dp), modifier = Modifier.fillMaxWidth(), @@ -466,10 +589,12 @@ fun FailureStep( focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent ), - trailingIcon = {Icon( - painter = painterResource(id = R.drawable.calendar_default), - contentDescription = "" - )} + trailingIcon = { + Icon( + painter = painterResource(id = R.drawable.calendar_default), + contentDescription = "" + ) + } ) Spacer(modifier = Modifier.height(10.dp)) @@ -485,15 +610,19 @@ fun FailureStep( focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent ), - trailingIcon = {Icon(painter = painterResource(id = R.drawable.calendar_default), - contentDescription = "" - )} + trailingIcon = { + Icon( + painter = painterResource(id = R.drawable.calendar_default), + contentDescription = "" + ) + } ) Spacer(modifier = Modifier.height(20.dp)) ActiveButton(onClick = { /*TODO*/ }, text = "Отправить", modifier = Modifier.fillMaxWidth()) } } +@OptIn(ExperimentalComposeUiApi::class) @Preview(showBackground = true) @Composable fun TextInput( @@ -507,6 +636,8 @@ fun TextInput( mutableStateOf(true) } + val kbController = LocalSoftwareKeyboardController.current + BasicTextField( modifier = modifier @@ -515,7 +646,13 @@ fun TextInput( }, value = value, onValueChange = onValueChange, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions(onDone = { + kbController?.hide() + }), decorationBox = { Box( Modifier @@ -525,16 +662,16 @@ fun TextInput( .padding(horizontal = 10.dp, vertical = 5.dp) ) { Text( - modifier = Modifier.align(if (isFocused) Alignment.TopStart else Alignment.CenterStart), + modifier = Modifier.align(if (isFocused || value != "") Alignment.TopStart else Alignment.CenterStart), text = placeholder, fontWeight = FontWeight.Normal, - fontSize = if (isFocused) 13.sp else 18.sp, + fontSize = if (isFocused || value != "") 13.sp else 18.sp, color = Color.Gray ) - if (isFocused) { + if (isFocused || value != "") { Text( modifier = Modifier.align(Alignment.BottomStart), - text = value, + text = transformText(value), fontSize = 18.sp, fontWeight = FontWeight.Bold ) diff --git a/app/src/main/java/com/example/mpdriver/components/TaskComponent.kt b/app/src/main/java/com/example/mpdriver/components/TaskComponent.kt index 22b2870..c172b84 100644 --- a/app/src/main/java/com/example/mpdriver/components/TaskComponent.kt +++ b/app/src/main/java/com/example/mpdriver/components/TaskComponent.kt @@ -14,12 +14,14 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.KeyboardArrowUp import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton 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 @@ -32,10 +34,13 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.example.mpdriver.GetTaskByIdQuery import com.example.mpdriver.R -import com.example.mpdriver.api.Subtasks -import com.example.mpdriver.api.TaskResponse +import com.example.mpdriver.api.apolloClient import com.example.mpdriver.recievers.TimeTickReciever +import com.example.mpdriver.storage.Database +import com.example.mpdriver.type.MarshTemperaturePropertyQL +import com.example.mpdriver.type.StatusEnumQl import kotlinx.datetime.Clock import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.LocalDateTime @@ -46,7 +51,6 @@ import kotlinx.datetime.toInstant import kotlinx.datetime.until import kotlin.math.abs - enum class TaskStatus { DANGER, SUCCESS, @@ -58,8 +62,47 @@ enum class TaskStatus { @Composable fun Task(modifier: Modifier = Modifier, children: @Composable() ()->Unit = {}, - taskResponse: TaskResponse = TaskResponse(1, "2024-06-01T00:00", "2024-06-10T00:00"), + task_id: Long = 1, footerButton: @Composable ()-> Unit = {}) { + var taskResponse by remember { + mutableStateOf(null) + } + LaunchedEffect(Unit) { + + val dbTask = Database.tasks.find { it.id == task_id.toString() } + var task: GetTaskByIdQuery.Task + + dbTask?.let { + + val trailer = when(dbTask.route.trailer) { + null -> null + else -> GetTaskByIdQuery.Trailer(gost = dbTask.route.trailer.gost) + } + val truck = when(dbTask.route.truck) { + null -> null + else -> GetTaskByIdQuery.Truck(gost = dbTask.route.truck.gost) + } + + val route = GetTaskByIdQuery.Route(name = dbTask.route.name, dbTask.route.temperatureProperty, truck, trailer) + task = GetTaskByIdQuery.Task(status = dbTask.status, route, text = dbTask.text, startPln = dbTask.startPln, endPln = dbTask.endPln, startFact = dbTask.startFact, endFact = dbTask.endFact) + taskResponse = GetTaskByIdQuery.Data(task) + + return@LaunchedEffect + } + + taskResponse = apolloClient.query(GetTaskByIdQuery(task_id.toString(), "1125904232173609")).execute().data + + } + + val dateFormat = LocalDateTime.Format { + byUnicodePattern("d.MM.yyyy") + } + + val timeFormat = LocalDateTime.Format { + byUnicodePattern("HH:mm") + } + + var expanded by remember { mutableStateOf(false) } @@ -72,205 +115,208 @@ fun Task(modifier: Modifier = Modifier, nowTime = Clock.System.now() } - val startPln = LocalDateTime.parse(taskResponse.startPln!!) - val endPln = LocalDateTime.parse(taskResponse.endPln!!) + taskResponse?.let { + val startPln = LocalDateTime.parse(it.task!!.startPln.toString()) + val endPln = LocalDateTime.parse(it.task.endPln.toString()) + val leastBase = nowTime.until( + endPln.toInstant(offset = UtcOffset(3)), + timeZone = UtcOffset(3).asTimeZone(), unit = DateTimeUnit.MINUTE) - val dateFormat = LocalDateTime.Format { - byUnicodePattern("d.MM.yyyy") - } - - val timeFormat = LocalDateTime.Format { - byUnicodePattern("HH:mm") - } - - val leastBase = nowTime.until( - endPln.toInstant(offset = UtcOffset(3)), - timeZone = UtcOffset(3).asTimeZone(), unit = DateTimeUnit.MINUTE) - - val isDelay = leastBase != abs(leastBase) - val leastDays = abs( leastBase) / 60 / 24 - val leastHours = abs(leastBase) / 60 - leastDays * 24 - val leastMinutes = abs(leastBase) - leastDays * 60 * 24 - leastHours * 60 + val isDelay = leastBase != abs(leastBase) + val leastDays = abs( leastBase) / 60 / 24 + val leastHours = abs(leastBase) / 60 - leastDays * 24 + val leastMinutes = abs(leastBase) - leastDays * 60 * 24 - leastHours * 60 - val status = when { - taskResponse.status == "Completed" && !isDelay -> TaskStatus.SUCCESS - taskResponse.status == "Completed" && isDelay -> TaskStatus.WARNING - taskResponse.status == "Cancelled" -> TaskStatus.WARNING - taskResponse.status == "InProgress" || taskResponse.status == "NotDefined" && isDelay -> TaskStatus.DANGER - else -> TaskStatus.DEFAULT - } + val status = when { + it.task.status == StatusEnumQl.COMPLETED && !isDelay -> TaskStatus.SUCCESS + it.task.status == StatusEnumQl.COMPLETED && isDelay -> TaskStatus.WARNING + it.task.status == StatusEnumQl.CANCELLED -> TaskStatus.WARNING + (it.task.status == StatusEnumQl.IN_PROGRESS || it.task.status == StatusEnumQl.NOT_DEFINED) && isDelay -> TaskStatus.DANGER + else -> TaskStatus.DEFAULT + } - - Column( - modifier - .border( - 2.dp, when (status) { - TaskStatus.DEFAULT -> Color.Gray - TaskStatus.SUCCESS -> Color(0xFF45900B) - TaskStatus.DANGER -> Color(0xFFE5332A) - TaskStatus.WARNING -> Color(0xFFFFC700) - }, RoundedCornerShape(10.dp) - ) - .fillMaxWidth() - .padding(horizontal = 15.dp, vertical = 15.dp) - ) { - Row( - Modifier - .fillMaxWidth() - .padding(bottom = 15.dp), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - modifier = Modifier.weight(1f), - text = "Движение по маршруту\n${taskResponse.route?.name}", - fontWeight = FontWeight.Bold, - fontSize = 20.sp - ) - Spacer(modifier = Modifier.width(10.dp)) - - IconButton(onClick = { /*TODO*/ }, modifier = Modifier.weight(0.1f)) { - Image( - painter = painterResource(id = R.drawable.tick_default), - contentDescription = "tick" + return Column( + modifier + .border( + 2.dp, when (status) { + TaskStatus.DEFAULT -> Color.Gray + TaskStatus.SUCCESS -> Color(0xFF45900B) + TaskStatus.DANGER -> Color(0xFFE5332A) + TaskStatus.WARNING -> Color(0xFFFFC700) + }, RoundedCornerShape(10.dp) ) - } - } - Row(Modifier.padding(bottom = 20.dp)) { - InformationPlaceholderSmall( - Modifier.weight(1f), - mainText = "${taskResponse.route?.truck?.gost}", - subText = "Номер ТС" - ) - Spacer(modifier = Modifier.width(10.dp)) - InformationPlaceholderSmall( - Modifier.weight(1f), - mainText = "${if (taskResponse.route!!.trailer != null) taskResponse.route!!.trailer!!.gost else "-"}", - subText = "Номер прицепа" - ) - } - InformationPlaceholderSmall( - Modifier .fillMaxWidth() - .padding(bottom = 20.dp), - mainText = if (taskResponse.route!!.temperatureProperty == 1) "Горячая" else "Холодная", - subText = "Тип перевозки" + .padding(horizontal = 15.dp, vertical = 15.dp) ) { - when (taskResponse.route!!.temperatureProperty) { - 1 -> Image(painter = painterResource(id = R.drawable.hottransport), contentDescription = "" ) - else -> Image(painter = painterResource(id = R.drawable.coltransport), contentDescription = "" ) - } + Row( + Modifier + .fillMaxWidth() + .padding(bottom = 15.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + modifier = Modifier.weight(1f), + text = "Движение по маршруту\n${it.task.route.name}", + fontWeight = FontWeight.Bold, + fontSize = 20.sp + ) + Spacer(modifier = Modifier.width(10.dp)) - } - children() - AnimatedVisibility(visible = expanded) { - Column { + IconButton(onClick = { /*TODO*/ }, modifier = Modifier.weight(0.1f)) { + Image( + painter = painterResource(id = R.drawable.tick_default), + contentDescription = "tick" + ) + } + } + Row(Modifier.padding(bottom = 20.dp)) { InformationPlaceholderSmall( - Modifier - .fillMaxWidth() - .padding(bottom = 20.dp), - mainText = "${taskResponse.text}", - subText = "Полный маршрут" + Modifier.weight(1f), + mainText = "${if (it.task.route.truck != null) it.task.route.truck.gost else "-"}", + subText = "Номер ТС" ) - - Text( - modifier = Modifier.padding(bottom = 5.dp), - text = "Начало маршрута", - fontWeight = FontWeight.Bold, - fontSize = 14.sp + Spacer(modifier = Modifier.width(10.dp)) + InformationPlaceholderSmall( + Modifier.weight(1f), + mainText = "${if (it.task.route.trailer != null) it.task.route.trailer.gost else "-"}", + subText = "Номер прицепа" ) - Row(Modifier.padding(bottom = 20.dp)) { - InformationPlaceholderSmall( - Modifier.weight(2f), - mainText = dateFormat.format(startPln), - subText = "Дата" - ) - Spacer(modifier = Modifier.width(10.dp)) - InformationPlaceholderSmall( - Modifier.weight(1f), - mainText = timeFormat.format(startPln), - subText = "Время" - ) - } - - Text( - modifier = Modifier.padding(bottom = 5.dp), - text = "Конец маршрута", - fontWeight = FontWeight.Bold, - fontSize = 14.sp - ) - Row(Modifier.padding(bottom = 20.dp)) { - InformationPlaceholderSmall( - Modifier.weight(2f), - mainText = dateFormat.format(endPln), - subText = "Дата" - ) - Spacer(modifier = Modifier.width(10.dp)) - InformationPlaceholderSmall( - Modifier.weight(1f), - mainText = timeFormat.format(endPln), - subText = "Время" - ) - } - - Text( - modifier = Modifier.padding(bottom = 5.dp), - text = if (isDelay) "Задержка" else "Осталось времени до конца маршрута", - fontWeight = FontWeight.Bold, - fontSize = if (isDelay) 20.sp else 14.sp, - color = if (isDelay) Color(0xFFE5332A) else Color.Black - ) - Row(Modifier.padding(bottom = 20.dp)) { - InformationPlaceholderSmall( - Modifier - .weight(1f) - .border( - 1.dp, - color = if (isDelay) Color(0xFFE5332A) else Color.Gray, - shape = RoundedCornerShape(10.dp) - ), - mainText = leastDays.toString(), - subText = "Дни" - ) - Spacer(modifier = Modifier.width(10.dp)) - InformationPlaceholderSmall( - Modifier - .weight(1f) - .border( - 1.dp, - color = if (isDelay) Color(0xFFE5332A) else Color.Gray, - shape = RoundedCornerShape(10.dp) - ), - mainText = leastHours.toString(), - subText = "Часы" - ) - - Spacer(modifier = Modifier.width(10.dp)) - InformationPlaceholderSmall( - Modifier - .weight(1f) - .border( - 1.dp, - color = if (isDelay) Color(0xFFE5332A) else Color.Gray, - shape = RoundedCornerShape(10.dp) - ), - mainText = leastMinutes.toString(), - subText = "Минуты" - ) + } + InformationPlaceholderSmall( + Modifier + .fillMaxWidth() + .padding(bottom = 20.dp), + mainText = if (it.task.route.temperatureProperty == MarshTemperaturePropertyQL.HOT) "Горячая" else "Холодная", + subText = "Тип перевозки" + ) { + when (it.task.route.temperatureProperty) { + MarshTemperaturePropertyQL.HOT -> Image(painter = painterResource(id = R.drawable.hottransport), contentDescription = "" ) + else -> Image(painter = painterResource(id = R.drawable.coltransport), contentDescription = "" ) } } - } - Divider(thickness = 2.dp, color = Color.Gray) - TextButton(onClick = { expanded = !expanded }, colors = ButtonDefaults.buttonColors(contentColor = Color.Black, containerColor = Color.Transparent)) { - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { - Text(text = if (expanded) "Скрыть" else "Развернуть", fontSize = 16.sp) - Icon(Icons.Rounded.KeyboardArrowUp, contentDescription = "Chevron") + children() + AnimatedVisibility(visible = expanded) { + Column { + InformationPlaceholderSmall( + Modifier + .fillMaxWidth() + .padding(bottom = 20.dp), + mainText = it.task.text, + subText = "Полный маршрут" + ) + + Text( + modifier = Modifier.padding(bottom = 5.dp), + text = "Начало маршрута", + fontWeight = FontWeight.Bold, + fontSize = 14.sp + ) + Row(Modifier.padding(bottom = 20.dp)) { + InformationPlaceholderSmall( + Modifier.weight(2f), + mainText = dateFormat.format(startPln), + subText = "Дата" + ) + Spacer(modifier = Modifier.width(10.dp)) + InformationPlaceholderSmall( + Modifier.weight(1f), + mainText = timeFormat.format(startPln), + subText = "Время" + ) + } + + Text( + modifier = Modifier.padding(bottom = 5.dp), + text = "Конец маршрута", + fontWeight = FontWeight.Bold, + fontSize = 14.sp + ) + Row(Modifier.padding(bottom = 20.dp)) { + InformationPlaceholderSmall( + Modifier.weight(2f), + mainText = dateFormat.format(endPln), + subText = "Дата" + ) + Spacer(modifier = Modifier.width(10.dp)) + InformationPlaceholderSmall( + Modifier.weight(1f), + mainText = timeFormat.format(endPln), + subText = "Время" + ) + } + + Text( + modifier = Modifier.padding(bottom = 5.dp), + text = if (isDelay) "Задержка" else "Осталось времени до конца маршрута", + fontWeight = FontWeight.Bold, + fontSize = if (isDelay) 20.sp else 14.sp, + color = if (isDelay) Color(0xFFE5332A) else Color.Black + ) + Row(Modifier.padding(bottom = 20.dp)) { + InformationPlaceholderSmall( + Modifier + .weight(1f) + .border( + 1.dp, + color = if (isDelay) Color(0xFFE5332A) else Color.Gray, + shape = RoundedCornerShape(10.dp) + ), + mainText = leastDays.toString(), + subText = "Дни" + ) + Spacer(modifier = Modifier.width(10.dp)) + InformationPlaceholderSmall( + Modifier + .weight(1f) + .border( + 1.dp, + color = if (isDelay) Color(0xFFE5332A) else Color.Gray, + shape = RoundedCornerShape(10.dp) + ), + mainText = leastHours.toString(), + subText = "Часы" + ) + + Spacer(modifier = Modifier.width(10.dp)) + InformationPlaceholderSmall( + Modifier + .weight(1f) + .border( + 1.dp, + color = if (isDelay) Color(0xFFE5332A) else Color.Gray, + shape = RoundedCornerShape(10.dp) + ), + mainText = leastMinutes.toString(), + subText = "Минуты" + ) + } + + } } + Divider(thickness = 2.dp, color = Color.Gray) + TextButton(onClick = { expanded = !expanded }, colors = ButtonDefaults.buttonColors(contentColor = Color.Black, containerColor = Color.Transparent)) { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + Text(text = if (expanded) "Скрыть" else "Развернуть", fontSize = 16.sp) + Icon(Icons.Rounded.KeyboardArrowUp, contentDescription = "Chevron") + } + } + footerButton() + + + } - footerButton() } + + return Column ( + Modifier + .fillMaxWidth() + .padding(vertical = 60.dp), horizontalAlignment = Alignment.CenterHorizontally) { + CircularProgressIndicator(color = Color(0xFFE5332A)) + } + } diff --git a/app/src/main/java/com/example/mpdriver/components/feed/ActiveTask.kt b/app/src/main/java/com/example/mpdriver/components/feed/ActiveTask.kt index b2afcc7..e0eab56 100644 --- a/app/src/main/java/com/example/mpdriver/components/feed/ActiveTask.kt +++ b/app/src/main/java/com/example/mpdriver/components/feed/ActiveTask.kt @@ -25,6 +25,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState 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 @@ -39,24 +40,44 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController +import com.apollographql.apollo3.api.ApolloResponse +import com.example.mpdriver.GetActiveSubtaskIDQuery +import com.example.mpdriver.GetActiveTaskIdQuery +import com.example.mpdriver.GetTaskByIdQuery import com.example.mpdriver.api.TaskResponse +import com.example.mpdriver.api.apolloClient import com.example.mpdriver.components.ButtonType import com.example.mpdriver.components.EmptyList import com.example.mpdriver.components.IteractionButton import com.example.mpdriver.components.JDEButton import com.example.mpdriver.components.Subtask import com.example.mpdriver.components.Task +import com.example.mpdriver.storage.Database +import com.example.mpdriver.type.StatusEnumQl @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ActiveTask(activeTask: TaskResponse? = null, hostController: NavHostController) { +fun ActiveTask(activeTaskID: Long? = null, hostController: NavHostController) { + + var responseGetActiveSubtaskID by remember { + mutableStateOf(null) + } + + activeTaskID?.let { + LaunchedEffect(Unit) { + val sbtDB = Database.subtasks.find { it.status == StatusEnumQl.IN_PROGRESS } + sbtDB?.let { + responseGetActiveSubtaskID = GetActiveSubtaskIDQuery.ActiveSubtask(sbtDB.id) + } + } + } Column( Modifier .fillMaxWidth() .padding(vertical = 15.dp) ) { - if (activeTask == null) { + if (activeTaskID == null) { EmptyList(Modifier.padding(vertical = 60.dp), text = "У вас нет активных задач") return } @@ -66,9 +87,9 @@ fun ActiveTask(activeTask: TaskResponse? = null, hostController: NavHostControll fontSize = 18.sp ) - activeTask.let { - IteractionButton(onClick = { hostController.navigate("tasks/${it.id}") }) { - Task(taskResponse = it) + activeTaskID.let { + IteractionButton(onClick = { hostController.navigate("tasks/${activeTaskID}") }) { + Task(task_id = activeTaskID) } } @@ -81,14 +102,14 @@ fun ActiveTask(activeTask: TaskResponse? = null, hostController: NavHostControll Spacer(modifier = Modifier.height(10.dp)) - activeTask.let { task -> + responseGetActiveSubtaskID.let { task -> IteractionButton(onClick = { /*TODO*/ }) { - Subtask(subtask = task.subtasks.filter { it.status == "InProgress" }[0]) + responseGetActiveSubtaskID?.let { + Subtask(subtaskID = it.id.toLong()) + } } } } - - } diff --git a/app/src/main/java/com/example/mpdriver/components/note/NoteComponent.kt b/app/src/main/java/com/example/mpdriver/components/note/NoteComponent.kt new file mode 100644 index 0000000..8d13453 --- /dev/null +++ b/app/src/main/java/com/example/mpdriver/components/note/NoteComponent.kt @@ -0,0 +1,35 @@ +package com.example.mpdriver.components.note + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.mpdriver.components.ActiveButton +import com.example.mpdriver.components.JDEButton + + +@Preview(showBackground = true) +@Composable +fun NoteComponent(modifier: Modifier = Modifier) { + Column( + modifier + .fillMaxWidth() + .border(2.dp, color = Color.Gray, shape = RoundedCornerShape(10.dp)) + .padding(15.dp)) { + Text(text = "Изменился статус события", fontSize = 18.sp, fontWeight = FontWeight.Bold) + Text(text = "Изменился статус события\n“Прибыть на ПГР 20.12.2023 к 10:00”", fontSize = 15.sp, fontWeight = FontWeight.Normal, color = Color.Gray) + Spacer(modifier = Modifier.height(10.dp)) + ActiveButton(onClick = { /*TODO*/ }, text = "Перейти к задаче", modifier = Modifier.fillMaxWidth()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/mpdriver/screens/Feed.kt b/app/src/main/java/com/example/mpdriver/screens/Feed.kt index 9025cef..1d6f8e6 100644 --- a/app/src/main/java/com/example/mpdriver/screens/Feed.kt +++ b/app/src/main/java/com/example/mpdriver/screens/Feed.kt @@ -1,6 +1,7 @@ package com.example.mpdriver.screens import android.os.Build +import android.util.Log import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -9,6 +10,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.CircularProgressIndicator 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 @@ -19,11 +21,17 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController +import com.apollographql.apollo3.api.ApolloResponse +import com.example.mpdriver.GetActiveTaskIdQuery +import com.example.mpdriver.GetTaskByIdQuery import com.example.mpdriver.api.TaskApi import com.example.mpdriver.api.TaskResponse +import com.example.mpdriver.api.apolloClient import com.example.mpdriver.components.Layout import com.example.mpdriver.components.feed.ActiveTask import com.example.mpdriver.components.feed.FeedTaskDataCard +import com.example.mpdriver.storage.Database +import com.example.mpdriver.type.StatusEnumQl import kotlinx.datetime.LocalDateTime import kotlinx.datetime.format.byUnicodePattern @@ -37,29 +45,17 @@ fun Feed(modifier: Modifier = Modifier, hostController: NavHostController) { mutableStateOf(true) } - val context = LocalContext.current var activeTask by remember { - mutableStateOf(null) + mutableStateOf(null) } var plannedTasks by remember { - mutableStateOf>(emptyList()) + mutableStateOf(emptyList()) } var completedTasks by remember { - mutableStateOf>(emptyList()) + mutableStateOf(emptyList()) } - val taskApi = TaskApi(context) - taskApi.getActiveTask { task -> - isLoading = false - task.id?.let { - activeTask = task - } - } - - taskApi.getPlannedTasks { tasks -> - plannedTasks = tasks - } val dateFormat = LocalDateTime.Format { byUnicodePattern("dd.MM.yyyy") } @@ -70,7 +66,7 @@ fun Feed(modifier: Modifier = Modifier, hostController: NavHostController) { "count" to plannedTasks.count(), "date" to when (plannedTasks.count()) { 0 -> "-" - else -> dateFormat.format(LocalDateTime.parse(plannedTasks[0].startPln!!)) + else -> dateFormat.format(LocalDateTime.parse(plannedTasks[0].startPln.toString())) }, "buttonLabel" to "Смотреть запланированные задачи", "dateDescription" to "Ближайшая", @@ -80,23 +76,59 @@ fun Feed(modifier: Modifier = Modifier, hostController: NavHostController) { "count" to completedTasks.count(), "date" to when (completedTasks.count()) { 0 -> "-" - else -> dateFormat.format(LocalDateTime.parse(completedTasks[0].startPln!!)) + else -> dateFormat.format(LocalDateTime.parse(completedTasks[0].startPln.toString())) }, "buttonLabel" to "Смотреть завершенные задачи", "dateDescription" to "Последняя", ), ) + LaunchedEffect(Unit) { + + val tasks = Database.tasks + + val activeTaskDB = tasks.find { it.status == StatusEnumQl.IN_PROGRESS } + val completedTasksDB = tasks.filter { it.status == StatusEnumQl.COMPLETED } + val plannedTasksDB = tasks.filter { it.status == StatusEnumQl.NOT_DEFINED } + + activeTask = when (activeTaskDB) { + null -> null + else -> GetActiveTaskIdQuery.Task(id = activeTaskDB.id) + } + completedTasks = when (completedTasksDB.count()) { + 0 -> emptyList() + else -> completedTasksDB.map {t -> + GetActiveTaskIdQuery.CompletedTask(t.id, t.startPln) + } + } + + plannedTasks = when (plannedTasksDB.count()) { + 0 -> emptyList() + else -> plannedTasksDB.map { t -> + GetActiveTaskIdQuery.PlannedTask(t.id, t.startPln) + } + } + Log.d("FEED:PlannedTasks", "${plannedTasks}") + Log.d("FEED:ActiveTask", "${activeTask}") + Log.d("FEED:CompletedTask", "${completedTasks}") + + isLoading = false + } + if(isLoading) { - Column (Modifier.fillMaxWidth().padding(vertical = 60.dp), horizontalAlignment = Alignment.CenterHorizontally) { + Column ( + Modifier + .fillMaxWidth() + .padding(vertical = 60.dp), horizontalAlignment = Alignment.CenterHorizontally) { CircularProgressIndicator(color = Color(0xFFE5332A)) } return } + Layout(dataList = dataList, header = { Column { - ActiveTask(activeTask = activeTask, hostController = hostController) + ActiveTask(activeTaskID = activeTask?.id?.toLong(), hostController = hostController) Spacer(modifier = Modifier.height(20.dp)) } diff --git a/app/src/main/java/com/example/mpdriver/screens/NoteScreen.kt b/app/src/main/java/com/example/mpdriver/screens/NoteScreen.kt new file mode 100644 index 0000000..e1dfdad --- /dev/null +++ b/app/src/main/java/com/example/mpdriver/screens/NoteScreen.kt @@ -0,0 +1,17 @@ +package com.example.mpdriver.screens + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.example.mpdriver.components.Layout +import com.example.mpdriver.components.note.NoteComponent + + +@Preview(showBackground = true) +@Composable +fun NoteScreen() { + + + Layout(dataList = (0..5).toMutableList()) { + NoteComponent() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/mpdriver/screens/SubtaskAndEvents.kt b/app/src/main/java/com/example/mpdriver/screens/SubtaskAndEvents.kt index 55a5af2..77349ea 100644 --- a/app/src/main/java/com/example/mpdriver/screens/SubtaskAndEvents.kt +++ b/app/src/main/java/com/example/mpdriver/screens/SubtaskAndEvents.kt @@ -25,6 +25,7 @@ import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -40,6 +41,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle +import com.example.mpdriver.FetchApplicationDataQuery import com.example.mpdriver.api.Subtasks import com.example.mpdriver.api.TaskApi import com.example.mpdriver.components.ComposableLifecycle @@ -47,6 +49,8 @@ import com.example.mpdriver.components.HeaderTabs import com.example.mpdriver.components.HeaderTabsData import com.example.mpdriver.components.Layout import com.example.mpdriver.components.Subtask +import com.example.mpdriver.storage.Database +import kotlinx.datetime.LocalDateTime @Preview(showBackground = true) @@ -65,8 +69,9 @@ fun SubtaskScreen(taskId: Long = 0) { var datalist by remember { - mutableStateOf>(mutableListOf()) + mutableStateOf(emptyList()) } + var activeTab by remember { mutableStateOf(0) } @@ -74,25 +79,24 @@ fun SubtaskScreen(taskId: Long = 0) { val listState = rememberLazyListState() var offset = listState.firstVisibleItemScrollOffset + listState.firstVisibleItemIndex - val tabsData = listOf(HeaderTabsData(0, "Подзадачи"), HeaderTabsData(1, "События")) + val tabsData = + listOf(HeaderTabsData(0, "Подзадачи"), HeaderTabsData(1, "События")) - ComposableLifecycle { _, event -> - when (event) { - Lifecycle.Event.ON_CREATE -> { - api.getSubtasks(taskId) { - datalist = it - isLoading = false - } - } - else -> {} - } + LaunchedEffect(Unit) { + val task = Database.tasks.find { it.id == taskId.toString() } + datalist = Database.subtasks.filter { sbt -> + task!!.subtasks.find { it.id == sbt.id } != null + }.sortedBy { LocalDateTime.parse(it.startPln.toString()) } + isLoading = false } - if(isLoading) { - Column ( + + if (isLoading) { + Column( Modifier .fillMaxWidth() - .padding(vertical = 60.dp), horizontalAlignment = Alignment.CenterHorizontally) { + .padding(vertical = 60.dp), horizontalAlignment = Alignment.CenterHorizontally + ) { CircularProgressIndicator(color = Color(0xFFE5332A)) } return @@ -100,18 +104,26 @@ fun SubtaskScreen(taskId: Long = 0) { Layout(state = listState, dataList = datalist, header = { HeaderTabs(modifier = Modifier.onGloballyPositioned { lc -> - val cords = lc.boundsInParent() + val cords = lc.boundsInParent() floatingActionShowOffset = cords.bottom }, tabsData = tabsData, activeTab = activeTab) { - activeTab = it + activeTab = it } }) { - Subtask(subtask = it) + Subtask(subtaskID = it.id!!.toLong()) } - AnimatedVisibility(visible = offset + 200 > floatingActionShowOffset + 100, enter = slideInVertically() + fadeIn(), exit = slideOutVertically() + fadeOut()) { - ExtendedFloatingActionButton(onClick = { /*TODO*/ }, - shape = RoundedCornerShape(10.dp), containerColor = Color.White, contentColor = Color.Black) { + AnimatedVisibility( + visible = offset + 200 > floatingActionShowOffset + 100, + enter = slideInVertically() + fadeIn(), + exit = slideOutVertically() + fadeOut() + ) { + ExtendedFloatingActionButton( + onClick = { /*TODO*/ }, + shape = RoundedCornerShape(10.dp), + containerColor = Color.White, + contentColor = Color.Black + ) { Row(Modifier.fillMaxWidth()) { Text(text = "Скрыть завершенные задачи") } diff --git a/app/src/main/java/com/example/mpdriver/screens/TasksList.kt b/app/src/main/java/com/example/mpdriver/screens/TasksList.kt index dd2730d..c1ee6fa 100644 --- a/app/src/main/java/com/example/mpdriver/screens/TasksList.kt +++ b/app/src/main/java/com/example/mpdriver/screens/TasksList.kt @@ -16,6 +16,7 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text 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 @@ -28,17 +29,25 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController +import com.apollographql.apollo3.api.ApolloResponse +import com.example.mpdriver.GetPlannedTasksIDsQuery +import com.example.mpdriver.GetTaskByIdQuery import com.example.mpdriver.api.TaskApi import com.example.mpdriver.api.TaskResponse +import com.example.mpdriver.api.apolloClient import com.example.mpdriver.components.ActiveButton import com.example.mpdriver.components.HeaderTabs import com.example.mpdriver.components.HeaderTabsData import com.example.mpdriver.components.Layout import com.example.mpdriver.components.StaleButton import com.example.mpdriver.components.Task +import com.example.mpdriver.storage.CreateUpdateTaskData import com.example.mpdriver.storage.Database +import com.example.mpdriver.type.StatusEnumQl import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import java.time.LocalDateTime @Composable @@ -50,26 +59,41 @@ fun TasksList(modifier: Modifier = Modifier, hostController: NavHostController) } - var itemsList by remember { - mutableStateOf>(emptyList()) - } - api.getPlannedTasks { - itemsList = it - isLoading = false + var itemsListResponse by remember { + mutableStateOf(emptyList()) } + var activeTab by remember { mutableIntStateOf(0) } + LaunchedEffect(Unit) { + val d = Database.tasks.filter { it -> it.status == StatusEnumQl.NOT_DEFINED} + + itemsListResponse = when(d.count()) { + 0 -> emptyList() + else -> d.map { + val sbts = it.subtasks.map {sbt -> + GetPlannedTasksIDsQuery.Subtask(sbt.id) + } + GetPlannedTasksIDsQuery.Task(id = it.id, sbts) + } + } + isLoading = false + } + if(isLoading) { - Column (Modifier.fillMaxWidth().padding(vertical = 60.dp), horizontalAlignment = Alignment.CenterHorizontally) { + Column ( + Modifier + .fillMaxWidth() + .padding(vertical = 60.dp), horizontalAlignment = Alignment.CenterHorizontally) { CircularProgressIndicator(color = Color(0xFFE5332A)) } return } - Layout(dataList = itemsList, header = { + Layout(dataList = itemsListResponse, header = { HeaderTabs( tabsData = listOf( HeaderTabsData(0, "Запланированные"), @@ -79,15 +103,24 @@ fun TasksList(modifier: Modifier = Modifier, hostController: NavHostController) activeTab = it } }) { - Task(Modifier.padding(bottom = 10.dp), taskResponse = it) { + Task(Modifier.padding(bottom = 10.dp), task_id = it.id.toLong()) { when (activeTab) { 0 -> Button( onClick = { - api.setTaskStatusInProgress(it.id!!) { - MainScope().launch { - hostController.navigate("feed") - } - } + Database.createUpdateTaskDataLocally( + CreateUpdateTaskData( + it.id.toLong(), + Clock.System.now().toEpochMilliseconds(), + "InProgress" + ) + ) + Database.createUpdateTaskDataLocally( + CreateUpdateTaskData( + it.subtasks[0].id.toLong(), Clock.System.now().toEpochMilliseconds(), "InProgress", + ) + ) + hostController.navigate("feed") + }, colors = ButtonDefaults.buttonColors( contentColor = Color.White, @@ -96,7 +129,7 @@ fun TasksList(modifier: Modifier = Modifier, hostController: NavHostController) disabledContainerColor = Color.Gray ), shape = RoundedCornerShape(10.dp), - enabled = if (Database.tasks.filter { it.status == "InProgress" }.count() >= 1) false else true, +// enabled = if (Database.tasks.filter { it.status == "InProgress" }.count() >= 1) false else true, modifier = Modifier.fillMaxWidth() ) { Text(text = "Приступить к выполнению") diff --git a/app/src/main/java/com/example/mpdriver/storage/Base.kt b/app/src/main/java/com/example/mpdriver/storage/Base.kt index 650d8aa..25a322e 100644 --- a/app/src/main/java/com/example/mpdriver/storage/Base.kt +++ b/app/src/main/java/com/example/mpdriver/storage/Base.kt @@ -1,43 +1,28 @@ package com.example.mpdriver.storage -import com.example.mpdriver.api.TaskResponse +import android.util.Log +import com.benasher44.uuid.uuid4 +import com.example.mpdriver.FetchApplicationDataQuery +import com.example.mpdriver.api.toJson +import com.example.mpdriver.type.DateTime +import com.example.mpdriver.type.StatusEnumQl import com.google.gson.Gson -import com.google.gson.reflect.TypeToken import com.tencent.mmkv.MMKV -import java.lang.reflect.Type + + +data class CreateUpdateTaskData( + val task_id: Long, + val dt: Long, + val status: String, + val error_text: String? = null +) object Database { + val kv = MMKV.defaultMMKV() - var tasks: List - get() { - val data = kv.decodeString("tasks") - data?.let { json -> - val type: Type = object : TypeToken>() {}.type - return Gson().fromJson(json, type) - } - return emptyList() - } - set(value) { - val data = Gson().toJson(value) - kv.encode("tasks", data) - } - - var planned_tasks: List - get() { - val data = kv.decodeString("planned_tasks") - data?.let { json -> - val type: Type = object : TypeToken>() {}.type - return Gson().fromJson(json, type) - } - return emptyList() - } - set(value) { - val data = Gson().toJson(value) - println(value) - println(data) - kv.encode("planned_tasks", data) - } + val allKeys: Array? + get() = kv.allKeys() var access_token: String? get() { @@ -52,4 +37,96 @@ object Database { set(value) { kv.encode("phone_number", value) } -} \ No newline at end of file + + + private inline fun parseData(prefix: String): MutableList { + val returnedList = mutableListOf() + + for (key in allKeys!!) { + if (key.startsWith(prefix)) { + val data = kv.decodeString(key) + data.let { + returnedList.add(Gson().fromJson(it, D::class.java)) + } + } + } + return returnedList + } + + val tasks: MutableList + get() = parseData("task:") + + val notes: MutableList + get() = parseData("note:") + + val subtasks: MutableList + get() = parseData("subtask:") + + + val updateTasks: MutableList + get() = parseData("update:task:") + + fun createUpdateTaskDataLocally(d: CreateUpdateTaskData) { + + kv.encode("update:task:${uuid4()}", d.toJson()) + val task = tasks.find { it.id == d.task_id.toString() } + val sbt = subtasks.find { it.id == d.task_id.toString() } + + task?.let { + + val newTask = FetchApplicationDataQuery.Task( + id = task.id, + startPln = task.startPln, + startFact = task.startFact, + endPln = task.endPln, + endFact = task.endFact, + status = when (d.status) { + "InProgress" -> StatusEnumQl.IN_PROGRESS + "Completed" -> StatusEnumQl.COMPLETED + "Cancelled" -> StatusEnumQl.CANCELLED + else -> task.status + }, + events = task.events, + subtasks = task.subtasks, + taskType = task.taskType, + route = task.route, + text = task.text + ) + kv.encode("task:${newTask.id}", newTask.toJson()) +// Log.d("MMKV: task:${newTask.id}", kv.decodeString("task:${newTask.id}")!!) + Log.d("MMKV.KEYS", allKeys!!.toMutableList().joinToString(", ")) + } + + sbt?.let { s -> + val newSubtask = FetchApplicationDataQuery.Subtask1( + id = s.id, + startPln = s.startPln, + startFact = s.startFact, + endPln = s.endPln, + endFact = s.endFact, + station = s.station, + status = when (d.status) { + "InProgress" -> StatusEnumQl.IN_PROGRESS + "Completed" -> StatusEnumQl.COMPLETED + "Cancelled" -> StatusEnumQl.CANCELLED + else -> s.status + }, + taskType = s.taskType, + text = s.text + ) + kv.encode("subtask:${newSubtask.id}", newSubtask.toJson()) + Log.d("MMKV.KEYS", allKeys!!.toMutableList().joinToString(", ")) + } + + } + + + fun dropUpdates() { + for (key in allKeys!!){ + if (key.startsWith("update:task:")) { + kv.remove(key) + } + } + } + +} diff --git a/build.gradle.kts b/build.gradle.kts index f74b04b..d925da6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,4 +2,5 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.jetbrains.kotlin.android) apply false + id("com.google.gms.google-services") version "4.4.2" apply false } \ No newline at end of file