add offline mode

main
Ernest Litvinenko 2024-07-10 19:08:36 +03:00
parent d31bce2f89
commit 0074e651d0
25 changed files with 1623 additions and 450 deletions

View File

@ -10,6 +10,14 @@
</SelectionState>
<SelectionState runConfigName="MainActivity">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2024-07-09T21:11:01.409507Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/ernestlitvinenko/.android/avd/Small_Phone_API_28.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>

View File

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">

View File

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

View File

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

View File

@ -0,0 +1,7 @@
query GetActiveSubtaskID($userID: String!) {
task(userId: $userID, isActive: true) {
activeSubtask {
id
}
}
}

View File

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

View File

@ -0,0 +1,8 @@
query GetPlannedTasksIDs($userID: String!) {
tasks(userId: $userID, isPlanned:true) {
id
subtasks {
id
}
}
}

View File

@ -0,0 +1,9 @@
query GetSubtaskByID($userID: String!, $subtaskID: String!) {
subtask(userId: $userID, subtaskId: $subtaskID) {
id
text
startPln
endPln
status
}
}

View File

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

View File

@ -0,0 +1,7 @@
query GetTasksFeedScreen($userID: String!){
tasks(userId: $userID) {
id
text
}
}

View File

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

View File

@ -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<String, FooterNavMenuLabel>(
"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)
}

View File

@ -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 <reified T> Response.parseList(): List<T> {
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 {

View File

@ -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<Events> = arrayListOf(),
@SerializedName("subtasks" ) var subtasks: ArrayList<Subtasks> = 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<Events> = arrayListOf(),
@SerializedName("subtasks") var subtasks: ArrayList<Subtasks> = 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<TaskResponse>) -> Unit) {
data class SendTaskStatusReq(
val data: List<CreateUpdateTaskData>
)
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<TaskResponse>) -> 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<TaskResponse>) -> 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<TaskResponse>) -> 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<TaskResponse>())
}
}
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<TaskResponse>) -> Unit) {
fun getCompletedTask(
errorHandler: (Exception) -> Unit = {},
handler: (List<TaskResponse>) -> Unit
) {
val req = Request.Builder()
.url("$BASE_URL/tasks/completed")
.build()
@ -149,7 +193,12 @@ class TaskApi(ctx: Context): Api(ctx) {
handler(it.parseList<TaskResponse>())
}
}
fun getSubtasks(taskId: Long, errorHandler: (Exception)-> Unit = {}, handler: (List<Subtasks>) -> Unit) {
fun getSubtasks(
taskId: Long,
errorHandler: (Exception) -> Unit = {},
handler: (List<Subtasks>) -> 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<Subtasks>())
}
}
fun getEvents(taskId: Long, errorHandler: (Exception)-> Unit = {}, handler: (List<Events>) -> Unit) {
fun getEvents(
taskId: Long,
errorHandler: (Exception) -> Unit = {},
handler: (List<Events>) -> 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<TaskResponse>()
Database.tasks = data
}
}
// fun getAllTasksForUser() {
// val req = Request.Builder().url("$BASE_URL/tasks").build()
// performRequest(req) {res ->
// val data = res.parseList<TaskResponse>()
// 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())
// }
// }
}

View File

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

View File

@ -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<GetSubtaskByIDQuery.Subtask?>(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
)

View File

@ -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<GetTaskByIdQuery.Data?>(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))
}
}

View File

@ -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<GetActiveSubtaskIDQuery.ActiveSubtask?>(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())
}
}
}
}
}

View File

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

View File

@ -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<TaskResponse?>(null)
mutableStateOf<GetActiveTaskIdQuery.Task?>(null)
}
var plannedTasks by remember {
mutableStateOf<List<TaskResponse>>(emptyList())
mutableStateOf(emptyList<GetActiveTaskIdQuery.PlannedTask>())
}
var completedTasks by remember {
mutableStateOf<List<TaskResponse>>(emptyList())
mutableStateOf(emptyList<GetActiveTaskIdQuery.CompletedTask>())
}
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))
}

View File

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

View File

@ -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<List<Subtasks>>(mutableListOf())
mutableStateOf(emptyList<FetchApplicationDataQuery.Subtask1>())
}
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>(HeaderTabsData(0, "Подзадачи"), HeaderTabsData(1, "События"))
val tabsData =
listOf<HeaderTabsData>(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 = "Скрыть завершенные задачи")
}

View File

@ -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<List<TaskResponse>>(emptyList())
}
api.getPlannedTasks {
itemsList = it
isLoading = false
var itemsListResponse by remember {
mutableStateOf(emptyList<GetPlannedTasksIDsQuery.Task>())
}
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 = "Приступить к выполнению")

View File

@ -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<TaskResponse>
get() {
val data = kv.decodeString("tasks")
data?.let { json ->
val type: Type = object : TypeToken<List<TaskResponse>>() {}.type
return Gson().fromJson(json, type)
}
return emptyList()
}
set(value) {
val data = Gson().toJson(value)
kv.encode("tasks", data)
}
var planned_tasks: List<TaskResponse>
get() {
val data = kv.decodeString("planned_tasks")
data?.let { json ->
val type: Type = object : TypeToken<List<TaskResponse>>() {}.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<out String>?
get() = kv.allKeys()
var access_token: String?
get() {
@ -52,4 +37,96 @@ object Database {
set(value) {
kv.encode("phone_number", value)
}
}
private inline fun <reified D> parseData(prefix: String): MutableList<D> {
val returnedList = mutableListOf<D>()
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<FetchApplicationDataQuery.Task>
get() = parseData("task:")
val notes: MutableList<FetchApplicationDataQuery.Note>
get() = parseData("note:")
val subtasks: MutableList<FetchApplicationDataQuery.Subtask1>
get() = parseData("subtask:")
val updateTasks: MutableList<CreateUpdateTaskData>
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)
}
}
}
}

View File

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