From ae1a4e8bf4a72238d78214dd84c80aa5f11798eb Mon Sep 17 00:00:00 2001 From: Usatov Pavel Date: Wed, 4 Jun 2025 16:39:45 +0300 Subject: [PATCH 01/11] API: fix task add&edit Now client send server id, Task state after all tasks operations works Optimize imports --- .../smartcalendar/activity/MainActivity.kt | 18 -------- .../hse/smartcalendar/data/DailyTaskType.kt | 1 - .../hse/smartcalendar/data/SettingsButton.kt | 1 - .../hse/smartcalendar/network/ApiInterface.kt | 8 ++-- .../hse/smartcalendar/network/DataResponse.kt | 38 ++++++++-------- .../smartcalendar/network/RequestClasses.kt | 45 +++++++++++++++---- .../notification/ReminderViewModel.kt | 7 --- .../notification/StatisticsUploadWorker.kt | 1 - .../repository/AuthRepository.kt | 1 - .../repository/TaskRepository.kt | 12 +++-- .../hse/smartcalendar/ui/elements/ChartPie.kt | 2 - .../ui/elements/MultipleLinearProgressBar.kt | 2 +- .../hse/smartcalendar/ui/navigation/App.kt | 2 +- .../ui/screens/AchievementsScreen.kt | 5 ++- .../ui/screens/StatisticsScreen.kt | 3 +- .../smartcalendar/ui/task/DailyTasksList.kt | 19 ++++---- .../hse/smartcalendar/utility/AppDriver.kt | 4 +- .../smartcalendar/utility/ListViewUtility.kt | 2 +- .../hse/smartcalendar/utility/Serializer.kt | 7 ++- .../smartcalendar/view/model/AuthViewModel.kt | 2 - .../smartcalendar/view/model/ListViewModel.kt | 27 +++++------ .../view/model/SettingsViewModel.kt | 7 +-- .../view/model/StatisticsViewModel.kt | 4 -- .../view/model/TaskEditViewModel.kt | 5 +-- 24 files changed, 106 insertions(+), 117 deletions(-) diff --git a/app/src/main/java/org/hse/smartcalendar/activity/MainActivity.kt b/app/src/main/java/org/hse/smartcalendar/activity/MainActivity.kt index e96cec0..85a28c0 100644 --- a/app/src/main/java/org/hse/smartcalendar/activity/MainActivity.kt +++ b/app/src/main/java/org/hse/smartcalendar/activity/MainActivity.kt @@ -9,29 +9,11 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresApi -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat -import androidx.lifecycle.viewmodel.compose.viewModel import org.hse.smartcalendar.data.WorkManagerHolder import org.hse.smartcalendar.ui.navigation.App import org.hse.smartcalendar.ui.theme.SmartCalendarTheme -import org.hse.smartcalendar.utility.Navigation -import org.hse.smartcalendar.utility.Screens -import org.hse.smartcalendar.utility.rememberNavigation import org.hse.smartcalendar.view.model.ListViewModel import org.hse.smartcalendar.view.model.StatisticsManager import org.hse.smartcalendar.view.model.StatisticsViewModel diff --git a/app/src/main/java/org/hse/smartcalendar/data/DailyTaskType.kt b/app/src/main/java/org/hse/smartcalendar/data/DailyTaskType.kt index 847f52e..a5b4b30 100644 --- a/app/src/main/java/org/hse/smartcalendar/data/DailyTaskType.kt +++ b/app/src/main/java/org/hse/smartcalendar/data/DailyTaskType.kt @@ -1,7 +1,6 @@ package org.hse.smartcalendar.data import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.toUpperCase enum class DailyTaskType(private val printName: String, val color: Color) { COMMON("Common", Color.LightGray), diff --git a/app/src/main/java/org/hse/smartcalendar/data/SettingsButton.kt b/app/src/main/java/org/hse/smartcalendar/data/SettingsButton.kt index c2cd886..b633087 100644 --- a/app/src/main/java/org/hse/smartcalendar/data/SettingsButton.kt +++ b/app/src/main/java/org/hse/smartcalendar/data/SettingsButton.kt @@ -10,7 +10,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp diff --git a/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt b/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt index b0c28b0..caa33e9 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt @@ -3,12 +3,12 @@ package org.hse.smartcalendar.network import okhttp3.ResponseBody import retrofit2.Response import retrofit2.http.Body -import retrofit2.http.Path//auto import not work import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.PATCH import retrofit2.http.POST import retrofit2.http.PUT +import retrofit2.http.Path import java.util.UUID interface AuthApiInterface { @@ -35,7 +35,7 @@ interface TaskApiInterface { @POST("api/users/{userId}/events") suspend fun addTask( @Path("userId") userId: Long, - @Body request: AddTaskRequest + @Body request: TaskRequest ): Response @DELETE("api/users/events/{eventId}") @@ -43,10 +43,10 @@ interface TaskApiInterface { @Path("eventId") eventId: UUID ): Response - @POST("api/users/events/{eventId}") + @PATCH("api/users/events/{eventId}") suspend fun editTask( @Path("eventId") eventId: UUID, - @Body request: AddTaskRequest + @Body request: EditTaskRequest ): Response diff --git a/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt b/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt index 3e7553f..ee2d21e 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt @@ -33,27 +33,29 @@ data class UserInfoResponse( val username: String, val id: Long ) +@Serializable data class TaskResponse( val id: String, val title: String, val description: String, - val start: String, - val end: String, + val start: LocalTime, + val end: LocalTime, val date: String, - val type: String, - val creationTime: String, + val type: DailyTaskType, + val creationTime: LocalDateTime, val complete: Boolean -) - fun toTask(response: TaskResponse): DailyTask{ - return DailyTask( - id = UUID.fromString(response.id), - title =response.title, - description = response.description, - start = LocalTime.parse(response.start), - end = LocalTime.parse(response.end), - date = LocalDate.parse(response.date), - type = DailyTaskType.fromString(response.type), - creationTime = LocalDateTime.parse(response.creationTime), - isComplete = response.complete - ) - } +){ + fun toTask(): DailyTask { + return DailyTask( + id = UUID.fromString(id), + title = title, + description = description, + start = start, + end = end, + date = LocalDate.parse(date), + type = type, + creationTime = creationTime, + isComplete = complete + ) + } +} diff --git a/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt b/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt index 7208b2f..f450ecd 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt @@ -2,11 +2,8 @@ package org.hse.smartcalendar.network import kotlinx.datetime.LocalDateTime import kotlinx.serialization.Serializable -import org.hse.smartcalendar.network.NetworkResponse.Error -import retrofit2.Response import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType -import java.util.UUID data class LoginRequest( val username: String, @@ -24,25 +21,27 @@ data class ChangeCredentialsRequest( val newPassword: String ) @Serializable -data class AddTaskRequest( +data class EditTaskRequest( val creationTime: LocalDateTime, val title: String, val description: String, val start: LocalDateTime, val end: LocalDateTime, val location: String, - val type: DailyTaskType + val type: DailyTaskType, + val completed: Boolean ){ companion object{ - fun fromTask(task: DailyTask): AddTaskRequest{ - return AddTaskRequest( + fun fromTask(task: DailyTask): EditTaskRequest{ + return EditTaskRequest( title = task.getDailyTaskTitle(), description = task.getDailyTaskDescription(), start = LocalDateTime(task.getTaskDate(), task.getDailyTaskStartTime()), end = LocalDateTime(task.getTaskDate(), task.getDailyTaskEndTime()), location = "", type = task.getDailyTaskType(), - creationTime = task.getDailyTaskCreationTime() + creationTime = task.getDailyTaskCreationTime(), + completed = task.isComplete() ) } } @@ -56,4 +55,34 @@ data class CompleteStatusRequest( return CompleteStatusRequest(task.isComplete()) } } +} + +@Serializable +data class TaskRequest( + val id: String, + val title: String, + val description: String, + val start: LocalDateTime, + val end: LocalDateTime, + val date: String, + val type: DailyTaskType, + val creationTime: LocalDateTime, + val complete: Boolean +) { + companion object { + fun fromTask(task: DailyTask): TaskRequest { + val date = task.getTaskDate() + return TaskRequest( + id = task.getId().toString(), + title = task.getDailyTaskTitle(), + description = task.getDailyTaskDescription(), + start = LocalDateTime(date = date, time = task.getDailyTaskStartTime()), + end = LocalDateTime(date = date, time = task.getDailyTaskEndTime()), + date = date.toString(), + type = task.getDailyTaskType(), + creationTime = task.getDailyTaskCreationTime(), + complete = task.isComplete() + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/notification/ReminderViewModel.kt b/app/src/main/java/org/hse/smartcalendar/notification/ReminderViewModel.kt index bc3852c..01c7b2e 100644 --- a/app/src/main/java/org/hse/smartcalendar/notification/ReminderViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/notification/ReminderViewModel.kt @@ -3,24 +3,17 @@ package org.hse.smartcalendar.notification import android.app.Application import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.lifecycle.viewmodel.initializer -import androidx.lifecycle.viewmodel.viewModelFactory import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import androidx.work.workDataOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.datetime.Clock -import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalTime -import org.hse.smartcalendar.activity.BaseApplication import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.utility.prettyPrint import org.hse.smartcalendar.utility.toEpochMilliseconds -import org.hse.smartcalendar.view.model.SettingsViewModel import java.util.concurrent.TimeUnit import kotlin.time.DurationUnit import kotlin.time.toDuration diff --git a/app/src/main/java/org/hse/smartcalendar/notification/StatisticsUploadWorker.kt b/app/src/main/java/org/hse/smartcalendar/notification/StatisticsUploadWorker.kt index 84414c4..1e9de03 100644 --- a/app/src/main/java/org/hse/smartcalendar/notification/StatisticsUploadWorker.kt +++ b/app/src/main/java/org/hse/smartcalendar/notification/StatisticsUploadWorker.kt @@ -2,7 +2,6 @@ package org.hse.smartcalendar.notification import android.content.Context import androidx.work.CoroutineWorker -import androidx.work.Worker import androidx.work.WorkerParameters import kotlinx.serialization.json.Json import org.hse.smartcalendar.network.ApiClient diff --git a/app/src/main/java/org/hse/smartcalendar/repository/AuthRepository.kt b/app/src/main/java/org/hse/smartcalendar/repository/AuthRepository.kt index 6bca6d1..ecd8711 100644 --- a/app/src/main/java/org/hse/smartcalendar/repository/AuthRepository.kt +++ b/app/src/main/java/org/hse/smartcalendar/repository/AuthRepository.kt @@ -10,7 +10,6 @@ import org.hse.smartcalendar.network.LoginResponse import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.network.RegisterRequest import org.hse.smartcalendar.network.UserInfoResponse -import retrofit2.Response //Repository - логика работы с задачами, получает данные, // формирует и отправляет запрос, возвращает данные/exception diff --git a/app/src/main/java/org/hse/smartcalendar/repository/TaskRepository.kt b/app/src/main/java/org/hse/smartcalendar/repository/TaskRepository.kt index 2c0154a..65ab6ed 100644 --- a/app/src/main/java/org/hse/smartcalendar/repository/TaskRepository.kt +++ b/app/src/main/java/org/hse/smartcalendar/repository/TaskRepository.kt @@ -6,11 +6,11 @@ import org.hse.smartcalendar.data.DailySchedule import org.hse.smartcalendar.data.DailySchedule.NestedTaskException import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.User -import org.hse.smartcalendar.network.AddTaskRequest import org.hse.smartcalendar.network.CompleteStatusRequest +import org.hse.smartcalendar.network.EditTaskRequest import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.network.TaskApiInterface -import org.hse.smartcalendar.network.toTask +import org.hse.smartcalendar.network.TaskRequest class TaskRepository(private val api: TaskApiInterface): BaseRepository() { suspend fun deleteTask(task: DailyTask): NetworkResponse{ @@ -27,17 +27,15 @@ class TaskRepository(private val api: TaskApiInterface): BaseRepository() { } suspend fun editTask(task: DailyTask): NetworkResponse{ val response = withSupplierRequest{ - ->api.editTask(task.getId(), AddTaskRequest.fromTask(task)) + ->api.editTask(task.getId(), EditTaskRequest.fromTask(task)) } return response } suspend fun addTask(task: DailyTask): NetworkResponse { val response = withIdRequest { id -> - api.addTask(id, AddTaskRequest.fromTask(task))} + api.addTask(id, TaskRequest.fromTask(task))} return when (response) { is NetworkResponse.Success -> { - //User.getSchedule().? - костыль - task.setId(response.data.id) NetworkResponse.Success("Task created".toResponseBody(null)) } is NetworkResponse.Error -> response @@ -52,7 +50,7 @@ class TaskRepository(private val api: TaskApiInterface): BaseRepository() { try { return when (val response = withIdRequest { id -> api.getDailyTasks(id) }) { is NetworkResponse.Success -> { - val listTask: List = response.data.map { toTask(it) } + val listTask: List = response.data.map { it.toTask() } val map = listTask.groupBy { it.getTaskDate() } .mapValues { (_, taskList) -> DailySchedule().apply { diff --git a/app/src/main/java/org/hse/smartcalendar/ui/elements/ChartPie.kt b/app/src/main/java/org/hse/smartcalendar/ui/elements/ChartPie.kt index 0c72a9e..b7424dd 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/elements/ChartPie.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/elements/ChartPie.kt @@ -1,8 +1,6 @@ package org.hse.smartcalendar.ui.elements import androidx.compose.foundation.Canvas -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable diff --git a/app/src/main/java/org/hse/smartcalendar/ui/elements/MultipleLinearProgressBar.kt b/app/src/main/java/org/hse/smartcalendar/ui/elements/MultipleLinearProgressBar.kt index f85d226..cedcdfa 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/elements/MultipleLinearProgressBar.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/elements/MultipleLinearProgressBar.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.hse.smartcalendar.data.DailyTaskType -import org.hse.smartcalendar.ui.theme.* +import org.hse.smartcalendar.ui.theme.SmartCalendarTheme @Composable fun MultipleLinearProgressIndicator( diff --git a/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt b/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt index 0a3199c..2676126 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt @@ -26,9 +26,9 @@ import org.hse.smartcalendar.ui.screens.ChangeLogin import org.hse.smartcalendar.ui.screens.ChangePassword import org.hse.smartcalendar.ui.screens.GreetingScreen import org.hse.smartcalendar.ui.screens.LoadingScreen -import org.hse.smartcalendar.ui.task.DailyTasksList import org.hse.smartcalendar.ui.screens.SettingsScreen import org.hse.smartcalendar.ui.screens.StatisticsScreen +import org.hse.smartcalendar.ui.task.DailyTasksList import org.hse.smartcalendar.ui.task.TaskEditWindow import org.hse.smartcalendar.utility.AppDrawer import org.hse.smartcalendar.utility.Navigation diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt index 66c3159..37d2586 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt @@ -33,7 +33,10 @@ import androidx.lifecycle.viewmodel.compose.viewModel import org.hse.smartcalendar.R import org.hse.smartcalendar.ui.navigation.App import org.hse.smartcalendar.ui.navigation.TopButton -import org.hse.smartcalendar.ui.theme.* +import org.hse.smartcalendar.ui.theme.DarkBlue +import org.hse.smartcalendar.ui.theme.DarkRed +import org.hse.smartcalendar.ui.theme.Graphite +import org.hse.smartcalendar.ui.theme.Purple import org.hse.smartcalendar.ui.theme.SmartCalendarTheme import org.hse.smartcalendar.utility.Navigation import org.hse.smartcalendar.utility.Screens diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt index f01391a..ccec651 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.CircularProgressIndicator @@ -28,8 +27,8 @@ import org.hse.smartcalendar.ui.navigation.TopButton import org.hse.smartcalendar.ui.theme.SmartCalendarTheme import org.hse.smartcalendar.utility.Navigation import org.hse.smartcalendar.utility.rememberNavigation -import org.hse.smartcalendar.view.model.StatisticsViewModel import org.hse.smartcalendar.view.model.AbstractStatisticsViewModel.Companion.toPercent +import org.hse.smartcalendar.view.model.StatisticsViewModel @Composable fun StatisticsScreen(navigation: Navigation, openMenu: () -> Unit, statisticsModel: StatisticsViewModel) { diff --git a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt index 3cc58f8..1a5ed2b 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt @@ -27,11 +27,14 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -39,12 +42,14 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalTime @@ -53,6 +58,7 @@ import kotlinx.datetime.format.MonthNames import kotlinx.datetime.format.char import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.notification.ReminderViewModel import org.hse.smartcalendar.notification.ReminderViewModelFactory import org.hse.smartcalendar.ui.navigation.TopButton @@ -61,16 +67,10 @@ import org.hse.smartcalendar.utility.Navigation import org.hse.smartcalendar.utility.Screens import org.hse.smartcalendar.utility.rememberNavigation import org.hse.smartcalendar.view.model.ListViewModel -import java.io.File -import org.hse.smartcalendar.view.model.TaskEditViewModel -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.zIndex -import kotlinx.coroutines.delay -import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.view.model.StatisticsManager import org.hse.smartcalendar.view.model.StatisticsViewModel +import org.hse.smartcalendar.view.model.TaskEditViewModel +import java.io.File @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -105,6 +105,9 @@ fun DailyTasksList( showLoading = false } } + LaunchedEffect(Unit) { + viewModel.loadDailyTasks() + } Scaffold( topBar = { TopButton( diff --git a/app/src/main/java/org/hse/smartcalendar/utility/AppDriver.kt b/app/src/main/java/org/hse/smartcalendar/utility/AppDriver.kt index dfe9b77..2a82af9 100644 --- a/app/src/main/java/org/hse/smartcalendar/utility/AppDriver.kt +++ b/app/src/main/java/org/hse/smartcalendar/utility/AppDriver.kt @@ -1,9 +1,8 @@ package org.hse.smartcalendar.utility -import androidx.compose.material.icons.filled.Settings -import org.hse.smartcalendar.ui.theme.SmartCalendarTheme import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Icon import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.NavigationDrawerItem @@ -18,6 +17,7 @@ import org.hse.smartcalendar.ui.elements.Calendar_today import org.hse.smartcalendar.ui.elements.Finance import org.hse.smartcalendar.ui.elements.Follow_the_signs import org.hse.smartcalendar.ui.elements.Medal +import org.hse.smartcalendar.ui.theme.SmartCalendarTheme @Composable diff --git a/app/src/main/java/org/hse/smartcalendar/utility/ListViewUtility.kt b/app/src/main/java/org/hse/smartcalendar/utility/ListViewUtility.kt index 22ccec2..991ac0e 100644 --- a/app/src/main/java/org/hse/smartcalendar/utility/ListViewUtility.kt +++ b/app/src/main/java/org/hse/smartcalendar/utility/ListViewUtility.kt @@ -20,7 +20,7 @@ fun editHandler( isEmptyTitle.value = newTask.getDailyTaskTitle().isEmpty() isConflictInTimeField.value = newTask.getDailyTaskStartTime() > newTask.getDailyTaskEndTime() isNestedTask.value = !viewModel.isUpdatable(oldTask, newTask) - + newTask.setId(oldTask.getId()) if (!isEmptyTitle.value && !isConflictInTimeField.value && !isNestedTask.value) { oldTask.updateDailyTask(newTask) statsUpdateOldToNewTask(oldTask, newTask) diff --git a/app/src/main/java/org/hse/smartcalendar/utility/Serializer.kt b/app/src/main/java/org/hse/smartcalendar/utility/Serializer.kt index f1c05a7..53c010d 100644 --- a/app/src/main/java/org/hse/smartcalendar/utility/Serializer.kt +++ b/app/src/main/java/org/hse/smartcalendar/utility/Serializer.kt @@ -1,8 +1,11 @@ package org.hse.smartcalendar.utility import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.* -import kotlinx.serialization.encoding.* +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import java.util.UUID object UUIDSerializer : KSerializer { diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/AuthViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/AuthViewModel.kt index 0c54daa..84504c7 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/AuthViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/AuthViewModel.kt @@ -5,14 +5,12 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch -import org.hse.smartcalendar.data.User import org.hse.smartcalendar.network.ApiClient import org.hse.smartcalendar.network.CredentialsResponse import org.hse.smartcalendar.network.LoginRequest import org.hse.smartcalendar.network.LoginResponse import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.network.RegisterRequest -import org.hse.smartcalendar.network.UserInfoResponse import org.hse.smartcalendar.repository.AuthRepository class AuthViewModel : ViewModel() { diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt index 3d7bf63..51ef787 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt @@ -1,15 +1,17 @@ package org.hse.smartcalendar.view.model import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.work.WorkManager +import androidx.lifecycle.viewModelScope import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.workDataOf +import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.DatePeriod import kotlinx.datetime.LocalDate @@ -24,12 +26,9 @@ import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskAction import org.hse.smartcalendar.data.User import org.hse.smartcalendar.data.WorkManagerHolder -import org.hse.smartcalendar.network.ApiClient import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.notification.TaskApiWorker -import org.hse.smartcalendar.repository.TaskRepository import java.io.File -import java.util.concurrent.TimeUnit open class AbstractListViewModel(val statisticsManager: StatisticsManager) : ViewModel() { var _actionResult = MutableLiveData>() @@ -37,19 +36,19 @@ open class AbstractListViewModel(val statisticsManager: StatisticsManager) : Vie fun getScreenDate(): LocalDate{ return dailyTaskSchedule.date } - private var dailyTaskSchedule: DailySchedule + private lateinit var dailyTaskSchedule: DailySchedule private var dailyScheduleDate = mutableStateOf( Clock.System.now() .toLocalDateTime(TimeZone.currentSystemDefault()).date ) - var dailyTaskList: SnapshotStateList + val dailyTaskList: SnapshotStateList = mutableStateListOf() private val user: User = User init { - val date: LocalDate = - Clock.System.now() - .toLocalDateTime(TimeZone.currentSystemDefault()).date - dailyTaskSchedule = user.getSchedule().getOrCreateDailySchedule(date) - dailyTaskList = SnapshotStateList(dailyTaskSchedule.getDailyTaskList()) + loadDailyTasks() + } + fun loadDailyTasks(){ + dailyTaskSchedule = user.getSchedule().getOrCreateDailySchedule(dailyScheduleDate.value) + dailyTaskList.addAll(dailyTaskSchedule.getDailyTaskList()) } open fun scheduleTaskRequest(task: DailyTask, action: DailyTaskAction.Type) { } @@ -148,12 +147,10 @@ class ListViewModel(statisticsManager: StatisticsManager) : AbstractListViewMode val workRequest = OneTimeWorkRequestBuilder() .setInputData(workDataOf(DailyTaskAction.jsonName to taskJson)) - .setInitialDelay(10, TimeUnit.SECONDS) .build() - workManager.enqueueUniqueWork( - "task_${action}_${task.getId()}", - ExistingWorkPolicy.REPLACE, + "task_${task.getId()}", + ExistingWorkPolicy.APPEND, workRequest ) } diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/SettingsViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/SettingsViewModel.kt index 73d10cb..009f1aa 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/SettingsViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/SettingsViewModel.kt @@ -1,15 +1,10 @@ package org.hse.smartcalendar.view.model -import android.app.Application -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.initializer -import androidx.lifecycle.viewmodel.viewModelFactory //import dagger.hilt.android.lifecycle.HiltViewModel //import jakarta.inject.Inject +import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import org.hse.smartcalendar.notification.ReminderViewModel class SettingsViewModel constructor( ) : ViewModel() { diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt index 3dda4d2..1731920 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt @@ -1,13 +1,11 @@ package org.hse.smartcalendar.view.model -import android.app.Application import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkManager import androidx.work.workDataOf import kotlinx.coroutines.launch import kotlinx.serialization.json.Json @@ -28,7 +26,6 @@ import org.hse.smartcalendar.utility.TimePeriod import java.util.concurrent.TimeUnit import kotlin.math.max import kotlin.math.roundToInt -import kotlin.time.DurationUnit open class AbstractStatisticsViewModel():ViewModel() { private val statisticsRepo: StatisticsRepository = StatisticsRepository(ApiClient.statisticsApiService) @@ -176,7 +173,6 @@ class StatisticsViewModel(): AbstractStatisticsViewModel(){ val workRequest = OneTimeWorkRequestBuilder() .setInputData(workDataOf("statistics_json" to json)) - .setInitialDelay(10, TimeUnit.SECONDS) .build() workManager.enqueueUniqueWork( diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/TaskEditViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/TaskEditViewModel.kt index fbef06b..51687ed 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/TaskEditViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/TaskEditViewModel.kt @@ -2,7 +2,6 @@ package org.hse.smartcalendar.view.model import androidx.compose.runtime.MutableState import androidx.lifecycle.ViewModel -import androidx.work.WorkManager import kotlinx.datetime.LocalTime import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskAction @@ -42,12 +41,10 @@ class TaskEditViewModel( isEmptyTitle = isEmptyTitle, isConflictInTimeField = isConflictInTimeField, isNestedTask = isNestedTask, - statsUpdateOldToNewTask = { oldTask, newTask - ->{ + statsUpdateOldToNewTask = { oldTask, newTask-> listViewModel.statisticsManager.updateDailyTask(oldTask=oldTask, newTask = newTask) listViewModel.scheduleTaskRequest(newTask, DailyTaskAction.Type.EDIT) } - } ) } } \ No newline at end of file From 35a00ea3e1ef2fbb190419218b1b35b3f4afd7c3 Mon Sep 17 00:00:00 2001 From: Usatov Pavel Date: Sat, 7 Jun 2025 13:53:43 +0300 Subject: [PATCH 02/11] Statistics: CalculableVars Add StatisticsCalculator class, which support update typesInDay, continues Current/Success days in statistics Rename notification lib to work --- .../activity/DailyTasksListActivity.kt | 4 +- .../smartcalendar/network/StatisticsData.kt | 4 +- .../hse/smartcalendar/ui/navigation/App.kt | 7 +- .../{utility => ui/navigation}/AppDriver.kt | 5 +- .../smartcalendar/ui/screens/LoadingScreen.kt | 5 +- .../ui/screens/SettingsScreen.kt | 2 +- .../ui/screens/StatisticsScreen.kt | 6 +- .../smartcalendar/ui/task/DailyTasksList.kt | 7 +- .../utility/StatisticsCalculator.kt | 86 +++++++++++++++++++ .../smartcalendar/utility/TimePeriodUtils.kt | 10 +-- .../smartcalendar/view/model/ListViewModel.kt | 8 +- .../model}/ReminderViewModel.kt | 15 ++-- .../view/model/StatisticsManager.kt | 13 ++- .../view/model/StatisticsViewModel.kt | 47 +++++----- .../{notification => work}/ReminderWorker.kt | 2 +- .../StatisticsUploadWorker.kt | 2 +- .../{notification => work}/TaskApiWorker.kt | 2 +- 17 files changed, 152 insertions(+), 73 deletions(-) rename app/src/main/java/org/hse/smartcalendar/{utility => ui/navigation}/AppDriver.kt (94%) create mode 100644 app/src/main/java/org/hse/smartcalendar/utility/StatisticsCalculator.kt rename app/src/main/java/org/hse/smartcalendar/{notification => view/model}/ReminderViewModel.kt (82%) rename app/src/main/java/org/hse/smartcalendar/{notification => work}/ReminderWorker.kt (98%) rename app/src/main/java/org/hse/smartcalendar/{notification => work}/StatisticsUploadWorker.kt (95%) rename app/src/main/java/org/hse/smartcalendar/{notification => work}/TaskApiWorker.kt (96%) diff --git a/app/src/main/java/org/hse/smartcalendar/activity/DailyTasksListActivity.kt b/app/src/main/java/org/hse/smartcalendar/activity/DailyTasksListActivity.kt index dfc7df6..68a6aa4 100644 --- a/app/src/main/java/org/hse/smartcalendar/activity/DailyTasksListActivity.kt +++ b/app/src/main/java/org/hse/smartcalendar/activity/DailyTasksListActivity.kt @@ -11,8 +11,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import org.hse.smartcalendar.data.WorkManagerHolder -import org.hse.smartcalendar.notification.ReminderViewModel -import org.hse.smartcalendar.notification.ReminderViewModelFactory +import org.hse.smartcalendar.view.model.ReminderViewModel +import org.hse.smartcalendar.view.model.ReminderViewModelFactory import org.hse.smartcalendar.ui.screens.StatisticsScreen import org.hse.smartcalendar.ui.task.DailyTasksList import org.hse.smartcalendar.ui.task.TaskEditWindow diff --git a/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt b/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt index dede84f..42d6199 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt @@ -28,8 +28,8 @@ data class StatisticsDTO( completed = viewModel.getTodayCompletedTime().time.inWholeMinutes ), continuesSuccessDays = ContinuesSuccessDays( - record = viewModel.getRecordContiniusSuccessDays().amount.toLong(), - now = viewModel.getTodayContinusSuccessDays().amount.toLong() + record = viewModel.getRecordContinuesSuccessDays().amount.toLong(), + now = viewModel.getTodayContinuesSuccessDays().amount.toLong() ), averageDayTime = AverageDayTime( totalWorkMinutes = viewModel.getTotalWorkTime().time.inWholeMinutes, diff --git a/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt b/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt index 2676126..6b9949c 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt @@ -19,8 +19,8 @@ import kotlinx.coroutines.launch import org.hse.smartcalendar.AuthScreen import org.hse.smartcalendar.AuthType import org.hse.smartcalendar.AuthViewModel -import org.hse.smartcalendar.notification.ReminderViewModel -import org.hse.smartcalendar.notification.ReminderViewModelFactory +import org.hse.smartcalendar.view.model.ReminderViewModel +import org.hse.smartcalendar.view.model.ReminderViewModelFactory import org.hse.smartcalendar.ui.screens.AchievementsScreen import org.hse.smartcalendar.ui.screens.ChangeLogin import org.hse.smartcalendar.ui.screens.ChangePassword @@ -30,7 +30,6 @@ import org.hse.smartcalendar.ui.screens.SettingsScreen import org.hse.smartcalendar.ui.screens.StatisticsScreen import org.hse.smartcalendar.ui.task.DailyTasksList import org.hse.smartcalendar.ui.task.TaskEditWindow -import org.hse.smartcalendar.utility.AppDrawer import org.hse.smartcalendar.utility.Navigation import org.hse.smartcalendar.utility.Screens import org.hse.smartcalendar.utility.rememberNavigation @@ -105,7 +104,7 @@ fun NestedNavigator(navigation: Navigation, authModel: AuthViewModel,openDrawer: AuthScreen(navigation, authModel, AuthType.Login) } composable(Screens.LOADING.route) { - LoadingScreen(navigation, statisticsModel) + LoadingScreen(navigation, statisticsModel, listModel) } } diff --git a/app/src/main/java/org/hse/smartcalendar/utility/AppDriver.kt b/app/src/main/java/org/hse/smartcalendar/ui/navigation/AppDriver.kt similarity index 94% rename from app/src/main/java/org/hse/smartcalendar/utility/AppDriver.kt rename to app/src/main/java/org/hse/smartcalendar/ui/navigation/AppDriver.kt index 2a82af9..b698324 100644 --- a/app/src/main/java/org/hse/smartcalendar/utility/AppDriver.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/navigation/AppDriver.kt @@ -1,4 +1,4 @@ -package org.hse.smartcalendar.utility +package org.hse.smartcalendar.ui.navigation import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons @@ -18,6 +18,9 @@ import org.hse.smartcalendar.ui.elements.Finance import org.hse.smartcalendar.ui.elements.Follow_the_signs import org.hse.smartcalendar.ui.elements.Medal import org.hse.smartcalendar.ui.theme.SmartCalendarTheme +import org.hse.smartcalendar.utility.Navigation +import org.hse.smartcalendar.utility.Screens +import org.hse.smartcalendar.utility.rememberNavigation @Composable diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/LoadingScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/LoadingScreen.kt index bf74262..6bb77fa 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/LoadingScreen.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/LoadingScreen.kt @@ -22,9 +22,9 @@ import org.hse.smartcalendar.utility.Navigation import org.hse.smartcalendar.utility.Screens import org.hse.smartcalendar.view.model.InitViewModel import org.hse.smartcalendar.view.model.StatisticsViewModel - +import org.hse.smartcalendar.view.model.ListViewModel @Composable -fun LoadingScreen(navigation: Navigation, statisticsVM: StatisticsViewModel){ +fun LoadingScreen(navigation: Navigation, statisticsVM: StatisticsViewModel, listModel: ListViewModel){ val initVM: InitViewModel = viewModel()//гарантирует 1 модель на Composable val initState by initVM.initResult.observeAsState() val statisticsState by statisticsVM.initResult.observeAsState() @@ -59,6 +59,7 @@ fun LoadingScreen(navigation: Navigation, statisticsVM: StatisticsViewModel){ initState is NetworkResponse.Success && statisticsState is NetworkResponse.Success-> { LaunchedEffect(statisticsState) { delay(1000) + listModel.loadDailyTasks() navigation.navigateToMainApp(Screens.CALENDAR.route) } } diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/SettingsScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/SettingsScreen.kt index 7863f3b..f29b1f4 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/SettingsScreen.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel import org.hse.smartcalendar.AuthViewModel -import org.hse.smartcalendar.notification.ReminderViewModel +import org.hse.smartcalendar.view.model.ReminderViewModel import org.hse.smartcalendar.ui.elements.Password import org.hse.smartcalendar.ui.elements.Person import org.hse.smartcalendar.ui.elements.Reminder diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt index ccec651..d067a7f 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt @@ -61,8 +61,8 @@ fun StatisticsScreen(navigation: Navigation, openMenu: () -> Unit, statisticsMod modifier = Modifier.weight(1f / 3f) ) SafeProgressBox( - dividend = statisticsModel.getTodayContinusSuccessDays().amount.toLong(), - divisor = statisticsModel.getRecordContiniusSuccessDays().amount.toLong(), + dividend = statisticsModel.getTodayContinuesSuccessDays().amount.toLong(), + divisor = statisticsModel.getRecordContinuesSuccessDays().amount.toLong(), label = "Days in a row, when completed all the tasks", color = Color.Red, modifier = Modifier.weight(1f / 3f) @@ -83,7 +83,7 @@ fun StatisticsScreen(navigation: Navigation, openMenu: () -> Unit, statisticsMod Text(statisticsModel.getAverageDailyTime().toFullString()) Text(statisticsModel.getTodayPlannedTime().toFullString()) Text(statisticsModel.getTotalWorkTime().toPrettyString()) - Text(statisticsModel.getRecordContiniusSuccessDays().toPrettyString()) + Text(statisticsModel.getRecordContinuesSuccessDays().toPrettyString()) } } } diff --git a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt index 1a5ed2b..bbe9ab8 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt @@ -59,8 +59,8 @@ import kotlinx.datetime.format.char import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType import org.hse.smartcalendar.network.NetworkResponse -import org.hse.smartcalendar.notification.ReminderViewModel -import org.hse.smartcalendar.notification.ReminderViewModelFactory +import org.hse.smartcalendar.view.model.ReminderViewModel +import org.hse.smartcalendar.view.model.ReminderViewModelFactory import org.hse.smartcalendar.ui.navigation.TopButton import org.hse.smartcalendar.ui.theme.SmartCalendarTheme import org.hse.smartcalendar.utility.Navigation @@ -105,9 +105,6 @@ fun DailyTasksList( showLoading = false } } - LaunchedEffect(Unit) { - viewModel.loadDailyTasks() - } Scaffold( topBar = { TopButton( diff --git a/app/src/main/java/org/hse/smartcalendar/utility/StatisticsCalculator.kt b/app/src/main/java/org/hse/smartcalendar/utility/StatisticsCalculator.kt new file mode 100644 index 0000000..6ee2dd1 --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/utility/StatisticsCalculator.kt @@ -0,0 +1,86 @@ +package org.hse.smartcalendar.utility + +import androidx.compose.runtime.mutableStateOf +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate +import kotlinx.datetime.daysUntil +import kotlinx.datetime.minus +import org.hse.smartcalendar.data.DailyTask +import org.hse.smartcalendar.data.User +import androidx.compose.runtime.State +import org.hse.smartcalendar.network.StatisticsDTO + +data class StatisticsCalculableData( + val typesInDay: Int, + val continuesCurrent: Int, + val continuesTotal: Int +) +class StatisticsCalculator { + private val _stats = mutableStateOf(StatisticsCalculableData(0,0,0)) + private val stats: State = _stats + private fun recalculateTypes(dayTasks: List ){ + val state = stats.value + val dayTypes = dayTasks + .map { it.getDailyTaskType() } + .toSet() + .size + _stats.value = StatisticsCalculableData(dayTypes, + continuesTotal = state.continuesTotal, + continuesCurrent = state.continuesCurrent) + } + private fun recalculateDays(){ + val currentDate = TimeUtils.getCurrentDateTime().date + var lastSuccessDate = currentDate + val mainSchedule = User.getSchedule() + val isSuccess: (LocalDate)->Boolean = { date -> + (mainSchedule + .getOrCreateDailySchedule(lastSuccessDate) + .getDailyTaskList() + .map { it.isComplete() } + .find { it.not() } != null) + } + while (isSuccess(lastSuccessDate)){ + lastSuccessDate.minus(1, DateTimeUnit.DAY) + } + val continuesCurrent = lastSuccessDate.daysUntil(currentDate) + val continuesTotal = maxOf(stats.value.continuesTotal, continuesCurrent) + _stats.value = StatisticsCalculableData( + typesInDay = stats.value.typesInDay, + continuesTotal = continuesTotal, + continuesCurrent = continuesCurrent + ) + } + private fun recalculateDays(changedDate: LocalDate){ + val currentDate = TimeUtils.getCurrentDateTime().date + if (changedDate>currentDate + || changedDate.daysUntil(currentDate)>stats.value.continuesCurrent){ + return + } + recalculateDays() + } + fun addOrDeleteTask(currentDateTasks: List ){ + recalculateTypes(currentDateTasks) + } + fun changeTaskCompletion(task: DailyTask){ + recalculateDays(task.getTaskDate()) + } + fun init(statisticsDTO: StatisticsDTO){ + _stats.value = StatisticsCalculableData(typesInDay = 0, + continuesTotal = statisticsDTO.continuesSuccessDays.record.toInt(), + continuesCurrent = 0) + recalculateDays() + val currentDate = TimeUtils.getCurrentDateTime().date + val tasks = User.getSchedule().getOrCreateDailySchedule(currentDate) + .getDailyTaskList() + recalculateTypes(tasks) + } + fun getCurrentDayTypes(): Int{ + return stats.value.typesInDay + } + fun getTodayContinuesSuccessDays(): Int{ + return stats.value.continuesCurrent + } + fun getRecordContinuesSuccessDays(): Int{ + return stats.value.continuesTotal + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/utility/TimePeriodUtils.kt b/app/src/main/java/org/hse/smartcalendar/utility/TimePeriodUtils.kt index 72b4dac..1d966aa 100644 --- a/app/src/main/java/org/hse/smartcalendar/utility/TimePeriodUtils.kt +++ b/app/src/main/java/org/hse/smartcalendar/utility/TimePeriodUtils.kt @@ -38,7 +38,6 @@ class TimeUtils { } } -//every class in Kotlin is final by default, so inheritance from LocalTime not ok class TimePeriod(minute: Long) { private var _time = mutableStateOf(Duration.ZERO) val time: Duration get() = _time.value @@ -73,14 +72,7 @@ class TimePeriod(minute: Long) { } } -class DaysAmount(initialAmount: Int) { - private var _amount = mutableStateOf(initialAmount) - val amount: Int get() = _amount.value - - init { - _amount.value = amount - } - +data class DaysAmount(val amount: Int) { fun toPrettyString(): String { return if (amount != 1) "$amount days" else "1 day" } diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt index 51ef787..0cd6731 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt @@ -7,11 +7,9 @@ import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.workDataOf -import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.DatePeriod import kotlinx.datetime.LocalDate @@ -27,7 +25,7 @@ import org.hse.smartcalendar.data.DailyTaskAction import org.hse.smartcalendar.data.User import org.hse.smartcalendar.data.WorkManagerHolder import org.hse.smartcalendar.network.NetworkResponse -import org.hse.smartcalendar.notification.TaskApiWorker +import org.hse.smartcalendar.work.TaskApiWorker import java.io.File open class AbstractListViewModel(val statisticsManager: StatisticsManager) : ViewModel() { @@ -62,7 +60,7 @@ open class AbstractListViewModel(val statisticsManager: StatisticsManager) : Vie dailyTaskList.sortBy { task -> task.getDailyTaskStartTime() } - statisticsManager.addDailyTask(newTask) + statisticsManager.addDailyTask(newTask, dailyTaskList) scheduleTaskRequest(newTask, DailyTaskAction.Type.ADD) } @@ -71,7 +69,7 @@ open class AbstractListViewModel(val statisticsManager: StatisticsManager) : Vie // TODO } else { dailyTaskList.remove(task) - statisticsManager.removeDailyTask(task) + statisticsManager.removeDailyTask(task, dailyTaskList) scheduleTaskRequest(task, DailyTaskAction.Type.DELETE) } } diff --git a/app/src/main/java/org/hse/smartcalendar/notification/ReminderViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/ReminderViewModel.kt similarity index 82% rename from app/src/main/java/org/hse/smartcalendar/notification/ReminderViewModel.kt rename to app/src/main/java/org/hse/smartcalendar/view/model/ReminderViewModel.kt index 01c7b2e..bf48d80 100644 --- a/app/src/main/java/org/hse/smartcalendar/notification/ReminderViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/ReminderViewModel.kt @@ -1,4 +1,4 @@ -package org.hse.smartcalendar.notification +package org.hse.smartcalendar.view.model import android.app.Application import androidx.lifecycle.ViewModel @@ -14,6 +14,7 @@ import kotlinx.datetime.LocalTime import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.utility.prettyPrint import org.hse.smartcalendar.utility.toEpochMilliseconds +import org.hse.smartcalendar.work.ReminderWorker import java.util.concurrent.TimeUnit import kotlin.time.DurationUnit import kotlin.time.toDuration @@ -60,12 +61,12 @@ class ReminderViewModel(application: Application): ViewModel() { } myWorkRequestBuilder.setInputData( workDataOf( - ReminderWorker.TYPE_KEY to task.getDailyTaskType().toString().lowercase(), - ReminderWorker.BEFORE_KEY to realMinutesBefore, - ReminderWorker.TITLE_KEY to task.getDailyTaskTitle(), - ReminderWorker.MESSAGE_KEY to task.getDailyTaskDescription(), - ReminderWorker.START_KEY to LocalTime.prettyPrint(task.getDailyTaskStartTime()), - ReminderWorker.END_KEY to LocalTime.prettyPrint(task.getDailyTaskEndTime()), + ReminderWorker.Companion.TYPE_KEY to task.getDailyTaskType().toString().lowercase(), + ReminderWorker.Companion.BEFORE_KEY to realMinutesBefore, + ReminderWorker.Companion.TITLE_KEY to task.getDailyTaskTitle(), + ReminderWorker.Companion.MESSAGE_KEY to task.getDailyTaskDescription(), + ReminderWorker.Companion.START_KEY to LocalTime.prettyPrint(task.getDailyTaskStartTime()), + ReminderWorker.Companion.END_KEY to LocalTime.prettyPrint(task.getDailyTaskEndTime()), ) ) workManager.enqueue(myWorkRequestBuilder.build()) diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsManager.kt b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsManager.kt index c3f52a1..abe3cde 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsManager.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsManager.kt @@ -9,11 +9,16 @@ class StatisticsManager(private val viewModel: AbstractStatisticsViewModel){ fun changeTaskCompletion(task: DailyTask, isCompleted: Boolean){ viewModel.changeTaskCompletion(task, isCompleted) } - fun removeDailyTask(task: DailyTask){ - viewModel.createOrDeleteTask(task, false) + fun removeDailyTask(task: DailyTask, dailyTaskList: List){ + viewModel.createOrDeleteTask(dailyTaskList = dailyTaskList, + task = task, + isCreate = false) } - fun addDailyTask(task: DailyTask){ - viewModel.createOrDeleteTask(task, true) + fun addDailyTask(task: DailyTask, dailyTaskList: List){ + viewModel.createOrDeleteTask( + dailyTaskList= dailyTaskList, + task = task, + isCreate = false) } /** diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt index 1731920..38f8196 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt @@ -14,16 +14,15 @@ import org.hse.smartcalendar.data.TotalTimeTaskTypes import org.hse.smartcalendar.data.WorkManagerHolder import org.hse.smartcalendar.network.ApiClient import org.hse.smartcalendar.network.AverageDayTime -import org.hse.smartcalendar.network.ContinuesSuccessDays import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.network.StatisticsDTO import org.hse.smartcalendar.network.TodayTime -import org.hse.smartcalendar.notification.StatisticsUploadWorker +import org.hse.smartcalendar.work.StatisticsUploadWorker import org.hse.smartcalendar.repository.StatisticsRepository import org.hse.smartcalendar.utility.DayPeriod import org.hse.smartcalendar.utility.DaysAmount +import org.hse.smartcalendar.utility.StatisticsCalculator import org.hse.smartcalendar.utility.TimePeriod -import java.util.concurrent.TimeUnit import kotlin.math.max import kotlin.math.roundToInt @@ -56,16 +55,6 @@ open class AbstractStatisticsViewModel():ViewModel() { } } } - private class ContinuesSuccessDaysVars(record: Int, now: Int){ - val Record: DaysAmount = DaysAmount(record) - val Now: DaysAmount = DaysAmount(now) - companion object{ - fun fromContinuesSuccessDTO(continuesSuccessDTO: ContinuesSuccessDays): ContinuesSuccessDaysVars{ - return ContinuesSuccessDaysVars(record = continuesSuccessDTO.record.toInt(), - now = continuesSuccessDTO.now.toInt()) - } - } - } private class AverageDayTimeVars(totalWorkMinutes: Long, val totalDays: Long){ var All: DayPeriod = DayPeriod(totalWorkMinutes/totalDays) fun update(totalTimeMinutes: Long){ @@ -79,11 +68,12 @@ open class AbstractStatisticsViewModel():ViewModel() { } } } - private var ContiniusSuccessDays: ContinuesSuccessDaysVars = ContinuesSuccessDaysVars(0, 0) + private var TotalTime: TotalTimeTaskTypes = TotalTimeTaskTypes(0, 0, 0, 0) private var weekTime = WeekTime(0) private var AverageDayTime: AverageDayTimeVars = AverageDayTimeVars(totalDays = 1, totalWorkMinutes = 0) private var TodayTime: TodayTimeVars = TodayTimeVars(0, 0) + private var statisticsCalculator: StatisticsCalculator = StatisticsCalculator() fun init(){ viewModelScope.launch { _initResult.value = NetworkResponse.Loading @@ -93,22 +83,26 @@ open class AbstractStatisticsViewModel():ViewModel() { TotalTime = data.totalTime.toVMTotalTime() AverageDayTime = AverageDayTimeVars.fromAverageDayDTO(data.averageDayTime) weekTime = WeekTime(data.weekTime) - ContiniusSuccessDays = ContinuesSuccessDaysVars.fromContinuesSuccessDTO(data.continuesSuccessDays) TodayTime = TodayTimeVars.fromTodayTimeDTO(data.todayTime) + statisticsCalculator.init(data) } _initResult.value = response } } open fun uploadStatistics(){ } - fun createOrDeleteTask(task: DailyTask, isCreate: Boolean, isUploadStats: Boolean =true){ + private fun createOrDeleteTask(task: DailyTask, isCreate: Boolean){ if (task.isComplete() && isCreate==false){ changeTaskCompletion(task, false) } if (task.belongsCurrentDay()){ TodayTime.Planned.plusMinutes(task.getMinutesLength(), isCreate) } - if (isUploadStats) {uploadStatistics()} + } + fun createOrDeleteTask(task: DailyTask, isCreate: Boolean, dailyTaskList: List){ + createOrDeleteTask(task, isCreate) + statisticsCalculator.addOrDeleteTask(dailyTaskList) + uploadStatistics() } fun changeTaskCompletion(task: DailyTask, isComplete: Boolean, isUploadStats: Boolean =true){//когда таска запатчена @@ -121,6 +115,7 @@ open class AbstractStatisticsViewModel():ViewModel() { } TotalTime.completeTask(task, isComplete) AverageDayTime.update(TotalTime.totalMinutes) + statisticsCalculator.changeTaskCompletion(task) if (isUploadStats) {uploadStatistics()} } @@ -129,8 +124,8 @@ open class AbstractStatisticsViewModel():ViewModel() { changeTaskCompletion(task, false, isUploadStats = false) changeTaskCompletion(newTask, true, isUploadStats = false) } - createOrDeleteTask(task, false, isUploadStats = false) - createOrDeleteTask(newTask, true, isUploadStats = false) + createOrDeleteTask(task, false) + createOrDeleteTask(newTask, true) uploadStatistics() } @@ -149,11 +144,11 @@ open class AbstractStatisticsViewModel():ViewModel() { return TodayTime.Completed } - fun getRecordContiniusSuccessDays():DaysAmount{ - return ContiniusSuccessDays.Record + fun getRecordContinuesSuccessDays():DaysAmount{ + return DaysAmount(statisticsCalculator.getRecordContinuesSuccessDays()) } - fun getTodayContinusSuccessDays():DaysAmount{ - return ContiniusSuccessDays.Now + fun getTodayContinuesSuccessDays():DaysAmount{ + return DaysAmount(statisticsCalculator.getTodayContinuesSuccessDays()) } fun getTotalTimeActivityTypes():TotalTimeTaskTypes{ return TotalTimeTaskTypes(TotalTime.Common.time.inWholeMinutes, TotalTime.Work.time.inWholeMinutes, TotalTime.Study.time.inWholeMinutes, TotalTime.Fitness.time.inWholeMinutes) @@ -161,9 +156,11 @@ open class AbstractStatisticsViewModel():ViewModel() { fun getWeekWorkTime(): TimePeriod{ return weekTime.All } - fun getTypesInDay(): Long{ - return 2 + fun getTypesInCurrentDay(): Int{ + return statisticsCalculator.getCurrentDayTypes() } + + } class StatisticsViewModel(): AbstractStatisticsViewModel(){ override fun uploadStatistics() { diff --git a/app/src/main/java/org/hse/smartcalendar/notification/ReminderWorker.kt b/app/src/main/java/org/hse/smartcalendar/work/ReminderWorker.kt similarity index 98% rename from app/src/main/java/org/hse/smartcalendar/notification/ReminderWorker.kt rename to app/src/main/java/org/hse/smartcalendar/work/ReminderWorker.kt index cbf3578..038d4ab 100644 --- a/app/src/main/java/org/hse/smartcalendar/notification/ReminderWorker.kt +++ b/app/src/main/java/org/hse/smartcalendar/work/ReminderWorker.kt @@ -1,4 +1,4 @@ -package org.hse.smartcalendar.notification +package org.hse.smartcalendar.work import android.annotation.SuppressLint import android.app.PendingIntent diff --git a/app/src/main/java/org/hse/smartcalendar/notification/StatisticsUploadWorker.kt b/app/src/main/java/org/hse/smartcalendar/work/StatisticsUploadWorker.kt similarity index 95% rename from app/src/main/java/org/hse/smartcalendar/notification/StatisticsUploadWorker.kt rename to app/src/main/java/org/hse/smartcalendar/work/StatisticsUploadWorker.kt index 1e9de03..36b5aa7 100644 --- a/app/src/main/java/org/hse/smartcalendar/notification/StatisticsUploadWorker.kt +++ b/app/src/main/java/org/hse/smartcalendar/work/StatisticsUploadWorker.kt @@ -1,4 +1,4 @@ -package org.hse.smartcalendar.notification +package org.hse.smartcalendar.work import android.content.Context import androidx.work.CoroutineWorker diff --git a/app/src/main/java/org/hse/smartcalendar/notification/TaskApiWorker.kt b/app/src/main/java/org/hse/smartcalendar/work/TaskApiWorker.kt similarity index 96% rename from app/src/main/java/org/hse/smartcalendar/notification/TaskApiWorker.kt rename to app/src/main/java/org/hse/smartcalendar/work/TaskApiWorker.kt index e1f0ec3..57ccc82 100644 --- a/app/src/main/java/org/hse/smartcalendar/notification/TaskApiWorker.kt +++ b/app/src/main/java/org/hse/smartcalendar/work/TaskApiWorker.kt @@ -1,4 +1,4 @@ -package org.hse.smartcalendar.notification +package org.hse.smartcalendar.work import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters From fa3c7fa9a9564671e9286e4475118a3a6bb140a0 Mon Sep 17 00:00:00 2001 From: Usatov Pavel Date: Sat, 7 Jun 2025 19:53:21 +0300 Subject: [PATCH 03/11] Achievements: Test Add test for Achievement Refactor: Add AchievementType enum which provide data to AchievementCard --- .../ui/screens/AchievementsScreenTest.kt | 87 +++++++++++++++++++ .../ui/screens/AchievementsScreen.kt | 84 ++++-------------- .../ui/screens/model/AchievmentType.kt | 64 ++++++++++++++ .../view/model/StatisticsManager.kt | 2 +- 4 files changed, 171 insertions(+), 66 deletions(-) create mode 100644 app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt create mode 100644 app/src/main/java/org/hse/smartcalendar/ui/screens/model/AchievmentType.kt diff --git a/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt new file mode 100644 index 0000000..ae7e0dd --- /dev/null +++ b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt @@ -0,0 +1,87 @@ +package org.hse.smartcalendar.ui.screens + +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.datetime.LocalTime +import org.hse.smartcalendar.data.DailyTask +import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.ui.screens.model.AchievementType +import org.hse.smartcalendar.ui.theme.SmartCalendarTheme +import org.hse.smartcalendar.utility.TimeUtils +import org.hse.smartcalendar.utility.fromMinutesOfDay +import org.hse.smartcalendar.utility.rememberNavigation +import org.hse.smartcalendar.view.model.AbstractListViewModel +import org.hse.smartcalendar.view.model.AbstractStatisticsViewModel +import org.hse.smartcalendar.view.model.StatisticsManager +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.UUID +@RunWith(AndroidJUnit4::class) +class AchievementsScreenTest { + private lateinit var firstTask: DailyTask + private lateinit var secondTask: DailyTask + @get:Rule + val composeTestRule = createComposeRule() + @Before + fun initTasks(){ + firstTask = DailyTask( + title = "first", + id = UUID.randomUUID(), + isComplete = false, + type = DailyTaskType.WORK, + creationTime = TimeUtils.Companion.getCurrentDateTime(), + description = "", + start = LocalTime.Companion.fromMinutesOfDay(0), + end = LocalTime.Companion.fromMinutesOfDay(300), + date = TimeUtils.Companion.getCurrentDateTime().date, + ) + secondTask = DailyTask( + title = "first", + id = UUID.randomUUID(), + isComplete = false, + type = DailyTaskType.WORK, + creationTime = TimeUtils.Companion.getCurrentDateTime(), + description = "", + start = LocalTime.Companion.fromMinutesOfDay(300), + end = LocalTime.Companion.fromMinutesOfDay(1440-1), + date = TimeUtils.Companion.getCurrentDateTime().date, + ) + } + fun assertAchievementData(type: AchievementType, text: String){ + composeTestRule + .onNodeWithTag(type.testTag) + .onChildren() + .filter(hasText(text)) + .onFirst() + .assertExists() + } + @Test + fun achievementsShowsStreak() { + val statisticsViewModel = AbstractStatisticsViewModel() + val listViewModel = AbstractListViewModel(StatisticsManager(statisticsViewModel)) +//нужно потестить каждый элемент:Planning everything - без заданий 0, +// с заданием 5ч 5/10, c 24ч 24 часа + composeTestRule.setContent { + SmartCalendarTheme { + AchievementsScreen( + navigation = rememberNavigation(), + openDrawer = {}, + statisticsModel = statisticsViewModel + ) + } + } + + composeTestRule.onNodeWithText("Eternal Flame").assertIsDisplayed() + listViewModel.addDailyTask(firstTask) + composeTestRule.runOnIdle { + } + assertAchievementData(AchievementType.PlanToday, "5/10") + listViewModel.addDailyTask(secondTask) + composeTestRule.runOnIdle { + } + assertAchievementData(AchievementType.PlanToday, "24/24") + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt index 37d2586..1d5a3a7 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight @@ -33,6 +34,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import org.hse.smartcalendar.R import org.hse.smartcalendar.ui.navigation.App import org.hse.smartcalendar.ui.navigation.TopButton +import org.hse.smartcalendar.ui.screens.model.AchievementType import org.hse.smartcalendar.ui.theme.DarkBlue import org.hse.smartcalendar.ui.theme.DarkRed import org.hse.smartcalendar.ui.theme.Graphite @@ -40,60 +42,17 @@ import org.hse.smartcalendar.ui.theme.Purple import org.hse.smartcalendar.ui.theme.SmartCalendarTheme import org.hse.smartcalendar.utility.Navigation import org.hse.smartcalendar.utility.Screens +import org.hse.smartcalendar.view.model.AbstractStatisticsViewModel import org.hse.smartcalendar.view.model.ListViewModel import org.hse.smartcalendar.view.model.StatisticsManager import org.hse.smartcalendar.view.model.StatisticsViewModel import org.hse.smartcalendar.view.model.TaskEditViewModel -import kotlin.time.DurationUnit @Composable fun AchievementsScreen(navigation: Navigation, openDrawer: (()->Unit)?=null, - statisticsModel: StatisticsViewModel) { - val fire = AchievementData( - "Eternal Flame", - R.drawable.fire, - { i -> "Reach a $i day streak" }, - statisticsModel.getRecordContiniusSuccessDays().amount.toLong(), - listOf(5, 10, 20, 50, 100) - ) - val plan = AchievementData( - "Planning everything", - R.drawable.writing_hand, - { i -> "Plan $i hours of your time today" }, - statisticsModel.getTodayPlannedTime().toMinutes() / 60, - listOf(5, 10, 20, 24) - ) - val common = AchievementData( - "Types are boring", - R.drawable.yawning_face, - { i -> "Spend $i hours with common tasks" }, - statisticsModel.getTotalTimeActivityTypes().Common.time.toLong(DurationUnit.HOURS), - listOf(10, 20, 50, 100, 1000) - ) - val taskByTask = AchievementData( - "Hour by Hour", - R.drawable.tasks_complete, - { i -> "Work a total of $i hours" }, - statisticsModel.getTotalWorkTime().toMinutes()/60, - listOf(10, 20, 50, 100, 1000) - ) - val automatic = AchievementData( - "Mechanical Focus", - R.drawable.robot, - { i -> "Work a total of $i hours last week" }, - statisticsModel.getWeekWorkTime().toMinutes()/60/7, - listOf(4, 6, 8, 10) - ) - val totallyBalanced = AchievementData( - "Totally balanced", - R.drawable.balance_scale, - { i -> "Have $i types of task in day" }, - statisticsModel.getTypesInDay(), - listOf(4) - ) - val itemsData = - listOf(fire, plan, common, taskByTask, automatic, totallyBalanced) + statisticsModel: AbstractStatisticsViewModel) { + val itemsData = AchievementType.entries.toTypedArray() Scaffold( topBar = { TopButton(openDrawer, navigation, "Achievements") } ) {paddingValues-> @@ -102,7 +61,11 @@ fun AchievementsScreen(navigation: Navigation, .padding(paddingValues), content = { items(itemsData.size) { index -> - AchievementCard(itemsData[index]) + val parameterProvider = itemsData[index].parameterProvider + AchievementCard( + itemsData[index], + parameterProvider.invoke(statisticsModel) + ) } } ) @@ -111,23 +74,25 @@ fun AchievementsScreen(navigation: Navigation, } @Composable -fun AchievementCard(data: AchievementData) { +fun AchievementCard(data: AchievementType, + parameter: Long) { val colors: List = listOf(colorResource(R.color.bronze), Color.LightGray, Color.Yellow, colorResource(R.color.emerald), Color.Cyan, Color.Cyan) val backColors: List = listOf(DarkBlue, Graphite, Purple, Color.DarkGray, DarkRed, DarkRed) var level = 0 var description = data.description val lastLevel = data.levels.takeLast(1)[0] - if (lastLevel <= data.parameter) { + if (lastLevel <= parameter) { description = { long -> "You have max level, you achieve:" + data.description(lastLevel) } level = data.levels.size-1 } else { - while (data.levels[level] <= data.parameter) { + while (data.levels[level] <= parameter) { level++ } } Row( Modifier .padding(32.dp) + .testTag(data.testTag) ) { Box(modifier = Modifier.weight(0.3f) .wrapContentSize(align = Alignment.Center) @@ -165,14 +130,14 @@ fun AchievementCard(data: AchievementData) { fontWeight = FontWeight.SemiBold ) Text( - data.parameter.toString() + "/" + data.levels[level].toString(), + parameter.toString() + "/" + data.levels[level].toString(), fontSize = 14.sp, fontWeight = FontWeight.Light ) } LinearProgressIndicator( - progress = { data.parameter.toFloat() / data.levels[level] }, + progress = {parameter.toFloat() / data.levels[level] }, color = Color.Yellow, trackColor = Color.LightGray, modifier = Modifier @@ -184,12 +149,6 @@ fun AchievementCard(data: AchievementData) { } } -data class AchievementData( - val title: String, - val iconId: Int, val description: (Long) -> String, - val parameter: Long, - val levels: List -) @Preview @Composable @@ -213,13 +172,8 @@ fun AchievementsScreenPreview() { fun AchievementElementPreview() { SmartCalendarTheme { AchievementCard( - AchievementData( - title = "Eternal Flame", - iconId = R.drawable.fire, - description = { i -> "Reach a $i day streak" }, - parameter = 5, - levels = listOf(5, 10, 20, 50, 100) - ) + AchievementType.Fire, + parameter = 5 ) } } \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/model/AchievmentType.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/model/AchievmentType.kt new file mode 100644 index 0000000..2f49e6c --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/model/AchievmentType.kt @@ -0,0 +1,64 @@ +package org.hse.smartcalendar.ui.screens.model + +import androidx.annotation.DrawableRes +import org.hse.smartcalendar.R +import org.hse.smartcalendar.view.model.AbstractStatisticsViewModel +import kotlin.time.DurationUnit + +enum class AchievementType( + val title: String, + @DrawableRes val iconId: Int, + val description: (Long) -> String, + val levels: List, + val parameterProvider: (AbstractStatisticsViewModel) -> Long, + val testTag: String +) { + Fire( + title = "Eternal Flame", + iconId = R.drawable.fire, + description = { i -> "Reach a $i day streak" }, + levels = listOf(5, 10, 20, 50, 100), + parameterProvider = { stats -> stats.getRecordContinuesSuccessDays().amount.toLong() }, + testTag = "Fire" + ), + PlanToday( + title = "Planning everything", + iconId = R.drawable.writing_hand, + description = { i -> "Plan $i hours of your time today" }, + levels = listOf(5, 10, 20, 24), + parameterProvider = { stats -> (stats.getTodayPlannedTime().toMinutes() + 1) / 60 }, + testTag = "PlanToday" + ), + CommonSpend( + title = "Types are boring", + iconId = R.drawable.yawning_face, + description = { i -> "Spend $i hours with common tasks" }, + levels = listOf(10, 20, 50, 100, 1000), + parameterProvider = { stats -> stats.getTotalTimeActivityTypes().Common.time.toLong(DurationUnit.HOURS) }, + testTag = "CommonSpend" + ), + WorkTotal( + title = "Hour by Hour", + iconId = R.drawable.tasks_complete, + description = { i -> "Work a total of $i hours" }, + levels = listOf(10, 20, 50, 100, 1000), + parameterProvider = { stats -> stats.getTotalWorkTime().toMinutes() / 60 }, + testTag = "WorkTotal" + ), + WorkWeek( + title = "Mechanical Focus", + iconId = R.drawable.robot, + description = { i -> "Work a total of $i hours last week" }, + levels = listOf(4, 6, 8, 10), + parameterProvider = { stats -> stats.getWeekWorkTime().toMinutes() / 60 / 7 }, + testTag = "WorkWeek" + ), + TypesToday( + title = "Totally balanced", + iconId = R.drawable.balance_scale, + description = { i -> "Have $i types of task in day" }, + levels = listOf(4), + parameterProvider = { stats -> stats.getTypesInCurrentDay().toLong() }, + testTag = "TypesToday" + ); +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsManager.kt b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsManager.kt index abe3cde..093f3aa 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsManager.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsManager.kt @@ -18,7 +18,7 @@ class StatisticsManager(private val viewModel: AbstractStatisticsViewModel){ viewModel.createOrDeleteTask( dailyTaskList= dailyTaskList, task = task, - isCreate = false) + isCreate = true) } /** From 38a3940133801bdd1d41cade2ceeb2952e619c72 Mon Sep 17 00:00:00 2001 From: Usatov Pavel Date: Sat, 7 Jun 2025 23:32:04 +0300 Subject: [PATCH 04/11] CardTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Нашёл багу в StatisticsVM. getTotalTime() не передаёт State оказывается --- .../ui/screens/AchievementsScreenTest.kt | 29 ++++++++-- .../ui/screens/AchievmentsCardTest.kt | 54 +++++++++++++++++++ .../hse/smartcalendar/ui/navigation/App.kt | 2 - .../ui/screens/AchievementsScreen.kt | 1 + .../ui/screens/model/AchievmentType.kt | 2 +- .../view/model/SettingsViewModel.kt | 17 ------ .../view/model/StatisticsViewModel.kt | 2 +- .../view/model/StatisticsTest.kt | 6 --- 8 files changed, 82 insertions(+), 31 deletions(-) create mode 100644 app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievmentsCardTest.kt delete mode 100644 app/src/main/java/org/hse/smartcalendar/view/model/SettingsViewModel.kt diff --git a/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt index ae7e0dd..e8e969a 100644 --- a/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt +++ b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt @@ -1,8 +1,13 @@ package org.hse.smartcalendar.ui.screens +import android.content.Context +import android.util.Log import androidx.compose.ui.test.* import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.work.Configuration +import androidx.work.impl.utils.SynchronousExecutor import kotlinx.datetime.LocalTime import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType @@ -31,7 +36,7 @@ class AchievementsScreenTest { title = "first", id = UUID.randomUUID(), isComplete = false, - type = DailyTaskType.WORK, + type = DailyTaskType.COMMON, creationTime = TimeUtils.Companion.getCurrentDateTime(), description = "", start = LocalTime.Companion.fromMinutesOfDay(0), @@ -79,9 +84,25 @@ class AchievementsScreenTest { composeTestRule.runOnIdle { } assertAchievementData(AchievementType.PlanToday, "5/10") + assertAchievementData(AchievementType.CommonSpend, "0/10") + listViewModel.changeTaskCompletion(firstTask, true) + assert(statisticsViewModel.getTotalTimeActivityTypes().Common.toMinutes().toInt() == firstTask.getMinutesLength()) + composeTestRule.runOnIdle {} + assertAchievementData(AchievementType.CommonSpend, "5/10") listViewModel.addDailyTask(secondTask) - composeTestRule.runOnIdle { - } + composeTestRule.runOnIdle {} assertAchievementData(AchievementType.PlanToday, "24/24") } -} \ No newline at end of file +} +// assertAchievementData(AchievementType.CommonSpend, "0/10") +// assertAchievementData(AchievementType.PlanToday, "5/10") +// listViewModel.changeTaskCompletion(firstTask, true) +// assertAchievementData(AchievementType.CommonSpend, "5/10") +// listViewModel.addDailyTask(secondTask) +// composeTestRule.runOnIdle {} +// assertAchievementData(AchievementType.PlanToday, "24/24") +// listViewModel.changeTaskCompletion(secondTask, true) +// composeTestRule.runOnIdle {} +// assertAchievementData(AchievementType.WorkWeek, (24/7).toInt().toString()+"/${AchievementType.WorkWeek.levels[0]}") +// } +//} diff --git a/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievmentsCardTest.kt b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievmentsCardTest.kt new file mode 100644 index 0000000..954bc49 --- /dev/null +++ b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievmentsCardTest.kt @@ -0,0 +1,54 @@ +package org.hse.smartcalendar.ui.screens + +import androidx.activity.ComponentActivity +import androidx.compose.ui.semantics.ProgressBarRangeInfo +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.junit4.createComposeRule +import org.hse.smartcalendar.ui.screens.model.AchievementType +import org.junit.Rule +import org.junit.Test + +class AchievementCardTest { + val type = AchievementType.PlanToday + val tag = type.testTag + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun nullProgress() { + composeTestRule.setContent {//cannot setContent twice + AchievementCard(data = type, parameter = 0L) + } + composeTestRule.onNode( + hasText("Plan ${type.levels[0]} hours of your time today") + .and(hasParent(hasTestTag(tag))) + ).assertExists() + } + @Test fun mediumProgress() { + composeTestRule.setContent { + AchievementCard(data = type, parameter = 6L) + } + composeTestRule.onNodeWithTag("${tag}_progress") + .assertIsDisplayed() + .assertRangeInfoEquals(ProgressBarRangeInfo(current = 0.6f, range = 0f..1f)) + composeTestRule.onNode( + hasText("Plan ${type.levels[1]} hours of your time today") + .and(hasParent(hasTestTag(tag))) + ).assertExists() + } + @Test + fun maxProgress(){ + val max = type.levels.last() + composeTestRule.setContent { + AchievementCard(data = type, parameter = max) + } + composeTestRule.onNodeWithTag("${tag}_progress") + .assertIsDisplayed() + .assertRangeInfoEquals(ProgressBarRangeInfo(current = 1.0f, range = 0f..1f)) + composeTestRule.onNode( + hasText("You have max level, you achieve:${type.description(max)}") + .and(hasParent(hasTestTag(tag))) + ).assertExists() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt b/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt index 6b9949c..9093df3 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt @@ -34,7 +34,6 @@ import org.hse.smartcalendar.utility.Navigation import org.hse.smartcalendar.utility.Screens import org.hse.smartcalendar.utility.rememberNavigation import org.hse.smartcalendar.view.model.ListViewModel -import org.hse.smartcalendar.view.model.SettingsViewModel import org.hse.smartcalendar.view.model.StatisticsViewModel import org.hse.smartcalendar.view.model.TaskEditViewModel @@ -81,7 +80,6 @@ fun App( @Composable fun NestedNavigator(navigation: Navigation, authModel: AuthViewModel,openDrawer: ()-> Unit, statisticsModel: StatisticsViewModel, listModel: ListViewModel, editModel: TaskEditViewModel){ - var settingsViewModel: SettingsViewModel = viewModel() val reminderModel: ReminderViewModel = viewModel(factory = ReminderViewModelFactory( LocalContext.current.applicationContext as Application )) diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt index 1d5a3a7..4d12701 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt @@ -143,6 +143,7 @@ fun AchievementCard(data: AchievementType, modifier = Modifier .padding(8.dp) .align(Alignment.Start) + .testTag("${data.testTag}_progress") ) Text(description(data.levels.get(level))) } diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/model/AchievmentType.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/model/AchievmentType.kt index 2f49e6c..f01cede 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/model/AchievmentType.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/model/AchievmentType.kt @@ -34,7 +34,7 @@ enum class AchievementType( iconId = R.drawable.yawning_face, description = { i -> "Spend $i hours with common tasks" }, levels = listOf(10, 20, 50, 100, 1000), - parameterProvider = { stats -> stats.getTotalTimeActivityTypes().Common.time.toLong(DurationUnit.HOURS) }, + parameterProvider = { stats -> stats.TotalTime.Common.time.toLong(DurationUnit.HOURS) }, testTag = "CommonSpend" ), WorkTotal( diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/SettingsViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/SettingsViewModel.kt deleted file mode 100644 index 009f1aa..0000000 --- a/app/src/main/java/org/hse/smartcalendar/view/model/SettingsViewModel.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.hse.smartcalendar.view.model - -//import dagger.hilt.android.lifecycle.HiltViewModel -//import jakarta.inject.Inject -import androidx.lifecycle.ViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow - -class SettingsViewModel constructor( -) : ViewModel() { - private val _isReminders: MutableStateFlow = MutableStateFlow(true) - var isReminders = _isReminders.asStateFlow() - fun switchReminders(){ - _isReminders.value = _isReminders.value.not() - } - //SettingsVM 46d3d -} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt index 38f8196..1ad45b0 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt @@ -69,7 +69,7 @@ open class AbstractStatisticsViewModel():ViewModel() { } } - private var TotalTime: TotalTimeTaskTypes = TotalTimeTaskTypes(0, 0, 0, 0) + var TotalTime: TotalTimeTaskTypes = TotalTimeTaskTypes(0, 0, 0, 0) private var weekTime = WeekTime(0) private var AverageDayTime: AverageDayTimeVars = AverageDayTimeVars(totalDays = 1, totalWorkMinutes = 0) private var TodayTime: TodayTimeVars = TodayTimeVars(0, 0) diff --git a/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt b/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt index 54f1aad..a26d3b2 100644 --- a/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt +++ b/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt @@ -1,10 +1,5 @@ package org.hse.smartcalendar.view.model -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import io.mockk.every -import io.mockk.mockk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher @@ -14,7 +9,6 @@ import kotlinx.datetime.LocalTime import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType import org.hse.smartcalendar.data.User -import org.hse.smartcalendar.data.WorkManagerHolder import org.hse.smartcalendar.utility.TimeUtils import org.hse.smartcalendar.utility.fromMinutesOfDay import org.junit.jupiter.api.AfterAll From 84af01bda506ca44656bf3d796634b2a02694f2a Mon Sep 17 00:00:00 2001 From: Usatov Pavel Date: Sun, 8 Jun 2025 17:55:51 +0300 Subject: [PATCH 05/11] Statistics: CalcTest Move statistics state classes to state directory Add CalculatorTests for just Calculator& Calculator inside VM Refactor VM's BeforeAll/After methods --- app/build.gradle.kts | 1 + .../ui/screens/AchievementsScreenTest.kt | 7 +- .../hse/smartcalendar/data/DailySchedule.kt | 2 +- .../hse/smartcalendar/data/TotalTaskTypes.kt | 2 +- .../ui/screens/AchievementsScreen.kt | 2 +- .../ui/screens/model/AchievmentType.kt | 18 +-- .../smartcalendar/utility/TimePeriodUtils.kt | 76 --------- .../view/model/StatisticsViewModel.kt | 56 +++---- .../model/state}/StatisticsCalculator.kt | 28 ++-- .../view/model/state/StatisticsState.kt | 32 ++++ .../view/model/state/TimeStateClasses.kt | 89 ++++++++++ .../view/model/StatisticsTest.kt | 153 ++++++++++-------- .../smartcalendar/view/model/TaskProvider.kt | 67 ++++++++ .../model/state/StatisticsCalculatorTest.kt | 111 +++++++++++++ gradle/libs.versions.toml | 2 + 15 files changed, 439 insertions(+), 207 deletions(-) rename app/src/main/java/org/hse/smartcalendar/{utility => view/model/state}/StatisticsCalculator.kt (75%) create mode 100644 app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsState.kt create mode 100644 app/src/main/java/org/hse/smartcalendar/view/model/state/TimeStateClasses.kt create mode 100644 app/src/test/kotlin/org/hse/smartcalendar/view/model/TaskProvider.kt create mode 100644 app/src/test/kotlin/org/hse/smartcalendar/view/model/state/StatisticsCalculatorTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 700f2cb..89e0263 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -46,6 +46,7 @@ android { dependencies { testImplementation(libs.mockk) + testImplementation(libs.jupiter.junit.jupiter) androidTestImplementation(libs.ui.test.junit4) debugImplementation(libs.ui.test.manifest) implementation(libs.androidx.lifecycle.process) diff --git a/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt index e8e969a..dc7eae2 100644 --- a/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt +++ b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt @@ -1,13 +1,8 @@ package org.hse.smartcalendar.ui.screens -import android.content.Context -import android.util.Log import androidx.compose.ui.test.* import androidx.compose.ui.test.junit4.createComposeRule -import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.work.Configuration -import androidx.work.impl.utils.SynchronousExecutor import kotlinx.datetime.LocalTime import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType @@ -89,9 +84,11 @@ class AchievementsScreenTest { assert(statisticsViewModel.getTotalTimeActivityTypes().Common.toMinutes().toInt() == firstTask.getMinutesLength()) composeTestRule.runOnIdle {} assertAchievementData(AchievementType.CommonSpend, "5/10") + assertAchievementData(AchievementType.Streak, "1/5") listViewModel.addDailyTask(secondTask) composeTestRule.runOnIdle {} assertAchievementData(AchievementType.PlanToday, "24/24") + assertAchievementData(AchievementType.Streak, "0/5") } } // assertAchievementData(AchievementType.CommonSpend, "0/10") diff --git a/app/src/main/java/org/hse/smartcalendar/data/DailySchedule.kt b/app/src/main/java/org/hse/smartcalendar/data/DailySchedule.kt index 87bd8fc..1ef64ae 100644 --- a/app/src/main/java/org/hse/smartcalendar/data/DailySchedule.kt +++ b/app/src/main/java/org/hse/smartcalendar/data/DailySchedule.kt @@ -29,7 +29,7 @@ class DailySchedule (val date : LocalDate = Clock.System.now() fun setCompletionById(id: UUID, status: Boolean): Boolean { dailyTasksList.forEach { task -> - if (task.getId() == id && task.isComplete()!=status) { + if (task.getId() == id && task.isComplete()!=status) {//т.е. у нас complete поэтому не меняем task.setCompletion(status) return true } diff --git a/app/src/main/java/org/hse/smartcalendar/data/TotalTaskTypes.kt b/app/src/main/java/org/hse/smartcalendar/data/TotalTaskTypes.kt index b138201..2c39343 100644 --- a/app/src/main/java/org/hse/smartcalendar/data/TotalTaskTypes.kt +++ b/app/src/main/java/org/hse/smartcalendar/data/TotalTaskTypes.kt @@ -1,6 +1,6 @@ package org.hse.smartcalendar.data -import org.hse.smartcalendar.utility.TimePeriod +import org.hse.smartcalendar.view.model.state.TimePeriod import org.hse.smartcalendar.view.model.AbstractStatisticsViewModel.Companion.getPercent class TotalTimeTaskTypes(common: Long, work: Long, study: Long, fitness: Long){ diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt index 4d12701..6b10430 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt @@ -173,7 +173,7 @@ fun AchievementsScreenPreview() { fun AchievementElementPreview() { SmartCalendarTheme { AchievementCard( - AchievementType.Fire, + AchievementType.Streak, parameter = 5 ) } diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/model/AchievmentType.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/model/AchievmentType.kt index f01cede..5cd02e5 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/model/AchievmentType.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/model/AchievmentType.kt @@ -1,24 +1,24 @@ package org.hse.smartcalendar.ui.screens.model import androidx.annotation.DrawableRes +import androidx.compose.runtime.Composable import org.hse.smartcalendar.R import org.hse.smartcalendar.view.model.AbstractStatisticsViewModel -import kotlin.time.DurationUnit enum class AchievementType( val title: String, @DrawableRes val iconId: Int, val description: (Long) -> String, val levels: List, - val parameterProvider: (AbstractStatisticsViewModel) -> Long, + val parameterProvider: @Composable (AbstractStatisticsViewModel) -> Long,//читает изменения State val testTag: String ) { - Fire( + Streak( title = "Eternal Flame", iconId = R.drawable.fire, description = { i -> "Reach a $i day streak" }, levels = listOf(5, 10, 20, 50, 100), - parameterProvider = { stats -> stats.getRecordContinuesSuccessDays().amount.toLong() }, + parameterProvider = { stats -> stats.statisticsCalculator.stats.value.continuesCurrent.toLong() }, testTag = "Fire" ), PlanToday( @@ -26,7 +26,7 @@ enum class AchievementType( iconId = R.drawable.writing_hand, description = { i -> "Plan $i hours of your time today" }, levels = listOf(5, 10, 20, 24), - parameterProvider = { stats -> (stats.getTodayPlannedTime().toMinutes() + 1) / 60 }, + parameterProvider = { stats -> (stats.TodayTime.Planned.time.inWholeMinutes + 1) / 60 }, testTag = "PlanToday" ), CommonSpend( @@ -34,7 +34,7 @@ enum class AchievementType( iconId = R.drawable.yawning_face, description = { i -> "Spend $i hours with common tasks" }, levels = listOf(10, 20, 50, 100, 1000), - parameterProvider = { stats -> stats.TotalTime.Common.time.toLong(DurationUnit.HOURS) }, + parameterProvider = { stats -> stats.TotalTime.Common.time.inWholeHours }, testTag = "CommonSpend" ), WorkTotal( @@ -42,7 +42,7 @@ enum class AchievementType( iconId = R.drawable.tasks_complete, description = { i -> "Work a total of $i hours" }, levels = listOf(10, 20, 50, 100, 1000), - parameterProvider = { stats -> stats.getTotalWorkTime().toMinutes() / 60 }, + parameterProvider = { stats -> stats.TotalTime.All.time.inWholeHours }, testTag = "WorkTotal" ), WorkWeek( @@ -50,7 +50,7 @@ enum class AchievementType( iconId = R.drawable.robot, description = { i -> "Work a total of $i hours last week" }, levels = listOf(4, 6, 8, 10), - parameterProvider = { stats -> stats.getWeekWorkTime().toMinutes() / 60 / 7 }, + parameterProvider = { stats -> stats.weekTime.All.time.inWholeHours / 7 }, testTag = "WorkWeek" ), TypesToday( @@ -58,7 +58,7 @@ enum class AchievementType( iconId = R.drawable.balance_scale, description = { i -> "Have $i types of task in day" }, levels = listOf(4), - parameterProvider = { stats -> stats.getTypesInCurrentDay().toLong() }, + parameterProvider = { stats -> stats.statisticsCalculator.stats.value.typesInDay.toLong() }, testTag = "TypesToday" ); } \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/utility/TimePeriodUtils.kt b/app/src/main/java/org/hse/smartcalendar/utility/TimePeriodUtils.kt index 1d966aa..9281a68 100644 --- a/app/src/main/java/org/hse/smartcalendar/utility/TimePeriodUtils.kt +++ b/app/src/main/java/org/hse/smartcalendar/utility/TimePeriodUtils.kt @@ -1,17 +1,10 @@ package org.hse.smartcalendar.utility -import androidx.compose.runtime.mutableStateOf import kotlinx.datetime.Clock import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone -import kotlinx.datetime.toDateTimePeriod import kotlinx.datetime.toLocalDateTime -import org.hse.smartcalendar.utility.TimeUtils.Companion.numberToWord -import kotlin.time.Duration -import kotlin.time.Duration.Companion.minutes -import kotlin.time.DurationUnit -import kotlin.time.toDuration class TimeUtils { @@ -38,72 +31,3 @@ class TimeUtils { } } -class TimePeriod(minute: Long) { - private var _time = mutableStateOf(Duration.ZERO) - val time: Duration get() = _time.value - - init { - fromMinutes(minute) - } - - fun fromMinutes(minute: Long) { - _time.value += minute.minutes - } - - fun toMinutes(): Long { - return time.inWholeMinutes - } - - fun addMinutes(minutes: Long, sign: Boolean) { - when (sign) { - true -> _time.value += minutes.toDuration(DurationUnit.MINUTES) - false -> _time.value -= minutes.toDuration(DurationUnit.MINUTES) - } - } - - fun toPrettyString(): String { - val stringBuilder = StringBuilder() - val dataTime = time.toDateTimePeriod() - stringBuilder.append(numberToWord(dataTime.years, "year")) - stringBuilder.append(numberToWord(dataTime.days, "day")) - stringBuilder.append(numberToWord(dataTime.hours, "hour")) - stringBuilder.append(numberToWord(dataTime.minutes, "minute")) - return if (stringBuilder.toString() != "") stringBuilder.toString() else "0 minute" - } -} - -data class DaysAmount(val amount: Int) { - fun toPrettyString(): String { - return if (amount != 1) "$amount days" else "1 day" - } -} - -class DayPeriod(minute: Long) { - private var _time = mutableStateOf(Duration.ZERO) - val time: Duration get() = _time.value - fun toMinutes(): Long { - return time.inWholeMinutes - } - - init { - fromMinutes(minute) - } - - fun fromMinutes(minute: Long) { - _time.value = minute.toDuration(DurationUnit.MINUTES) - } - - fun plusMinutes(minute: Int, sign: Boolean) { - when (sign) { - true -> _time.value += minute.toDuration(DurationUnit.MINUTES); - false -> _time.value -= minute.toDuration(DurationUnit.MINUTES); - } - } - - fun toFullString(): String { - var stringBuilder: StringBuilder = StringBuilder() - stringBuilder.append(numberToWord(time.toDateTimePeriod().hours, "hour")) - stringBuilder.append(numberToWord(time.toDateTimePeriod().minutes, "minute")) - return if (stringBuilder.toString() != "") stringBuilder.toString() else "0 minute" - } -} diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt index 1ad45b0..75e0102 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt @@ -13,17 +13,17 @@ import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.TotalTimeTaskTypes import org.hse.smartcalendar.data.WorkManagerHolder import org.hse.smartcalendar.network.ApiClient -import org.hse.smartcalendar.network.AverageDayTime import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.network.StatisticsDTO -import org.hse.smartcalendar.network.TodayTime import org.hse.smartcalendar.work.StatisticsUploadWorker import org.hse.smartcalendar.repository.StatisticsRepository -import org.hse.smartcalendar.utility.DayPeriod -import org.hse.smartcalendar.utility.DaysAmount -import org.hse.smartcalendar.utility.StatisticsCalculator -import org.hse.smartcalendar.utility.TimePeriod -import kotlin.math.max +import org.hse.smartcalendar.view.model.state.DayPeriod +import org.hse.smartcalendar.view.model.state.DaysAmount +import org.hse.smartcalendar.view.model.state.StatisticsCalculator +import org.hse.smartcalendar.view.model.state.TimePeriod +import org.hse.smartcalendar.view.model.state.AverageDayTimeVars +import org.hse.smartcalendar.view.model.state.TodayTimeVars +import org.hse.smartcalendar.view.model.state.WeekTime import kotlin.math.roundToInt open class AbstractStatisticsViewModel():ViewModel() { @@ -42,38 +42,16 @@ open class AbstractStatisticsViewModel():ViewModel() { return (part * 1000).roundToInt().toFloat() / 10 } } - private class WeekTime(all: Long){ - val All: TimePeriod = TimePeriod(all) - } - private class TodayTimeVars(planned: Long, completed: Long){ - val Planned: DayPeriod = DayPeriod(planned) - var Completed: DayPeriod = DayPeriod(completed) - companion object{ - fun fromTodayTimeDTO(todayTimeDTO: TodayTime): TodayTimeVars{ - return TodayTimeVars(planned = todayTimeDTO.planned, - completed = todayTimeDTO.completed) - } - } - } - private class AverageDayTimeVars(totalWorkMinutes: Long, val totalDays: Long){ - var All: DayPeriod = DayPeriod(totalWorkMinutes/totalDays) - fun update(totalTimeMinutes: Long){ - All = DayPeriod(totalTimeMinutes/totalDays) - } - companion object{ - fun fromAverageDayDTO(averageDayTimeDTO: AverageDayTime): AverageDayTimeVars{ - return AverageDayTimeVars(totalWorkMinutes = averageDayTimeDTO.totalWorkMinutes, - totalDays = max(averageDayTimeDTO.totalDays, 1) - ) - } - } - } var TotalTime: TotalTimeTaskTypes = TotalTimeTaskTypes(0, 0, 0, 0) - private var weekTime = WeekTime(0) - private var AverageDayTime: AverageDayTimeVars = AverageDayTimeVars(totalDays = 1, totalWorkMinutes = 0) - private var TodayTime: TodayTimeVars = TodayTimeVars(0, 0) - private var statisticsCalculator: StatisticsCalculator = StatisticsCalculator() + private set + var weekTime = WeekTime(0) + private set + var AverageDayTime: AverageDayTimeVars = AverageDayTimeVars(totalDays = 1, totalWorkMinutes = 0) + private set + var TodayTime: TodayTimeVars = TodayTimeVars(0, 0) + private set + val statisticsCalculator: StatisticsCalculator = StatisticsCalculator() fun init(){ viewModelScope.launch { _initResult.value = NetworkResponse.Loading @@ -101,7 +79,9 @@ open class AbstractStatisticsViewModel():ViewModel() { } fun createOrDeleteTask(task: DailyTask, isCreate: Boolean, dailyTaskList: List){ createOrDeleteTask(task, isCreate) - statisticsCalculator.addOrDeleteTask(dailyTaskList) + statisticsCalculator.addOrDeleteTask( + StatisticsCalculator.AddOrDeleteRequest + (date = task.getTaskDate(), dateTasks = dailyTaskList)) uploadStatistics() } diff --git a/app/src/main/java/org/hse/smartcalendar/utility/StatisticsCalculator.kt b/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsCalculator.kt similarity index 75% rename from app/src/main/java/org/hse/smartcalendar/utility/StatisticsCalculator.kt rename to app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsCalculator.kt index 6ee2dd1..f902577 100644 --- a/app/src/main/java/org/hse/smartcalendar/utility/StatisticsCalculator.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsCalculator.kt @@ -1,4 +1,4 @@ -package org.hse.smartcalendar.utility +package org.hse.smartcalendar.view.model.state import androidx.compose.runtime.mutableStateOf import kotlinx.datetime.DateTimeUnit @@ -9,7 +9,7 @@ import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.User import androidx.compose.runtime.State import org.hse.smartcalendar.network.StatisticsDTO - +import org.hse.smartcalendar.utility.TimeUtils data class StatisticsCalculableData( val typesInDay: Int, val continuesCurrent: Int, @@ -17,7 +17,7 @@ data class StatisticsCalculableData( ) class StatisticsCalculator { private val _stats = mutableStateOf(StatisticsCalculableData(0,0,0)) - private val stats: State = _stats + val stats: State = _stats private fun recalculateTypes(dayTasks: List ){ val state = stats.value val dayTypes = dayTasks @@ -29,18 +29,18 @@ class StatisticsCalculator { continuesCurrent = state.continuesCurrent) } private fun recalculateDays(){ - val currentDate = TimeUtils.getCurrentDateTime().date + val currentDate = TimeUtils.Companion.getCurrentDateTime().date var lastSuccessDate = currentDate val mainSchedule = User.getSchedule() val isSuccess: (LocalDate)->Boolean = { date -> - (mainSchedule - .getOrCreateDailySchedule(lastSuccessDate) + val tasks = mainSchedule + .getOrCreateDailySchedule(date) .getDailyTaskList() .map { it.isComplete() } - .find { it.not() } != null) + !tasks.isEmpty()&&tasks.all{it} } while (isSuccess(lastSuccessDate)){ - lastSuccessDate.minus(1, DateTimeUnit.DAY) + lastSuccessDate = lastSuccessDate.minus(1, DateTimeUnit.DAY) } val continuesCurrent = lastSuccessDate.daysUntil(currentDate) val continuesTotal = maxOf(stats.value.continuesTotal, continuesCurrent) @@ -51,15 +51,19 @@ class StatisticsCalculator { ) } private fun recalculateDays(changedDate: LocalDate){ - val currentDate = TimeUtils.getCurrentDateTime().date + val currentDate = TimeUtils.Companion.getCurrentDateTime().date if (changedDate>currentDate || changedDate.daysUntil(currentDate)>stats.value.continuesCurrent){ return } recalculateDays() } - fun addOrDeleteTask(currentDateTasks: List ){ - recalculateTypes(currentDateTasks) + data class AddOrDeleteRequest(val dateTasks: List, val date: LocalDate) + fun addOrDeleteTask(request: AddOrDeleteRequest){ + if (request.date== TimeUtils.getCurrentDateTime().date) { + recalculateTypes(request.dateTasks) + } + recalculateDays(request.date) } fun changeTaskCompletion(task: DailyTask){ recalculateDays(task.getTaskDate()) @@ -69,7 +73,7 @@ class StatisticsCalculator { continuesTotal = statisticsDTO.continuesSuccessDays.record.toInt(), continuesCurrent = 0) recalculateDays() - val currentDate = TimeUtils.getCurrentDateTime().date + val currentDate = TimeUtils.Companion.getCurrentDateTime().date val tasks = User.getSchedule().getOrCreateDailySchedule(currentDate) .getDailyTaskList() recalculateTypes(tasks) diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsState.kt b/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsState.kt new file mode 100644 index 0000000..9ce12d0 --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsState.kt @@ -0,0 +1,32 @@ +package org.hse.smartcalendar.view.model.state + +import org.hse.smartcalendar.network.AverageDayTime +import org.hse.smartcalendar.network.TodayTime +import kotlin.math.max + +class TodayTimeVars(planned: Long, completed: Long){ + val Planned: DayPeriod = DayPeriod(planned) + var Completed: DayPeriod = DayPeriod(completed) + companion object{ + fun fromTodayTimeDTO(todayTimeDTO: TodayTime): TodayTimeVars{ + return TodayTimeVars(planned = todayTimeDTO.planned, + completed = todayTimeDTO.completed) + } + } +} +class AverageDayTimeVars(totalWorkMinutes: Long, val totalDays: Long){ + var All: DayPeriod = DayPeriod(totalWorkMinutes/totalDays) + fun update(totalTimeMinutes: Long){ + All = DayPeriod(totalTimeMinutes/totalDays) + } + companion object{ + fun fromAverageDayDTO(averageDayTimeDTO: AverageDayTime): AverageDayTimeVars{ + return AverageDayTimeVars(totalWorkMinutes = averageDayTimeDTO.totalWorkMinutes, + totalDays = max(averageDayTimeDTO.totalDays, 1) + ) + } + } +} +class WeekTime(all: Long){ + val All: TimePeriod = TimePeriod(all) +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/state/TimeStateClasses.kt b/app/src/main/java/org/hse/smartcalendar/view/model/state/TimeStateClasses.kt new file mode 100644 index 0000000..a14ea3b --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/view/model/state/TimeStateClasses.kt @@ -0,0 +1,89 @@ +package org.hse.smartcalendar.view.model.state + +import androidx.compose.runtime.mutableStateOf +import kotlinx.datetime.toDateTimePeriod +import org.hse.smartcalendar.utility.TimeUtils +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +class TimePeriod(minute: Long) { + private var _time = mutableStateOf(Duration.Companion.ZERO) + val time: Duration get() = _time.value + + init { + fromMinutes(minute) + } + + fun fromMinutes(minute: Long) { + _time.value += minute.minutes + } + + fun toMinutes(): Long { + return time.inWholeMinutes + } + + fun addMinutes(minutes: Long, sign: Boolean) { + when (sign) { + true -> _time.value += minutes.toDuration(DurationUnit.MINUTES) + false -> _time.value -= minutes.toDuration(DurationUnit.MINUTES) + } + } + + fun toPrettyString(): String { + val stringBuilder = StringBuilder() + val dataTime = time.toDateTimePeriod() + stringBuilder.append(TimeUtils.Companion.numberToWord(dataTime.years, "year")) + stringBuilder.append(TimeUtils.Companion.numberToWord(dataTime.days, "day")) + stringBuilder.append(TimeUtils.Companion.numberToWord(dataTime.hours, "hour")) + stringBuilder.append(TimeUtils.Companion.numberToWord(dataTime.minutes, "minute")) + return if (stringBuilder.toString() != "") stringBuilder.toString() else "0 minute" + } +} + +data class DaysAmount(val amount: Int) { + fun toPrettyString(): String { + return if (amount != 1) "$amount days" else "1 day" + } +} + +class DayPeriod(minute: Long) { + private var _time = mutableStateOf(Duration.Companion.ZERO) + val time: Duration get() = _time.value + fun toMinutes(): Long { + return time.inWholeMinutes + } + + init { + fromMinutes(minute) + } + + fun fromMinutes(minute: Long) { + _time.value = minute.toDuration(DurationUnit.MINUTES) + } + + fun plusMinutes(minute: Int, sign: Boolean) { + when (sign) { + true -> _time.value += minute.toDuration(DurationUnit.MINUTES); + false -> _time.value -= minute.toDuration(DurationUnit.MINUTES); + } + } + + fun toFullString(): String { + var stringBuilder: StringBuilder = StringBuilder() + stringBuilder.append( + TimeUtils.Companion.numberToWord( + time.toDateTimePeriod().hours, + "hour" + ) + ) + stringBuilder.append( + TimeUtils.Companion.numberToWord( + time.toDateTimePeriod().minutes, + "minute" + ) + ) + return if (stringBuilder.toString() != "") stringBuilder.toString() else "0 minute" + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt b/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt index a26d3b2..8fdddb3 100644 --- a/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt +++ b/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt @@ -5,19 +5,18 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain -import kotlinx.datetime.LocalTime import org.hse.smartcalendar.data.DailyTask -import org.hse.smartcalendar.data.DailyTaskType import org.hse.smartcalendar.data.User import org.hse.smartcalendar.utility.TimeUtils -import org.hse.smartcalendar.utility.fromMinutesOfDay +import org.hse.smartcalendar.view.model.state.CalcState import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance -import java.util.UUID import kotlin.test.assertEquals @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -26,10 +25,11 @@ class StatisticsTest { private val testDispatcher = StandardTestDispatcher() private lateinit var statisticsViewModel: AbstractStatisticsViewModel private lateinit var listViewModel: AbstractListViewModel - private lateinit var firstTask: DailyTask - private lateinit var secondTask: DailyTask + private lateinit var todayWorkTask: DailyTask + private lateinit var todayCommonTask: DailyTask private lateinit var tomorrowTask: DailyTask private lateinit var weekFitnessTask: DailyTask + private lateinit var yesterdayTask: DailyTask fun addTaskInDay(task: DailyTask){ listViewModel.changeDailyTaskSchedule(task.getTaskDate()) @@ -46,88 +46,93 @@ class StatisticsTest { listViewModel.removeDailyTask(task) listViewModel.changeDailyTaskSchedule(TimeUtils.getCurrentDateTime().date) } + fun assertCalculatorState(calcState: CalcState){ + Assertions.assertEquals(calcState.currentStreak, statisticsViewModel.statisticsCalculator.stats.value.continuesCurrent) + Assertions.assertEquals(calcState.maxStreak, statisticsViewModel.statisticsCalculator.stats.value.continuesTotal) + Assertions.assertEquals(calcState.types, statisticsViewModel.statisticsCalculator.stats.value.typesInDay) + } + fun setTasks(){ + todayWorkTask = TaskProvider.TodayWorkTask.provide() + todayCommonTask = TaskProvider.TodayCommonTask.provide() + tomorrowTask = TaskProvider.TomorrowTask.provide() + weekFitnessTask = TaskProvider.WeekFitnessTask.provide() + yesterdayTask = TaskProvider.YesterdayTask.provide() + } @BeforeAll - fun setUp(){ + fun setUp(){//Global Init, ONE call before ALL tests Dispatchers.setMain(testDispatcher) - statisticsViewModel = AbstractStatisticsViewModel() - listViewModel = AbstractListViewModel(StatisticsManager(statisticsViewModel)) - firstTask = DailyTask( - title = "first", - id = UUID.randomUUID(), - isComplete = false, - type = DailyTaskType.WORK, - creationTime = TimeUtils.Companion.getCurrentDateTime(), - description = "", - start = LocalTime.Companion.fromMinutesOfDay(10), - end = LocalTime.Companion.fromMinutesOfDay(30), - date = TimeUtils.Companion.getCurrentDateTime().date, - ) - secondTask = DailyTask.fromTime( - start = LocalTime.Companion.fromMinutesOfDay(30), - end = LocalTime.Companion.fromMinutesOfDay(50), - date = TimeUtils.Companion.getCurrentDateTime().date) - tomorrowTask = DailyTask.fromTime( - start = LocalTime.Companion.fromMinutesOfDay(30), - end = LocalTime.Companion.fromMinutesOfDay(50), - date = TimeUtils.addDaysToNowDate(1) - ) - weekFitnessTask = DailyTask.fromTimeAndType( - start = LocalTime.Companion.fromMinutesOfDay(30), - end = LocalTime.Companion.fromMinutesOfDay(50), - date = TimeUtils.addDaysToNowDate(-6), - type = DailyTaskType.FITNESS - ) } - @AfterAll + @AfterAll//Global Clear, ONE call before ALL tests fun tearDown() { Dispatchers.resetMain() } + @BeforeEach//Init, call before EACH test + fun init(){ + statisticsViewModel = AbstractStatisticsViewModel() + listViewModel = AbstractListViewModel(StatisticsManager(statisticsViewModel)) + setTasks() + } + @AfterEach//Clear, call after EACH test + fun clear(){ + User.clearSchedule() + } + /** + * check state in BeforeAll in WithPreAddedTasks: Add 4 tasks + */ @Nested inner class EmptyState { @Test fun addTaskTest(){ - listViewModel.addDailyTask(firstTask) + listViewModel.addDailyTask(todayWorkTask) assertEquals( - firstTask.getMinutesLength().toLong(), + this@StatisticsTest.todayWorkTask.getMinutesLength().toLong(), statisticsViewModel.getTodayPlannedTime().time.inWholeMinutes, ) - listViewModel.addDailyTask(secondTask) + listViewModel.addDailyTask(todayCommonTask) assertEquals( - (firstTask.getMinutesLength() + secondTask.getMinutesLength()).toLong(), + (todayWorkTask.getMinutesLength() + todayCommonTask.getMinutesLength()).toLong(), statisticsViewModel.getTodayPlannedTime().time.inWholeMinutes, ) addTaskInDay(tomorrowTask) assertEquals( - (firstTask.getMinutesLength() + secondTask.getMinutesLength()).toLong(), + (todayWorkTask.getMinutesLength() + todayCommonTask.getMinutesLength()).toLong(), statisticsViewModel.getTodayPlannedTime().time.inWholeMinutes, ) addTaskInDay(weekFitnessTask) assertEquals( - (firstTask.getMinutesLength() + secondTask.getMinutesLength()).toLong(), + (todayWorkTask.getMinutesLength() + todayCommonTask.getMinutesLength()).toLong(), statisticsViewModel.getTodayPlannedTime().time.inWholeMinutes, ) } + @Test + fun calculatorAddTest(){ + listViewModel.addDailyTask(todayWorkTask) + assertCalculatorState(CalcState(types = 1, currentStreak = 0, maxStreak = 0)) + listViewModel.addDailyTask(todayCommonTask) + assertCalculatorState(CalcState(types = 2, currentStreak = 0, maxStreak = 0)) + addTaskInDay(tomorrowTask) + assertCalculatorState(CalcState(types = 2, currentStreak = 0, maxStreak = 0)) + addTaskInDay(weekFitnessTask) + assertCalculatorState(CalcState(types = 2, currentStreak = 0, maxStreak = 0)) + listViewModel.removeDailyTask(todayCommonTask) + assertCalculatorState(CalcState(types = 1, currentStreak = 0, maxStreak = 0)) + } } @Nested inner class WithPreAddedTasks{ @BeforeEach fun addTasks(){ - //User - синглтон - User.clearSchedule() - //нам нужен чистый listViewModel перед каждым тестом - statisticsViewModel = AbstractStatisticsViewModel() - listViewModel = AbstractListViewModel(StatisticsManager(statisticsViewModel)) - listViewModel.addDailyTask(firstTask) - listViewModel.addDailyTask(secondTask) + listViewModel.addDailyTask(todayWorkTask) + listViewModel.addDailyTask(todayCommonTask) addTaskInDay(tomorrowTask) addTaskInDay(weekFitnessTask) } @Test fun deleteTaskTest(){ - listViewModel.removeDailyTask(firstTask) + listViewModel.removeDailyTask(todayWorkTask) assertEquals( - (secondTask.getMinutesLength()).toLong(), + (todayCommonTask.getMinutesLength()).toLong(), statisticsViewModel.getTodayPlannedTime().time.inWholeMinutes ) } @@ -153,9 +158,9 @@ class StatisticsTest { } @Test fun completeTaskTest(){ - listViewModel.changeTaskCompletion(firstTask, true) + listViewModel.changeTaskCompletion(todayWorkTask, true) assertTaskTimeEquals( - firstTask, + todayWorkTask, listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes, statisticsViewModel.getTodayCompletedTime().time.inWholeMinutes, statisticsViewModel.getTotalWorkTime().time.inWholeMinutes, @@ -165,9 +170,9 @@ class StatisticsTest { 100.0f, statisticsViewModel.getTotalTimeActivityTypes().WorkPercent ) - listViewModel.changeTaskCompletion(firstTask, true) + listViewModel.changeTaskCompletion(todayWorkTask, true) assertTaskTimeEquals( - firstTask, + todayWorkTask, listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes, statisticsViewModel.getTodayCompletedTime().time.inWholeMinutes, statisticsViewModel.getTotalWorkTime().time.inWholeMinutes, @@ -177,24 +182,24 @@ class StatisticsTest { 100.0f, statisticsViewModel.getTotalTimeActivityTypes().WorkPercent ) - listViewModel.changeTaskCompletion(secondTask, true) + listViewModel.changeTaskCompletion(todayCommonTask, true) assertTaskTimeEquals( - listOf(firstTask, secondTask), + listOf(todayWorkTask, todayCommonTask), listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes, statisticsViewModel.getTodayCompletedTime().time.inWholeMinutes, statisticsViewModel.getTotalWorkTime().time.inWholeMinutes) ) assertTaskTimeEquals( - firstTask, + todayWorkTask, statisticsViewModel.getTotalTimeActivityTypes().Work.time.inWholeMinutes ) assertTaskTimeEquals( - secondTask, + todayCommonTask, statisticsViewModel.getTotalTimeActivityTypes().Common.time.inWholeMinutes ) - listViewModel.changeTaskCompletion(secondTask, false) + listViewModel.changeTaskCompletion(todayCommonTask, false) assertTaskTimeEquals( - firstTask, + todayWorkTask, listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes, statisticsViewModel.getTodayCompletedTime().time.inWholeMinutes, statisticsViewModel.getTotalWorkTime().time.inWholeMinutes, @@ -236,17 +241,17 @@ class StatisticsTest { fun integrityTest(){ changeTaskInDay(tomorrowTask, true) changeTaskInDay(weekFitnessTask, true) - changeTaskInDay(firstTask, true) + changeTaskInDay(todayWorkTask, true) changeTaskInDay(weekFitnessTask, false) changeTaskInDay(weekFitnessTask, true) assertTaskTimeEquals( - listOf(weekFitnessTask, firstTask), + listOf(weekFitnessTask, todayWorkTask), listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes) ) assertEquals(100.0f/3, statisticsViewModel.getTotalTimeActivityTypes().FitnessPercent, absoluteTolerance = 0.1f) - listViewModel.removeDailyTask(firstTask) + listViewModel.removeDailyTask(todayWorkTask) assertTaskTimeEquals( listOf(), listOf(statisticsViewModel.getTodayCompletedTime().time.inWholeMinutes) @@ -273,6 +278,26 @@ class StatisticsTest { statisticsViewModel.getTotalTimeActivityTypes().FitnessPercent, absoluteTolerance = 0.1f) } + + /** + * check what listVM changes apply to statsCalculator + */ + @Test + fun calculateTest(){ + assertCalculatorState(CalcState(types = 2, currentStreak = 0, maxStreak = 0)) + changeTaskInDay(tomorrowTask, true) + assertEquals(0, statisticsViewModel.statisticsCalculator.stats.value.continuesCurrent) + changeTaskInDay(todayWorkTask, true) + assertEquals(0, statisticsViewModel.statisticsCalculator.stats.value.continuesCurrent) + changeTaskInDay(todayCommonTask, true) + assertCalculatorState(CalcState(types = 2, currentStreak = 1, maxStreak = 1)) + addTaskInDay(yesterdayTask) + changeTaskInDay(yesterdayTask, true) + assertCalculatorState(CalcState(types = 2, currentStreak = 2, maxStreak = 2)) + removeTaskInDay(todayWorkTask) + removeTaskInDay(todayCommonTask) + assertCalculatorState(CalcState(types = 0, currentStreak = 0, maxStreak = 2)) + } } } diff --git a/app/src/test/kotlin/org/hse/smartcalendar/view/model/TaskProvider.kt b/app/src/test/kotlin/org/hse/smartcalendar/view/model/TaskProvider.kt new file mode 100644 index 0000000..5cd2ade --- /dev/null +++ b/app/src/test/kotlin/org/hse/smartcalendar/view/model/TaskProvider.kt @@ -0,0 +1,67 @@ +package org.hse.smartcalendar.view.model + +import org.hse.smartcalendar.data.DailyTask +import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.utility.TimeUtils +import kotlinx.datetime.LocalTime +import org.hse.smartcalendar.utility.fromMinutesOfDay +import java.util.UUID + +/** + * enum - singleton, if instead of provide store values like TaskProvider(task:DailyTask) + * after it change in tests(task = TaskProvider.TodayWorkTask; task.setCompletion(true)) + * it change in enum + */ +enum class TaskProvider(val provide:()-> DailyTask) { + TodayWorkTask({ + DailyTask( + title = "first", + id = UUID.randomUUID(), + isComplete = false, + type = DailyTaskType.WORK, + creationTime = TimeUtils.getCurrentDateTime(), + description = "", + start = LocalTime.fromMinutesOfDay(10), + end = LocalTime.fromMinutesOfDay(30), + date = TimeUtils.getCurrentDateTime().date + ) + }), + + TodayCommonTask({ + DailyTask.fromTime( + start = LocalTime.fromMinutesOfDay(30), + end = LocalTime.fromMinutesOfDay(50), + date = TimeUtils.getCurrentDateTime().date + ) + }), + + TomorrowTask({ + DailyTask.fromTime( + start = LocalTime.fromMinutesOfDay(30), + end = LocalTime.fromMinutesOfDay(50), + date = TimeUtils.addDaysToNowDate(1) + ) + }), + YesterdayTask({ + DailyTask.fromTime( + start = LocalTime.fromMinutesOfDay(30), + end = LocalTime.fromMinutesOfDay(50), + date = TimeUtils.addDaysToNowDate(-1) + ) + }), + TwoDaysAgoTask({ + DailyTask.fromTime( + start = LocalTime.fromMinutesOfDay(30), + end = LocalTime.fromMinutesOfDay(50), + date = TimeUtils.addDaysToNowDate(-2) + ) + }), + WeekFitnessTask({ + DailyTask.fromTimeAndType( + start = LocalTime.fromMinutesOfDay(30), + end = LocalTime.fromMinutesOfDay(50), + date = TimeUtils.addDaysToNowDate(-6), + type = DailyTaskType.FITNESS + ) + }); +} \ No newline at end of file diff --git a/app/src/test/kotlin/org/hse/smartcalendar/view/model/state/StatisticsCalculatorTest.kt b/app/src/test/kotlin/org/hse/smartcalendar/view/model/state/StatisticsCalculatorTest.kt new file mode 100644 index 0000000..098478f --- /dev/null +++ b/app/src/test/kotlin/org/hse/smartcalendar/view/model/state/StatisticsCalculatorTest.kt @@ -0,0 +1,111 @@ +package org.hse.smartcalendar.view.model.state + +import org.hse.smartcalendar.data.DailySchedule +import org.hse.smartcalendar.data.DailyTask +import org.hse.smartcalendar.data.MainSchedule +import org.hse.smartcalendar.data.User +import org.hse.smartcalendar.view.model.TaskProvider +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.jupiter.api.Assertions + +data class CalcState(val types: Int, val currentStreak: Int, val maxStreak: Int) + +class StatisticsCalculatorTest { + private lateinit var firstTodayTask: DailyTask + private lateinit var secondTask: DailyTask + private lateinit var yesterdayTask: DailyTask + private lateinit var twoDaysAgoTask: DailyTask + private lateinit var tomorrowTask: DailyTask + private lateinit var statisticsCalculator: StatisticsCalculator + private lateinit var mainSchedule: MainSchedule + @Before + fun initTask(){ + firstTodayTask = TaskProvider.TodayWorkTask.provide() + secondTask = TaskProvider.TodayCommonTask.provide() + yesterdayTask = TaskProvider.YesterdayTask.provide() + twoDaysAgoTask = TaskProvider.TwoDaysAgoTask.provide() + tomorrowTask = TaskProvider.TomorrowTask.provide() + statisticsCalculator = StatisticsCalculator() + mainSchedule = User.getSchedule() + } + @After + fun clearTask(){ + User.clearSchedule() + } + fun assertCalculatorState(calcState: CalcState){ + Assertions.assertEquals(calcState.currentStreak, statisticsCalculator.stats.value.continuesCurrent) + Assertions.assertEquals(calcState.maxStreak, statisticsCalculator.stats.value.continuesTotal) + Assertions.assertEquals(calcState.types, statisticsCalculator.stats.value.typesInDay) + } + fun addOrDeleteTask(dailySchedule: DailySchedule, task: DailyTask){ + statisticsCalculator.addOrDeleteTask( + StatisticsCalculator.AddOrDeleteRequest + (date = task.getTaskDate(), dateTasks = dailySchedule.getDailyTaskList())) + } + /** + * test types & streak in day + */ + @Test + fun oneDay(){ + val dailySchedule= mainSchedule.getOrCreateDailySchedule(firstTodayTask.getTaskDate()) + dailySchedule.addDailyTask(firstTodayTask) + addOrDeleteTask(dailySchedule, firstTodayTask) + assertCalculatorState(CalcState(types = 1, currentStreak = 0, maxStreak = 0)) + firstTodayTask.setCompletion(true) + statisticsCalculator.changeTaskCompletion(firstTodayTask) + assertCalculatorState(CalcState(types = 1, currentStreak = 1, maxStreak = 1)) + dailySchedule.addDailyTask(secondTask) + addOrDeleteTask(dailySchedule, secondTask) + assertCalculatorState(CalcState(types = 2, currentStreak = 0, maxStreak = 1)) + secondTask.setCompletion(true) + statisticsCalculator.changeTaskCompletion(secondTask) + assertCalculatorState(CalcState(types = 2, currentStreak = 1, maxStreak = 1)) + secondTask.setCompletion(false) + statisticsCalculator.changeTaskCompletion(secondTask) + assertCalculatorState(CalcState(types = 2, currentStreak = 0, maxStreak = 1)) + dailySchedule.removeDailyTask(firstTodayTask) + addOrDeleteTask(dailySchedule, firstTodayTask) + assertCalculatorState(CalcState(types = 1, currentStreak = 0, maxStreak = 1)) + } + + fun addTask(dailySchedule: DailySchedule, task: DailyTask){ + dailySchedule.addDailyTask(task) + addOrDeleteTask(dailySchedule, task) + } + fun setCompletion(task: DailyTask, isComplete: Boolean){ + task.setCompletion(isComplete) + statisticsCalculator.changeTaskCompletion(task) + } + /** + * test streak in period twoDaysAgo-tomorrow + */ + @Test + fun manyDay(){ + val todaySchedule= mainSchedule.getOrCreateDailySchedule(firstTodayTask.getTaskDate()) + val yesterdaySchedule = mainSchedule.getOrCreateDailySchedule(yesterdayTask.getTaskDate()) + val twoDaysAgoSchedule = mainSchedule.getOrCreateDailySchedule(twoDaysAgoTask.getTaskDate()) + val tomorrowSchedule = mainSchedule.getOrCreateDailySchedule(tomorrowTask.getTaskDate()) + //add task for every schedule + addTask(todaySchedule, firstTodayTask) + setCompletion(firstTodayTask, true) + addTask(twoDaysAgoSchedule, twoDaysAgoTask) + setCompletion(twoDaysAgoTask, true) + assertCalculatorState(CalcState(types = 1, currentStreak = 1, maxStreak = 1)) + addTask(yesterdaySchedule, yesterdayTask) + setCompletion(yesterdayTask, true) + assertCalculatorState(CalcState(types = 1, currentStreak = 3, maxStreak = 3)) + addTask(tomorrowSchedule, tomorrowTask) + setCompletion(tomorrowTask, true) + assertCalculatorState(CalcState(types = 1, currentStreak = 3, maxStreak = 3)) + //next set some to false + setCompletion(yesterdayTask, false) + assertCalculatorState(CalcState(types = 1, currentStreak = 1, maxStreak = 3)) + setCompletion(firstTodayTask, true) + assertCalculatorState(CalcState(types = 1, currentStreak = 1, maxStreak = 3)) + todaySchedule.removeDailyTask(firstTodayTask) + addOrDeleteTask(todaySchedule, firstTodayTask) + assertCalculatorState(CalcState(types = 0, currentStreak = 0, maxStreak = 3)) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d8db73a..c088385 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,7 @@ runtimeLivedata = "1.8.2" uiTestJunit4 = "" uiTestManifest = "" workRuntimeKtx = "2.10.1" +junitJupiterVersion = "5.8.1" [libraries] @@ -96,6 +97,7 @@ retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "converte retrofit-v2110 = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "uiTestJunit4" } ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "uiTestManifest" } +jupiter-junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junitJupiterVersion" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } From e824c8e33ba5746b701ffe6c7e306b2e5560d4c7 Mon Sep 17 00:00:00 2001 From: Usatov Pavel Date: Sun, 8 Jun 2025 19:05:28 +0300 Subject: [PATCH 06/11] try github actions --- .github/workflows/android-ci.yml | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .github/workflows/android-ci.yml diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml new file mode 100644 index 0000000..7d5bd25 --- /dev/null +++ b/.github/workflows/android-ci.yml @@ -0,0 +1,59 @@ +name: Android CI + +on: + push: + pull_request: + +jobs: + build-and-test: + runs-on: ubuntu-latest + + services: + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*','**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 17 + + - name: Set up Android SDK + uses: android-actions/setup-android@v2 + with: + sdk-version: "33.0.0" + api-level: 33 + build-tools: "33.0.0" + + - name: Start Android emulator + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 33 + target: google_apis + arch: x86_64 + emulator-options: -no-window -gpu swiftshader_indirect + + - name: Assemble debug APK + run: ./gradlew assembleDebug --no-daemon + + - name: Run unit tests + run: ./gradlew testDebugUnitTest --no-daemon + + - name: Run instrumentation tests + run: ./gradlew connectedDebugAndroidTest --no-daemon + + - name: Stop emulator + if: always() + run: adb -s emulator-5554 emu kill || true \ No newline at end of file From 05f925c420d3071fd0f148a3bc0785dec0dc4ce4 Mon Sep 17 00:00:00 2001 From: Usatov Pavel Date: Sun, 8 Jun 2025 18:58:33 +0300 Subject: [PATCH 07/11] Test: Github actions fail --- .github/workflows/android-ci.yml | 69 ++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/workflows/android-ci.yml diff --git a/.github/workflows/android-ci.yml b/.github/workflows/android-ci.yml new file mode 100644 index 0000000..c1b0a6c --- /dev/null +++ b/.github/workflows/android-ci.yml @@ -0,0 +1,69 @@ +name: Android CI + +on: + push: + branches: + - main + - 'feature/**' + pull_request: + branches: + - '*' + +jobs: + build-and-test: + runs-on: macos-latest + env: + ANDROID_SDK_ROOT: /Users/runner/Library/Android/sdk + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*','**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 17 + + - name: Install Android SDK + uses: android-actions/setup-android@v2 + with: + api-level: 33 + build-tools: "33.0.2" + components: | + platform-tools + emulator + tools + platforms;android-33 + system-images;android-33;google_apis;x86_64 + + - name: Accept Android SDK licenses + run: yes | sdkmanager --licenses + + - name: Create and start emulator + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 33 + target: google_apis + arch: x86_64 + force-avd-creation: true + emulator-options: -no-window -no-boot-anim -gpu swiftshader_indirect + disable-animations: true + emulator-boot-timeout: 180 + + - name: Build debug APK, run unit & instrumentation tests + run: | + ./gradlew clean assembleDebug \ + testDebugUnitTest \ + connectedDebugAndroidTest \ + --stacktrace From ba458faf518aa20577deefd7d831efdb6e07eb98 Mon Sep 17 00:00:00 2001 From: UsatovPavel Date: Sun, 8 Jun 2025 22:05:43 +0300 Subject: [PATCH 08/11] Reminders: change & statisticsTotalDays update Add Reminders change after edit/delete/patch task Recieve registerDay from server in statsVM init, send when register(for stats.averageVars) --- .../activity/DailyTasksListActivity.kt | 4 +++- .../hse/smartcalendar/activity/MainActivity.kt | 2 +- .../hse/smartcalendar/network/RequestClasses.kt | 4 +++- .../hse/smartcalendar/network/StatisticsData.kt | 5 +++-- .../org/hse/smartcalendar/ui/navigation/App.kt | 8 +++++--- .../ui/task/DailyTaskEditWindow.kt | 17 +++++++++++++---- .../hse/smartcalendar/ui/task/DailyTasksList.kt | 5 ++++- .../smartcalendar/utility/ListViewUtility.kt | 8 ++++++-- .../view/model/ReminderViewModel.kt | 12 +++++++++--- .../view/model/StatisticsViewModel.kt | 8 ++++++-- .../view/model/TaskEditViewModel.kt | 8 +++++--- .../view/model/state/StatisticsState.kt | 13 ++++++++----- 12 files changed, 66 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/org/hse/smartcalendar/activity/DailyTasksListActivity.kt b/app/src/main/java/org/hse/smartcalendar/activity/DailyTasksListActivity.kt index 68a6aa4..a6e4fdf 100644 --- a/app/src/main/java/org/hse/smartcalendar/activity/DailyTasksListActivity.kt +++ b/app/src/main/java/org/hse/smartcalendar/activity/DailyTasksListActivity.kt @@ -74,7 +74,9 @@ fun ListNavigation( listViewModel.removeDailyTask(task) }, onCancel = {}, - taskEditViewModel = taskEditViewModel, navController = navController + taskEditViewModel = taskEditViewModel, + navController = navController, + reminderModel = reminderModel ) } composable(route = Screens.SETTINGS.route) { diff --git a/app/src/main/java/org/hse/smartcalendar/activity/MainActivity.kt b/app/src/main/java/org/hse/smartcalendar/activity/MainActivity.kt index 85a28c0..0256135 100644 --- a/app/src/main/java/org/hse/smartcalendar/activity/MainActivity.kt +++ b/app/src/main/java/org/hse/smartcalendar/activity/MainActivity.kt @@ -42,7 +42,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WorkManagerHolder.init(this) - val statisticsModel: StatisticsViewModel = StatisticsViewModel() + val statisticsModel = StatisticsViewModel() val listModel = ListViewModel(StatisticsManager(statisticsModel)) val editModel = TaskEditViewModel(listModel) enableEdgeToEdge() diff --git a/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt b/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt index f450ecd..37f2963 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt @@ -4,6 +4,7 @@ import kotlinx.datetime.LocalDateTime import kotlinx.serialization.Serializable import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.utility.TimeUtils data class LoginRequest( val username: String, @@ -12,7 +13,8 @@ data class LoginRequest( data class RegisterRequest( val username: String, val email: String, - val password: String + val password: String, + val firstDay:String = TimeUtils.getCurrentDateTime().date.toString() ) data class ChangeCredentialsRequest( val currentUsername: String, diff --git a/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt b/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt index 42d6199..3478a1c 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt @@ -1,5 +1,6 @@ package org.hse.smartcalendar.network +import kotlinx.datetime.LocalDate import kotlinx.serialization.Serializable import org.hse.smartcalendar.data.TotalTimeTaskTypes import org.hse.smartcalendar.view.model.StatisticsViewModel @@ -33,7 +34,7 @@ data class StatisticsDTO( ), averageDayTime = AverageDayTime( totalWorkMinutes = viewModel.getTotalWorkTime().time.inWholeMinutes, - totalDays = 7 + firstDay = viewModel.AverageDayTime.firstDay ) ) } @@ -68,5 +69,5 @@ data class ContinuesSuccessDays( @Serializable data class AverageDayTime( val totalWorkMinutes: Long, - val totalDays: Long + val firstDay: LocalDate ) \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt b/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt index 9093df3..785255b 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt @@ -140,12 +140,14 @@ fun NestedNavigator(navigation: Navigation, authModel: AuthViewModel,openDrawer: } composable(Screens.EDIT_TASK.route) { TaskEditWindow( - onSave = {}, + onSave = {task->reminderModel.scheduleReminder(task)}, onDelete = { task -> listModel.removeDailyTask(task) - + reminderModel.cancelReminder(task) }, onCancel = {}, - taskEditViewModel = editModel, navController = navigation.navController + taskEditViewModel = editModel, + reminderModel = reminderModel, + navController = navigation.navController ) } } diff --git a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskEditWindow.kt b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskEditWindow.kt index fd782a0..7c1d67b 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskEditWindow.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskEditWindow.kt @@ -1,6 +1,7 @@ package org.hse.smartcalendar.ui.task import android.annotation.SuppressLint +import android.app.Application import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -29,6 +30,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.wear.compose.material3.Text import kotlinx.datetime.LocalTime @@ -37,6 +39,8 @@ import org.hse.smartcalendar.utility.Screens import org.hse.smartcalendar.utility.fromMinutesOfDay import org.hse.smartcalendar.utility.toMinutesOfDay import org.hse.smartcalendar.view.model.ListViewModel +import org.hse.smartcalendar.view.model.ReminderViewModel +import org.hse.smartcalendar.view.model.ReminderViewModelFactory import org.hse.smartcalendar.view.model.StatisticsManager import org.hse.smartcalendar.view.model.StatisticsViewModel import org.hse.smartcalendar.view.model.TaskEditViewModel @@ -45,10 +49,11 @@ import org.hse.smartcalendar.view.model.TaskEditViewModel @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun TaskEditWindow( - onSave: () -> Unit, + onSave: (DailyTask) -> Unit, onCancel: () -> Unit, onDelete: (DailyTask) -> Unit, taskEditViewModel: TaskEditViewModel, + reminderModel: ReminderViewModel, navController: NavController ) { val taskState = taskEditViewModel.getTask() @@ -78,12 +83,12 @@ fun TaskEditWindow( ) taskEditViewModel.changes.setDailyTaskEndTime(LocalTime.fromMinutesOfDay(endTime.intValue)) taskEditViewModel.changes.setDailyTaskType(taskType.value) - taskEditViewModel.updateInnerTask( + val result = taskEditViewModel.updateInnerTask( isEmptyTitle, isConflictInTimeField, - isNestedTask + isNestedTask, + reminderModel ) - onSave() if (!isEmptyTitle.value && !isConflictInTimeField.value && !isNestedTask.value) { navController.navigate(Screens.CALENDAR.route) } @@ -168,6 +173,9 @@ fun TaskEditWindow( @Preview @Composable fun TaskEditWindowPreview() { + val reminderModel: ReminderViewModel = viewModel(factory = ReminderViewModelFactory( + LocalContext.current.applicationContext as Application + )) TaskEditWindow( onSave = { }, onCancel = { }, @@ -175,6 +183,7 @@ fun TaskEditWindowPreview() { taskEditViewModel = TaskEditViewModel( listViewModel = ListViewModel(StatisticsManager(StatisticsViewModel())) ), + reminderModel = reminderModel, navController = NavController( LocalContext.current ) diff --git a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt index bbe9ab8..c37474d 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt @@ -124,6 +124,8 @@ fun DailyTasksList( DailyTaskCard( it, onCompletionChange = { + if (!it.isComplete()){reminderModel.cancelReminder(it)} + else {reminderModel.scheduleReminder(it)} viewModel.changeTaskCompletion(it, !it.isComplete()) }, taskEditViewModel = taskEditViewModel, @@ -301,7 +303,8 @@ fun DailyTaskListPreview() { onCancel = { navController.popBackStack() }, taskEditViewModel = taskEditViewModel, navController = navController, - onDelete = { } + onDelete = { }, + reminderModel = reminderModel ) } } diff --git a/app/src/main/java/org/hse/smartcalendar/utility/ListViewUtility.kt b/app/src/main/java/org/hse/smartcalendar/utility/ListViewUtility.kt index 991ac0e..7590a3c 100644 --- a/app/src/main/java/org/hse/smartcalendar/utility/ListViewUtility.kt +++ b/app/src/main/java/org/hse/smartcalendar/utility/ListViewUtility.kt @@ -11,8 +11,9 @@ fun editHandler( isEmptyTitle: MutableState, isConflictInTimeField: MutableState, isNestedTask: MutableState, - statsUpdateOldToNewTask: (DailyTask, DailyTask)-> Unit -) { + statsUpdateOldToNewTask: (DailyTask, DailyTask)-> Unit, + reminderUpdate: (DailyTask)->Unit +): Boolean { isEmptyTitle.value = false isNestedTask.value = false isConflictInTimeField.value = false @@ -24,5 +25,8 @@ fun editHandler( if (!isEmptyTitle.value && !isConflictInTimeField.value && !isNestedTask.value) { oldTask.updateDailyTask(newTask) statsUpdateOldToNewTask(oldTask, newTask) + reminderUpdate(newTask) + return true } + return false } diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/ReminderViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/ReminderViewModel.kt index bf48d80..1300a32 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/ReminderViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/ReminderViewModel.kt @@ -3,6 +3,7 @@ package org.hse.smartcalendar.view.model import android.app.Application import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import androidx.work.workDataOf @@ -36,8 +37,6 @@ class ReminderViewModel(application: Application): ViewModel() { internal fun scheduleReminder( task: DailyTask): Boolean { - //ReminderVm ff62295 - //разные ссылки на VM if (!isReminders.value){ return false } @@ -69,9 +68,16 @@ class ReminderViewModel(application: Application): ViewModel() { ReminderWorker.Companion.END_KEY to LocalTime.prettyPrint(task.getDailyTaskEndTime()), ) ) - workManager.enqueue(myWorkRequestBuilder.build()) + workManager.enqueueUniqueWork( + "reminder${task.getId()}", + ExistingWorkPolicy.REPLACE, + myWorkRequestBuilder.build()) return true } + internal fun cancelReminder(task: DailyTask) { + val workName = "reminder${task.getId()}" + workManager.cancelUniqueWork(workName) + } } class ReminderViewModelFactory(private val application: Application) : ViewModelProvider.Factory { diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt index 75e0102..bf169e6 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt @@ -8,6 +8,7 @@ import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.workDataOf import kotlinx.coroutines.launch +import kotlinx.datetime.LocalDate import kotlinx.serialization.json.Json import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.TotalTimeTaskTypes @@ -47,7 +48,7 @@ open class AbstractStatisticsViewModel():ViewModel() { private set var weekTime = WeekTime(0) private set - var AverageDayTime: AverageDayTimeVars = AverageDayTimeVars(totalDays = 1, totalWorkMinutes = 0) + var AverageDayTime: AverageDayTimeVars = AverageDayTimeVars(firstDay = LocalDate(2025, 6, 8), totalWorkMinutes = 0) private set var TodayTime: TodayTimeVars = TodayTimeVars(0, 0) private set @@ -131,7 +132,10 @@ open class AbstractStatisticsViewModel():ViewModel() { return DaysAmount(statisticsCalculator.getTodayContinuesSuccessDays()) } fun getTotalTimeActivityTypes():TotalTimeTaskTypes{ - return TotalTimeTaskTypes(TotalTime.Common.time.inWholeMinutes, TotalTime.Work.time.inWholeMinutes, TotalTime.Study.time.inWholeMinutes, TotalTime.Fitness.time.inWholeMinutes) + return TotalTimeTaskTypes(TotalTime.Common.time.inWholeMinutes, + TotalTime.Work.time.inWholeMinutes, + TotalTime.Study.time.inWholeMinutes, + TotalTime.Fitness.time.inWholeMinutes) } fun getWeekWorkTime(): TimePeriod{ return weekTime.All diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/TaskEditViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/TaskEditViewModel.kt index 51687ed..2b90f0b 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/TaskEditViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/TaskEditViewModel.kt @@ -33,8 +33,9 @@ class TaskEditViewModel( isEmptyTitle: MutableState, isConflictInTimeField: MutableState, isNestedTask: MutableState, - ) { - editHandler( + reminderViewModel: ReminderViewModel + ): Boolean { + return editHandler( oldTask = task, newTask = changes, viewModel = listViewModel, @@ -44,7 +45,8 @@ class TaskEditViewModel( statsUpdateOldToNewTask = { oldTask, newTask-> listViewModel.statisticsManager.updateDailyTask(oldTask=oldTask, newTask = newTask) listViewModel.scheduleTaskRequest(newTask, DailyTaskAction.Type.EDIT) - } + }, + reminderUpdate = {task ->reminderViewModel.scheduleReminder(task)} ) } } \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsState.kt b/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsState.kt index 9ce12d0..b26bb23 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsState.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsState.kt @@ -1,8 +1,10 @@ package org.hse.smartcalendar.view.model.state +import kotlinx.datetime.LocalDate +import kotlinx.datetime.daysUntil import org.hse.smartcalendar.network.AverageDayTime import org.hse.smartcalendar.network.TodayTime -import kotlin.math.max +import org.hse.smartcalendar.utility.TimeUtils class TodayTimeVars(planned: Long, completed: Long){ val Planned: DayPeriod = DayPeriod(planned) @@ -14,15 +16,16 @@ class TodayTimeVars(planned: Long, completed: Long){ } } } -class AverageDayTimeVars(totalWorkMinutes: Long, val totalDays: Long){ - var All: DayPeriod = DayPeriod(totalWorkMinutes/totalDays) +class AverageDayTimeVars(totalWorkMinutes: Long, val firstDay: LocalDate){ + private val dayLength = firstDay.daysUntil(TimeUtils.getCurrentDateTime().date)+1 + var All: DayPeriod = DayPeriod(totalWorkMinutes/dayLength) fun update(totalTimeMinutes: Long){ - All = DayPeriod(totalTimeMinutes/totalDays) + All = DayPeriod(totalTimeMinutes/dayLength) } companion object{ fun fromAverageDayDTO(averageDayTimeDTO: AverageDayTime): AverageDayTimeVars{ return AverageDayTimeVars(totalWorkMinutes = averageDayTimeDTO.totalWorkMinutes, - totalDays = max(averageDayTimeDTO.totalDays, 1) + firstDay = averageDayTimeDTO.firstDay ) } } From 8fb048d874dc24510b5811b5a30812a272b0f9c1 Mon Sep 17 00:00:00 2001 From: UsatovPavel Date: Wed, 11 Jun 2025 01:25:10 +0300 Subject: [PATCH 09/11] AudioApi & StatsStore Create AudioRepo, Interface&ChatResponse to recieve tasks for audio from server Add StatisticsStore-singlton class for statistics data Try send audio to server through worker Look at next commit why it will not work in our app --- .../data/store/StatisticsStore.kt | 100 ++++++++++++++++++ .../hse/smartcalendar/network/ApiClient.kt | 3 + .../hse/smartcalendar/network/ApiInterface.kt | 13 ++- .../hse/smartcalendar/network/DataResponse.kt | 28 +++++ .../smartcalendar/network/StatisticsData.kt | 22 ++-- .../repository/AudioRepository.kt | 13 +++ .../repository/StatisticsRepository.kt | 5 - .../smartcalendar/view/model/ListViewModel.kt | 5 + .../view/model/StatisticsViewModel.kt | 97 +---------------- .../org/hse/smartcalendar/work/AudioWorker.kt | 38 +++++++ 10 files changed, 212 insertions(+), 112 deletions(-) create mode 100644 app/src/main/java/org/hse/smartcalendar/data/store/StatisticsStore.kt create mode 100644 app/src/main/java/org/hse/smartcalendar/repository/AudioRepository.kt create mode 100644 app/src/main/java/org/hse/smartcalendar/work/AudioWorker.kt diff --git a/app/src/main/java/org/hse/smartcalendar/data/store/StatisticsStore.kt b/app/src/main/java/org/hse/smartcalendar/data/store/StatisticsStore.kt new file mode 100644 index 0000000..b967b59 --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/data/store/StatisticsStore.kt @@ -0,0 +1,100 @@ +package org.hse.smartcalendar.store + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.workDataOf +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.datetime.LocalDate +import kotlinx.serialization.json.Json +import org.hse.smartcalendar.data.DailyTask +import org.hse.smartcalendar.data.TotalTimeTaskTypes +import org.hse.smartcalendar.data.WorkManagerHolder +import org.hse.smartcalendar.network.ApiClient +import org.hse.smartcalendar.network.NetworkResponse +import org.hse.smartcalendar.network.StatisticsDTO +import org.hse.smartcalendar.repository.StatisticsRepository +import org.hse.smartcalendar.view.model.state.AverageDayTimeVars +import org.hse.smartcalendar.view.model.state.TodayTimeVars +import org.hse.smartcalendar.view.model.state.WeekTime +import org.hse.smartcalendar.view.model.state.StatisticsCalculator +import org.hse.smartcalendar.work.StatisticsUploadWorker + +object StatisticsStore { + private val repository = StatisticsRepository(ApiClient.statisticsApiService) + var totalTime by mutableStateOf(TotalTimeTaskTypes(0,0,0,0)) + private set + var averageDayTime by mutableStateOf(AverageDayTimeVars(firstDay = LocalDate(1970,1,1), totalWorkMinutes = 0)) + private set + var todayTime by mutableStateOf(TodayTimeVars(0,0)) + private set + var weekTime by mutableStateOf(WeekTime(0)) + private set + val calculator = StatisticsCalculator() + + suspend fun init(): NetworkResponse = withContext(Dispatchers.IO) { + when(val resp = repository.getStatistics()) { + is NetworkResponse.Success -> { + val d = resp.data + totalTime = d.totalTime.toVMTotalTime() + averageDayTime = AverageDayTimeVars.fromAverageDayDTO(d.averageDayTime) + todayTime = TodayTimeVars.fromTodayTimeDTO(d.todayTime) + weekTime = WeekTime(d.weekTime) + calculator.init(d) + resp + } + else -> resp + } + } + private fun createOrDeleteTask(task: DailyTask, isCreate: Boolean){ + if (task.isComplete() && isCreate==false){ + changeTaskCompletion(task, false) + } + if (task.belongsCurrentDay()){ + todayTime.Planned.plusMinutes(task.getMinutesLength(), isCreate) + } + } + + fun createOrDeleteTask(task: DailyTask, allTasks: List, isAdd: Boolean) { + if (task.isComplete().not() && !isAdd) return + if (task.belongsCurrentDay()) todayTime = todayTime.apply { Planned.plusMinutes(task.getMinutesLength(), isAdd) } + calculator.addOrDeleteTask(StatisticsCalculator.AddOrDeleteRequest(date = task.getTaskDate(), dateTasks = allTasks)) + uploadStatistics() + } + + fun changeTaskCompletion(task: DailyTask, isComplete: Boolean, isUploadStats: Boolean=true) { + if (task.belongsCurrentDay()) todayTime = todayTime.apply { Completed.plusMinutes(task.getMinutesLength(), isComplete) } + if (task.belongsCurrentWeek()) weekTime = weekTime.apply { All.addMinutes(task.getMinutesLength().toLong(), isComplete) } + totalTime = totalTime.apply { completeTask(task, isComplete) } + averageDayTime = averageDayTime.apply { update(totalTime.totalMinutes) } + calculator.changeTaskCompletion(task) + if (isUploadStats) {uploadStatistics()} + } + fun changeTask(task: DailyTask, newTask: DailyTask){ + if (task.isComplete()){ + changeTaskCompletion(task, false, isUploadStats = false) + changeTaskCompletion(newTask, true, isUploadStats = false) + } + createOrDeleteTask(task, false) + createOrDeleteTask(newTask, true) + uploadStatistics() + } + fun uploadStatistics() { + val workManager = WorkManagerHolder.getInstance() + val statsDTO = StatisticsDTO.fromStore() + val json = Json.encodeToString(StatisticsDTO.serializer(), statsDTO) + + val workRequest = OneTimeWorkRequestBuilder() + .setInputData(workDataOf("statistics_json" to json)) + .build() + + workManager.enqueueUniqueWork( + "upload_stats", + ExistingWorkPolicy.REPLACE, + workRequest + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt b/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt index b60b724..1dc8b20 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt @@ -41,6 +41,9 @@ object ApiClient { val statisticsApiService: StatisticsApiInterface by lazy { retrofit.create(StatisticsApiInterface::class.java) } + val audioApiService : AudioApiInterface by lazy { + retrofit.create(AudioApiInterface::class.java) + } } class AuthInterceptor(private val tokenProvider: () -> String?) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { diff --git a/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt b/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt index caa33e9..1179f75 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt @@ -1,5 +1,6 @@ package org.hse.smartcalendar.network +import okhttp3.MultipartBody import okhttp3.ResponseBody import retrofit2.Response import retrofit2.http.Body @@ -9,6 +10,8 @@ import retrofit2.http.PATCH import retrofit2.http.POST import retrofit2.http.PUT import retrofit2.http.Path +import retrofit2.http.Multipart +import retrofit2.http.Part import java.util.UUID interface AuthApiInterface { @@ -72,4 +75,12 @@ interface StatisticsApiInterface { @GET("api/statistics/total-time-task-types") suspend fun getGlobalTaskTypeStatistics(): Response -} \ No newline at end of file +} + +interface AudioApiInterface { + @Multipart + @POST("api/audio/process") + suspend fun processAudio( + @Part file: MultipartBody.Part + ): Response> +} diff --git a/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt b/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt index ee2d21e..021dc56 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt @@ -6,6 +6,7 @@ import kotlinx.datetime.LocalTime import kotlinx.serialization.Serializable import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.utility.TimeUtils import java.util.UUID data class RegisterResponse ( @@ -59,3 +60,30 @@ data class TaskResponse( ) } } +@Serializable +data class ChatTaskResponse( + val id: String, + val title: String, + val description: String, + val start: LocalDateTime?, + val end: LocalDateTime?, + val date: String, + val type: String?, + val creationTime: LocalDateTime?, + val complete: Boolean? +) { + fun toDailyTask(): DailyTask { + val task = DailyTask( + id = UUID.fromString(id), + title = title, + description = description, + start = start?.time ?: LocalTime(0, 0), + end = end?.time ?: LocalTime(0, 0), + date = (start ?: end?: creationTime?: TimeUtils.getCurrentDateTime()).date , + type = DailyTaskType.valueOf(type?.uppercase() ?: "COMMON"), + creationTime = creationTime?: TimeUtils.getCurrentDateTime(), + isComplete = complete == true, + ) + return task + } +} diff --git a/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt b/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt index 3478a1c..eaf2c6f 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt @@ -3,7 +3,8 @@ package org.hse.smartcalendar.network import kotlinx.datetime.LocalDate import kotlinx.serialization.Serializable import org.hse.smartcalendar.data.TotalTimeTaskTypes -import org.hse.smartcalendar.view.model.StatisticsViewModel +import org.hse.smartcalendar.store.StatisticsStore + @Serializable data class StatisticsDTO( val totalTime: TotalTime, @@ -13,9 +14,8 @@ data class StatisticsDTO( val averageDayTime: AverageDayTime ) { companion object { - fun fromViewModel(viewModel: StatisticsViewModel): StatisticsDTO { - val totalTime = viewModel.getTotalTimeActivityTypes() - + fun fromStore(): StatisticsDTO { + val totalTime: TotalTimeTaskTypes = StatisticsStore.totalTime return StatisticsDTO( totalTime = TotalTime( common = totalTime.Common.time.inWholeMinutes, @@ -23,18 +23,18 @@ data class StatisticsDTO( study = totalTime.Study.time.inWholeMinutes, fitness = totalTime.Fitness.time.inWholeMinutes ), - weekTime = viewModel.getWeekWorkTime().time.inWholeMinutes, + weekTime = StatisticsStore.weekTime.All.time.inWholeMinutes, todayTime = TodayTime( - planned = viewModel.getTodayPlannedTime().time.inWholeMinutes, - completed = viewModel.getTodayCompletedTime().time.inWholeMinutes + planned = StatisticsStore.todayTime.Planned.time.inWholeMinutes, + completed = StatisticsStore.todayTime.Completed.time.inWholeMinutes ), continuesSuccessDays = ContinuesSuccessDays( - record = viewModel.getRecordContinuesSuccessDays().amount.toLong(), - now = viewModel.getTodayContinuesSuccessDays().amount.toLong() + record = StatisticsStore.calculator.getRecordContinuesSuccessDays().toLong(), + now = StatisticsStore.calculator.getTodayContinuesSuccessDays().toLong() ), averageDayTime = AverageDayTime( - totalWorkMinutes = viewModel.getTotalWorkTime().time.inWholeMinutes, - firstDay = viewModel.AverageDayTime.firstDay + totalWorkMinutes = StatisticsStore.averageDayTime.All.time.inWholeMinutes, + firstDay = StatisticsStore.averageDayTime.firstDay ) ) } diff --git a/app/src/main/java/org/hse/smartcalendar/repository/AudioRepository.kt b/app/src/main/java/org/hse/smartcalendar/repository/AudioRepository.kt new file mode 100644 index 0000000..4edb6e2 --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/repository/AudioRepository.kt @@ -0,0 +1,13 @@ +package org.hse.smartcalendar.repository + +import okhttp3.MultipartBody +import org.hse.smartcalendar.network.AudioApiInterface +import org.hse.smartcalendar.network.ChatTaskResponse +import org.hse.smartcalendar.network.NetworkResponse + +class AudioRepository(private val api: AudioApiInterface) : BaseRepository() { + + suspend fun sendAudio(filePart: MultipartBody.Part): NetworkResponse> { + return withSupplierRequest { api.processAudio(filePart) } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/repository/StatisticsRepository.kt b/app/src/main/java/org/hse/smartcalendar/repository/StatisticsRepository.kt index ecac771..3b6a7e9 100644 --- a/app/src/main/java/org/hse/smartcalendar/repository/StatisticsRepository.kt +++ b/app/src/main/java/org/hse/smartcalendar/repository/StatisticsRepository.kt @@ -7,11 +7,6 @@ import org.hse.smartcalendar.network.StatisticsDTO import org.hse.smartcalendar.view.model.StatisticsViewModel class StatisticsRepository(private val api: StatisticsApiInterface): BaseRepository() { - suspend fun updateStatistics(viewModel: StatisticsViewModel): NetworkResponse { - return withIdRequest { id -> - api.updateUserStatistics(id, StatisticsDTO.fromViewModel(viewModel)) - } - } suspend fun updateStatistics(statisticsRequest: StatisticsDTO): NetworkResponse { return withIdRequest { id -> api.updateUserStatistics(id, statisticsRequest) diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt index 0cd6731..0430cb1 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt @@ -157,6 +157,7 @@ class ListViewModel(statisticsManager: StatisticsManager) : AbstractListViewMode description: AudioDescription, ): DailyTask? { // TODO Надо написать отправку файла и обработку ответа. + Thread.sleep(1000) val task: DailyTask = DailyTask( title = "TODO", @@ -165,6 +166,10 @@ class ListViewModel(statisticsManager: StatisticsManager) : AbstractListViewMode end = LocalTime(0, 0), date = DailyTask.defaultDate ) + val nowDate = this.getScheduleDate() + this.changeDailyTaskSchedule(task.getTaskDate()) + this.addDailyTask(task) + this.changeDailyTaskSchedule(nowDate) return task } diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt index bf169e6..063c898 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt @@ -18,6 +18,7 @@ import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.network.StatisticsDTO import org.hse.smartcalendar.work.StatisticsUploadWorker import org.hse.smartcalendar.repository.StatisticsRepository +import org.hse.smartcalendar.store.StatisticsStore import org.hse.smartcalendar.view.model.state.DayPeriod import org.hse.smartcalendar.view.model.state.DaysAmount import org.hse.smartcalendar.view.model.state.StatisticsCalculator @@ -28,7 +29,6 @@ import org.hse.smartcalendar.view.model.state.WeekTime import kotlin.math.roundToInt open class AbstractStatisticsViewModel():ViewModel() { - private val statisticsRepo: StatisticsRepository = StatisticsRepository(ApiClient.statisticsApiService) var _initResult = MutableLiveData>() val initResult:LiveData> = _initResult companion object { @@ -43,107 +43,14 @@ open class AbstractStatisticsViewModel():ViewModel() { return (part * 1000).roundToInt().toFloat() / 10 } } - - var TotalTime: TotalTimeTaskTypes = TotalTimeTaskTypes(0, 0, 0, 0) - private set - var weekTime = WeekTime(0) - private set - var AverageDayTime: AverageDayTimeVars = AverageDayTimeVars(firstDay = LocalDate(2025, 6, 8), totalWorkMinutes = 0) - private set - var TodayTime: TodayTimeVars = TodayTimeVars(0, 0) - private set - val statisticsCalculator: StatisticsCalculator = StatisticsCalculator() fun init(){ viewModelScope.launch { _initResult.value = NetworkResponse.Loading - val response = statisticsRepo.getStatistics() - if (response is NetworkResponse.Success){ - val data = response.data - TotalTime = data.totalTime.toVMTotalTime() - AverageDayTime = AverageDayTimeVars.fromAverageDayDTO(data.averageDayTime) - weekTime = WeekTime(data.weekTime) - TodayTime = TodayTimeVars.fromTodayTimeDTO(data.todayTime) - statisticsCalculator.init(data) - } - _initResult.value = response + _initResult.value = StatisticsStore.init() } } open fun uploadStatistics(){ } - private fun createOrDeleteTask(task: DailyTask, isCreate: Boolean){ - if (task.isComplete() && isCreate==false){ - changeTaskCompletion(task, false) - } - if (task.belongsCurrentDay()){ - TodayTime.Planned.plusMinutes(task.getMinutesLength(), isCreate) - } - } - fun createOrDeleteTask(task: DailyTask, isCreate: Boolean, dailyTaskList: List){ - createOrDeleteTask(task, isCreate) - statisticsCalculator.addOrDeleteTask( - StatisticsCalculator.AddOrDeleteRequest - (date = task.getTaskDate(), dateTasks = dailyTaskList)) - uploadStatistics() - } - - fun changeTaskCompletion(task: DailyTask, isComplete: Boolean, isUploadStats: Boolean =true){//когда таска запатчена - val taskMinutesLength = task.getMinutesLength() - if (task.belongsCurrentDay()) { - TodayTime.Completed.plusMinutes(taskMinutesLength, isComplete) - } - if (task.belongsCurrentWeek()){ - weekTime.All.addMinutes(taskMinutesLength.toLong(), isComplete) - } - TotalTime.completeTask(task, isComplete) - AverageDayTime.update(TotalTime.totalMinutes) - statisticsCalculator.changeTaskCompletion(task) - if (isUploadStats) {uploadStatistics()} - } - - fun changeTask(task: DailyTask, newTask: DailyTask){ - if (task.isComplete()){ - changeTaskCompletion(task, false, isUploadStats = false) - changeTaskCompletion(newTask, true, isUploadStats = false) - } - createOrDeleteTask(task, false) - createOrDeleteTask(newTask, true) - uploadStatistics() - } - - fun getTotalWorkTime():TimePeriod{ - return TotalTime.All - } - - fun getTodayPlannedTime(): DayPeriod{ - return TodayTime.Planned - } - fun getAverageDailyTime():DayPeriod{ - return AverageDayTime.All - } - - fun getTodayCompletedTime():DayPeriod{ - return TodayTime.Completed - } - - fun getRecordContinuesSuccessDays():DaysAmount{ - return DaysAmount(statisticsCalculator.getRecordContinuesSuccessDays()) - } - fun getTodayContinuesSuccessDays():DaysAmount{ - return DaysAmount(statisticsCalculator.getTodayContinuesSuccessDays()) - } - fun getTotalTimeActivityTypes():TotalTimeTaskTypes{ - return TotalTimeTaskTypes(TotalTime.Common.time.inWholeMinutes, - TotalTime.Work.time.inWholeMinutes, - TotalTime.Study.time.inWholeMinutes, - TotalTime.Fitness.time.inWholeMinutes) - } - fun getWeekWorkTime(): TimePeriod{ - return weekTime.All - } - fun getTypesInCurrentDay(): Int{ - return statisticsCalculator.getCurrentDayTypes() - } - } class StatisticsViewModel(): AbstractStatisticsViewModel(){ diff --git a/app/src/main/java/org/hse/smartcalendar/work/AudioWorker.kt b/app/src/main/java/org/hse/smartcalendar/work/AudioWorker.kt new file mode 100644 index 0000000..daf5277 --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/work/AudioWorker.kt @@ -0,0 +1,38 @@ +package org.hse.smartcalendar.work + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody +import org.hse.smartcalendar.repository.AudioRepository +import java.io.File +import org.hse.smartcalendar.data.User +import org.hse.smartcalendar.network.ApiClient +import org.hse.smartcalendar.network.NetworkResponse + +class AudioWorker( + context: Context, + params: WorkerParameters +) : CoroutineWorker(context, params) { + private val repo = AudioRepository(ApiClient.audioApiService) + + override suspend fun doWork(): Result { + val path = inputData.getString("audio_path") ?: return Result.failure() + val descJson = inputData.getString("audio_desc") ?: return Result.failure() + val file = File(path) + val requestFile = RequestBody.create("audio/*".toMediaType(), file) + val part = MultipartBody.Part.createFormData("file", file.name, requestFile) + val response = repo.sendAudio(part) + return if (response is NetworkResponse.Success) { + val newTasks = response.data.map{it.toDailyTask()}; + for (task in newTasks) { + User.getSchedule().getOrCreateDailySchedule(task.getTaskDate()).addDailyTask(task) + } + Result.success() + } else { + Result.retry() + } + } +} \ No newline at end of file From 77143d6f856e36d4fe4e686114ea28d143612410 Mon Sep 17 00:00:00 2001 From: UsatovPavel Date: Wed, 11 Jun 2025 17:30:24 +0300 Subject: [PATCH 10/11] AudioApi & StatsStore Add mutable uiState in StaticticsVM which update from StatsStore Send audio to server in AudioRepository instead async with worker(MVVM architecture need Database to move changes to VM). OpenAI API don't work Add percent test, fixup TotalTaskTypes --- .../ui/screens/AchievementsScreenTest.kt | 8 +- .../hse/smartcalendar/data/TotalTaskTypes.kt | 37 +++--- .../data/store/StatisticsStore.kt | 30 +++-- .../hse/smartcalendar/network/DataResponse.kt | 28 ++++ .../repository/AudioRepository.kt | 58 ++++++++ .../ui/screens/AchievementsScreen.kt | 13 +- .../ui/screens/StatisticsScreen.kt | 58 ++++---- .../ui/screens/model/AchievmentType.kt | 17 +-- .../ui/task/DailyTaskCreationWindow.kt | 17 +-- .../smartcalendar/view/model/ListViewModel.kt | 40 +++--- .../view/model/StatisticsManager.kt | 2 +- .../view/model/StatisticsViewModel.kt | 66 +++++---- .../view/model/state/StatisticsState.kt | 9 ++ .../org/hse/smartcalendar/work/AudioWorker.kt | 38 ------ .../view/model/StatisticsTest.kt | 125 +++++++++++------- 15 files changed, 325 insertions(+), 221 deletions(-) delete mode 100644 app/src/main/java/org/hse/smartcalendar/work/AudioWorker.kt diff --git a/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt index dc7eae2..09d5c37 100644 --- a/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt +++ b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt @@ -6,13 +6,14 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.datetime.LocalTime import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.store.StatisticsStore import org.hse.smartcalendar.ui.screens.model.AchievementType import org.hse.smartcalendar.ui.theme.SmartCalendarTheme import org.hse.smartcalendar.utility.TimeUtils import org.hse.smartcalendar.utility.fromMinutesOfDay import org.hse.smartcalendar.utility.rememberNavigation import org.hse.smartcalendar.view.model.AbstractListViewModel -import org.hse.smartcalendar.view.model.AbstractStatisticsViewModel +import org.hse.smartcalendar.view.model.StatisticsViewModel import org.hse.smartcalendar.view.model.StatisticsManager import org.junit.Before import org.junit.Rule @@ -27,6 +28,7 @@ class AchievementsScreenTest { val composeTestRule = createComposeRule() @Before fun initTasks(){ + StatisticsStore.uploader={} firstTask = DailyTask( title = "first", id = UUID.randomUUID(), @@ -60,7 +62,7 @@ class AchievementsScreenTest { } @Test fun achievementsShowsStreak() { - val statisticsViewModel = AbstractStatisticsViewModel() + val statisticsViewModel = StatisticsViewModel() val listViewModel = AbstractListViewModel(StatisticsManager(statisticsViewModel)) //нужно потестить каждый элемент:Planning everything - без заданий 0, // с заданием 5ч 5/10, c 24ч 24 часа @@ -81,7 +83,7 @@ class AchievementsScreenTest { assertAchievementData(AchievementType.PlanToday, "5/10") assertAchievementData(AchievementType.CommonSpend, "0/10") listViewModel.changeTaskCompletion(firstTask, true) - assert(statisticsViewModel.getTotalTimeActivityTypes().Common.toMinutes().toInt() == firstTask.getMinutesLength()) + assert(statisticsViewModel.uiState.value.total.Common.toMinutes().toInt() == firstTask.getMinutesLength()) composeTestRule.runOnIdle {} assertAchievementData(AchievementType.CommonSpend, "5/10") assertAchievementData(AchievementType.Streak, "1/5") diff --git a/app/src/main/java/org/hse/smartcalendar/data/TotalTaskTypes.kt b/app/src/main/java/org/hse/smartcalendar/data/TotalTaskTypes.kt index 2c39343..8c5f9a4 100644 --- a/app/src/main/java/org/hse/smartcalendar/data/TotalTaskTypes.kt +++ b/app/src/main/java/org/hse/smartcalendar/data/TotalTaskTypes.kt @@ -1,7 +1,7 @@ package org.hse.smartcalendar.data import org.hse.smartcalendar.view.model.state.TimePeriod -import org.hse.smartcalendar.view.model.AbstractStatisticsViewModel.Companion.getPercent +import org.hse.smartcalendar.view.model.StatisticsViewModel.Companion.getPercent class TotalTimeTaskTypes(common: Long, work: Long, study: Long, fitness: Long){ val All: TimePeriod = TimePeriod(work+study+common+fitness) @@ -19,6 +19,14 @@ class TotalTimeTaskTypes(common: Long, work: Long, study: Long, fitness: Long){ private set var WorkPercent: Float = getPercent(work, totalMinutes) private set + fun getPercentByType(type: DailyTaskType): Float { + return when (type) { + DailyTaskType.COMMON -> CommonPercent + DailyTaskType.FITNESS -> FitnessPercent + DailyTaskType.WORK -> WorkPercent + DailyTaskType.STUDIES -> StudyPercent + } + } fun completeTask(task: DailyTask, isComplete: Boolean){ val taskMinutesLength = task.getMinutesLength().toLong() when(isComplete){ @@ -28,21 +36,18 @@ class TotalTimeTaskTypes(common: Long, work: Long, study: Long, fitness: Long){ false -> totalMinutes-=taskMinutesLength } All.addMinutes(taskMinutesLength, isComplete) - when(task.getDailyTaskType()){ - DailyTaskType.COMMON -> {Common.addMinutes(taskMinutesLength, isComplete) - CommonPercent=getPercent(Common.toMinutes(), totalMinutes)} - DailyTaskType.FITNESS -> { - Fitness.addMinutes(taskMinutesLength, isComplete) - FitnessPercent=getPercent(Fitness.toMinutes(), totalMinutes) - } - DailyTaskType.WORK -> { - Work.addMinutes(taskMinutesLength, isComplete) - WorkPercent=getPercent(Work.toMinutes(), totalMinutes) - } - DailyTaskType.STUDIES -> { - Study.addMinutes(taskMinutesLength, isComplete) - StudyPercent=getPercent(Study.toMinutes(), totalMinutes) - } + when (task.getDailyTaskType()) { + DailyTaskType.COMMON -> Common.addMinutes(taskMinutesLength, isComplete) + DailyTaskType.FITNESS -> Fitness.addMinutes(taskMinutesLength, isComplete) + DailyTaskType.WORK -> Work.addMinutes(taskMinutesLength, isComplete) + DailyTaskType.STUDIES -> Study.addMinutes(taskMinutesLength, isComplete) } + recalculatePercents() + } + private fun recalculatePercents() { + StudyPercent = getPercent(Study.toMinutes(), totalMinutes) + CommonPercent = getPercent(Common.toMinutes(), totalMinutes) + FitnessPercent = getPercent(Fitness.toMinutes(), totalMinutes) + WorkPercent = getPercent(Work.toMinutes(), totalMinutes) } } \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/data/store/StatisticsStore.kt b/app/src/main/java/org/hse/smartcalendar/data/store/StatisticsStore.kt index b967b59..f062215 100644 --- a/app/src/main/java/org/hse/smartcalendar/data/store/StatisticsStore.kt +++ b/app/src/main/java/org/hse/smartcalendar/data/store/StatisticsStore.kt @@ -33,8 +33,16 @@ object StatisticsStore { private set var weekTime by mutableStateOf(WeekTime(0)) private set - val calculator = StatisticsCalculator() - + var calculator = StatisticsCalculator() + private set + fun clear() { + totalTime = TotalTimeTaskTypes(0, 0, 0, 0) + averageDayTime = AverageDayTimeVars(firstDay = LocalDate(1970, 1, 1), totalWorkMinutes = 0) + todayTime = TodayTimeVars(0, 0) + weekTime = WeekTime(0) + calculator = StatisticsCalculator() + } + var uploader: () -> Unit = { uploadStatistics() } suspend fun init(): NetworkResponse = withContext(Dispatchers.IO) { when(val resp = repository.getStatistics()) { is NetworkResponse.Success -> { @@ -58,11 +66,12 @@ object StatisticsStore { } } - fun createOrDeleteTask(task: DailyTask, allTasks: List, isAdd: Boolean) { - if (task.isComplete().not() && !isAdd) return - if (task.belongsCurrentDay()) todayTime = todayTime.apply { Planned.plusMinutes(task.getMinutesLength(), isAdd) } - calculator.addOrDeleteTask(StatisticsCalculator.AddOrDeleteRequest(date = task.getTaskDate(), dateTasks = allTasks)) - uploadStatistics() + fun createOrDeleteTask(task: DailyTask, dateTasks: List, isAdd: Boolean) { + createOrDeleteTask(task, isAdd) + calculator.addOrDeleteTask( + StatisticsCalculator.AddOrDeleteRequest + (date = task.getTaskDate(), dateTasks = dateTasks)) + uploader() } fun changeTaskCompletion(task: DailyTask, isComplete: Boolean, isUploadStats: Boolean=true) { @@ -71,7 +80,7 @@ object StatisticsStore { totalTime = totalTime.apply { completeTask(task, isComplete) } averageDayTime = averageDayTime.apply { update(totalTime.totalMinutes) } calculator.changeTaskCompletion(task) - if (isUploadStats) {uploadStatistics()} + if (isUploadStats) {uploader()} } fun changeTask(task: DailyTask, newTask: DailyTask){ if (task.isComplete()){ @@ -80,9 +89,9 @@ object StatisticsStore { } createOrDeleteTask(task, false) createOrDeleteTask(newTask, true) - uploadStatistics() + uploader() } - fun uploadStatistics() { + private fun uploadStatistics() { val workManager = WorkManagerHolder.getInstance() val statsDTO = StatisticsDTO.fromStore() val json = Json.encodeToString(StatisticsDTO.serializer(), statsDTO) @@ -97,4 +106,5 @@ object StatisticsStore { workRequest ) } + } \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt b/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt index 021dc56..5d3ad9a 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt @@ -8,6 +8,7 @@ import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType import org.hse.smartcalendar.utility.TimeUtils import java.util.UUID +import androidx.compose.runtime.MutableState data class RegisterResponse ( val id: Long? = null, @@ -86,4 +87,31 @@ data class ChatTaskResponse( ) return task } + private fun applyToState( + change: T?, + state: MutableState + ){ + if (change !=null){ + state.value = change + } + } + fun applyToUiStates( + taskTitle: MutableState, + taskDescription: MutableState, + taskType: MutableState, + startTime: MutableState, + endTime: MutableState, + isErrorInRecorder: MutableState + ) { + isErrorInRecorder.value = false + + taskTitle.value = this.title + taskDescription.value = this.description + val newType = type?.let { DailyTaskType.valueOf(it.uppercase()) } + applyToState(newType, taskType) + val newStart = start?.time?.toSecondOfDay()?.div(60) + applyToState(newStart, startTime) + val newEnd = start?.time?.toSecondOfDay()?.div(60) + applyToState(newEnd, endTime) + } } diff --git a/app/src/main/java/org/hse/smartcalendar/repository/AudioRepository.kt b/app/src/main/java/org/hse/smartcalendar/repository/AudioRepository.kt index 4edb6e2..cdab988 100644 --- a/app/src/main/java/org/hse/smartcalendar/repository/AudioRepository.kt +++ b/app/src/main/java/org/hse/smartcalendar/repository/AudioRepository.kt @@ -1,12 +1,70 @@ package org.hse.smartcalendar.repository +import okhttp3.MediaType.Companion.toMediaType import okhttp3.MultipartBody +import org.hse.smartcalendar.data.User import org.hse.smartcalendar.network.AudioApiInterface import org.hse.smartcalendar.network.ChatTaskResponse import org.hse.smartcalendar.network.NetworkResponse +import org.hse.smartcalendar.data.DailyTask +import java.io.File +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.RequestBody.Companion.asRequestBody +import org.hse.smartcalendar.store.StatisticsStore class AudioRepository(private val api: AudioApiInterface) : BaseRepository() { + suspend fun sendAudioAndAddTasks(audioFile: File): NetworkResponse> = withContext(Dispatchers.IO) { + return@withContext try { + val requestFile = audioFile.asRequestBody("audio/*".toMediaType()) + val part = MultipartBody.Part.createFormData("file", audioFile.name, requestFile) + + when (val resp = sendAudio(part)) { + is NetworkResponse.Success -> { + val newTasks: List = resp.data.map { it.toDailyTask() } + val addedTasks: ArrayList = ArrayList() + val schedule = User.getSchedule() + newTasks.forEach { task ->{ + val dailySchedule = schedule + .getOrCreateDailySchedule(task.getTaskDate()) + if (dailySchedule + .addDailyTask(task)){ + addedTasks.add(task) + } + } + } + NetworkResponse.Success(addedTasks) + } + is NetworkResponse.Error -> NetworkResponse.Error(resp.message) + is NetworkResponse.NetworkError -> NetworkResponse.NetworkError(resp.exceptionMessage) + is NetworkResponse.Loading -> NetworkResponse.Loading + } + } catch (e: Exception) { + NetworkResponse.NetworkError(e.localizedMessage ?: "Unknown error") + } + } + suspend fun sendAudioGetResponse(audioFile: File): NetworkResponse = withContext(Dispatchers.IO) { + return@withContext try { + val requestFile = audioFile.asRequestBody("audio/*".toMediaType()) + val part = MultipartBody.Part.createFormData("file", audioFile.name, requestFile) + when (val resp = sendAudio(part)) { + is NetworkResponse.Success -> { + if (resp.data.isEmpty()){ + NetworkResponse.Error("response empty") + } + NetworkResponse.Success(resp.data[0]) + } + is NetworkResponse.Error -> NetworkResponse.Error(resp.message) + is NetworkResponse.NetworkError -> NetworkResponse.NetworkError(resp.exceptionMessage) + is NetworkResponse.Loading -> NetworkResponse.Loading + } + } catch (e: Exception) { + NetworkResponse.NetworkError(e.localizedMessage ?: "Unknown error") + } + } + + // existing method suspend fun sendAudio(filePart: MultipartBody.Part): NetworkResponse> { return withSupplierRequest { api.processAudio(filePart) } } diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt index 6b10430..04ef3d5 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/AchievementsScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -42,16 +43,16 @@ import org.hse.smartcalendar.ui.theme.Purple import org.hse.smartcalendar.ui.theme.SmartCalendarTheme import org.hse.smartcalendar.utility.Navigation import org.hse.smartcalendar.utility.Screens -import org.hse.smartcalendar.view.model.AbstractStatisticsViewModel +import org.hse.smartcalendar.view.model.StatisticsViewModel import org.hse.smartcalendar.view.model.ListViewModel import org.hse.smartcalendar.view.model.StatisticsManager -import org.hse.smartcalendar.view.model.StatisticsViewModel import org.hse.smartcalendar.view.model.TaskEditViewModel - +import androidx.compose.runtime.getValue @Composable fun AchievementsScreen(navigation: Navigation, openDrawer: (()->Unit)?=null, - statisticsModel: AbstractStatisticsViewModel) { + statisticsModel: StatisticsViewModel) { + val uiState by statisticsModel.uiState.collectAsState() val itemsData = AchievementType.entries.toTypedArray() Scaffold( topBar = { TopButton(openDrawer, navigation, "Achievements") } @@ -61,10 +62,10 @@ fun AchievementsScreen(navigation: Navigation, .padding(paddingValues), content = { items(itemsData.size) { index -> - val parameterProvider = itemsData[index].parameterProvider + val parameter = itemsData[index].parameterProvider(uiState) AchievementCard( itemsData[index], - parameterProvider.invoke(statisticsModel) + parameter ) } } diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt index d067a7f..a9ccefe 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt @@ -25,13 +25,17 @@ import org.hse.smartcalendar.ui.elements.ChartModel import org.hse.smartcalendar.ui.elements.CircleColored import org.hse.smartcalendar.ui.navigation.TopButton import org.hse.smartcalendar.ui.theme.SmartCalendarTheme +import androidx.compose.runtime.collectAsState import org.hse.smartcalendar.utility.Navigation import org.hse.smartcalendar.utility.rememberNavigation -import org.hse.smartcalendar.view.model.AbstractStatisticsViewModel.Companion.toPercent +import org.hse.smartcalendar.view.model.StatisticsViewModel.Companion.toPercent import org.hse.smartcalendar.view.model.StatisticsViewModel +import androidx.compose.runtime.getValue +import org.hse.smartcalendar.view.model.state.DaysAmount @Composable fun StatisticsScreen(navigation: Navigation, openMenu: () -> Unit, statisticsModel: StatisticsViewModel) { + val uiState by statisticsModel.uiState.collectAsState() fun safeDelete(dividend: Long, divisor: Long): Float { return if (divisor == 0L || dividend == 0L) 0f else dividend.toFloat() / divisor } @@ -47,22 +51,22 @@ fun StatisticsScreen(navigation: Navigation, openMenu: () -> Unit, statisticsMod ) { Row { SafeProgressBox( - dividend = statisticsModel.getTodayCompletedTime().time.inWholeMinutes, - divisor = statisticsModel.getTodayPlannedTime().time.inWholeMinutes, + dividend = {uiState.today.Completed.time.inWholeMinutes}, + divisor = {uiState.today.Planned.time.inWholeMinutes}, label = "Completed/Planned", color = Color.Blue, modifier = Modifier.weight(1f / 3f) ) SafeProgressBox( - dividend = statisticsModel.getTodayCompletedTime().time.inWholeMinutes, - divisor = statisticsModel.getAverageDailyTime().time.inWholeMinutes, + dividend = {uiState.today.Completed.time.inWholeMinutes}, + divisor = {uiState.averageDay.All.time.inWholeMinutes}, label = "Completed/Average", color = Color.Green, modifier = Modifier.weight(1f / 3f) ) SafeProgressBox( - dividend = statisticsModel.getTodayContinuesSuccessDays().amount.toLong(), - divisor = statisticsModel.getRecordContinuesSuccessDays().amount.toLong(), + dividend = {uiState.calculable.continuesCurrent.toLong()}, + divisor = {uiState.calculable.continuesTotal.toLong()}, label = "Days in a row, when completed all the tasks", color = Color.Red, modifier = Modifier.weight(1f / 3f) @@ -72,43 +76,43 @@ fun StatisticsScreen(navigation: Navigation, openMenu: () -> Unit, statisticsMod Row { Box(modifier = Modifier.weight(0.5f), contentAlignment = Alignment.Center){ Column { - Text("Average work time:") + Text("Average activity time:") Text("Today planned work time:") - Text("Total work time:") + Text("Total activity time:") Text("Maximum days in a row when all tasks are completed:") } } Box(modifier = Modifier.weight(0.5f)){ Column { - Text(statisticsModel.getAverageDailyTime().toFullString()) - Text(statisticsModel.getTodayPlannedTime().toFullString()) - Text(statisticsModel.getTotalWorkTime().toPrettyString()) - Text(statisticsModel.getRecordContinuesSuccessDays().toPrettyString()) + Text(uiState.averageDay.All.toFullString()) + Text(uiState.today.Planned.toFullString()) + Text(uiState.total.All.toPrettyString()) + Text(DaysAmount(uiState.calculable.continuesTotal).toPrettyString()) } } } } - ActivityTypesDataDisplay(typesModel = statisticsModel.getTotalTimeActivityTypes(), + ActivityTypesDataDisplay(typesModel = {uiState.total}, columnHeight = 8.dp); } } } @Composable fun ActivityTypesDataDisplay(modifier: Modifier=Modifier, - typesModel: TotalTimeTaskTypes, + typesModel: ()->TotalTimeTaskTypes, columnHeight: Dp ){ val charts = listOf( - ChartModel(value = typesModel.StudyPercent, color = DailyTaskType.STUDIES.color), - ChartModel(value = typesModel.FitnessPercent, color = DailyTaskType.FITNESS.color), - ChartModel(value = typesModel.CommonPercent, color = DailyTaskType.COMMON.color), - ChartModel(value = typesModel.WorkPercent, color = DailyTaskType.WORK.color), + ChartModel(value = typesModel().StudyPercent, color = DailyTaskType.STUDIES.color), + ChartModel(value = typesModel().FitnessPercent, color = DailyTaskType.FITNESS.color), + ChartModel(value = typesModel().CommonPercent, color = DailyTaskType.COMMON.color), + ChartModel(value = typesModel().WorkPercent, color = DailyTaskType.WORK.color), ) - val tableData = mapOf("Work" to Pair(typesModel.WorkPercent, DailyTaskType.WORK.color), + val tableData = mapOf("Work" to Pair(typesModel().WorkPercent, DailyTaskType.WORK.color), "Study" to - Pair(typesModel.StudyPercent, DailyTaskType.STUDIES.color), - "Fitness" to Pair(typesModel.FitnessPercent, DailyTaskType.FITNESS.color), - "Common" to Pair(typesModel.CommonPercent, DailyTaskType.COMMON.color)) + Pair(typesModel().StudyPercent, DailyTaskType.STUDIES.color), + "Fitness" to Pair(typesModel().FitnessPercent, DailyTaskType.FITNESS.color), + "Common" to Pair(typesModel().CommonPercent, DailyTaskType.COMMON.color)) Column(verticalArrangement = Arrangement.Center) { ChartCirclePie(modifier.align(Alignment.CenterHorizontally), charts) tableData.forEach { @@ -129,19 +133,19 @@ fun ActivityTypesDataDisplay(modifier: Modifier=Modifier, } @Composable fun SafeProgressBox( - dividend: Long, - divisor: Long, + dividend: ()->Long, + divisor: ()->Long, label: String, color: Color, modifier: Modifier = Modifier ) { Box(modifier = modifier, contentAlignment = Alignment.Center) { - if (dividend == 0L) { + if (dividend() == 0L) { Box(modifier = modifier.then(Modifier.size(100.dp)), contentAlignment = Alignment.Center) { Text("No data") } } else { - fun progress(): Float = dividend.toFloat() / divisor + fun progress(): Float = dividend().toFloat() / divisor() ProgressCircleWithText( progress = { progress() }, text = label, diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/model/AchievmentType.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/model/AchievmentType.kt index 5cd02e5..3240b9d 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/model/AchievmentType.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/model/AchievmentType.kt @@ -3,14 +3,15 @@ package org.hse.smartcalendar.ui.screens.model import androidx.annotation.DrawableRes import androidx.compose.runtime.Composable import org.hse.smartcalendar.R -import org.hse.smartcalendar.view.model.AbstractStatisticsViewModel +import org.hse.smartcalendar.view.model.StatisticsViewModel +import org.hse.smartcalendar.view.model.state.StatisticsUiState enum class AchievementType( val title: String, @DrawableRes val iconId: Int, val description: (Long) -> String, val levels: List, - val parameterProvider: @Composable (AbstractStatisticsViewModel) -> Long,//читает изменения State + val parameterProvider: (StatisticsUiState) -> Long,//читает изменения State val testTag: String ) { Streak( @@ -18,7 +19,7 @@ enum class AchievementType( iconId = R.drawable.fire, description = { i -> "Reach a $i day streak" }, levels = listOf(5, 10, 20, 50, 100), - parameterProvider = { stats -> stats.statisticsCalculator.stats.value.continuesCurrent.toLong() }, + parameterProvider = { uiState->uiState.calculable.continuesCurrent.toLong() }, testTag = "Fire" ), PlanToday( @@ -26,7 +27,7 @@ enum class AchievementType( iconId = R.drawable.writing_hand, description = { i -> "Plan $i hours of your time today" }, levels = listOf(5, 10, 20, 24), - parameterProvider = { stats -> (stats.TodayTime.Planned.time.inWholeMinutes + 1) / 60 }, + parameterProvider = { uiState->(uiState.today.Planned.time.inWholeMinutes + 1) / 60 }, testTag = "PlanToday" ), CommonSpend( @@ -34,7 +35,7 @@ enum class AchievementType( iconId = R.drawable.yawning_face, description = { i -> "Spend $i hours with common tasks" }, levels = listOf(10, 20, 50, 100, 1000), - parameterProvider = { stats -> stats.TotalTime.Common.time.inWholeHours }, + parameterProvider = { uiState->uiState.total.Common.time.inWholeHours }, testTag = "CommonSpend" ), WorkTotal( @@ -42,7 +43,7 @@ enum class AchievementType( iconId = R.drawable.tasks_complete, description = { i -> "Work a total of $i hours" }, levels = listOf(10, 20, 50, 100, 1000), - parameterProvider = { stats -> stats.TotalTime.All.time.inWholeHours }, + parameterProvider = { uiState->uiState.total.All.time.inWholeHours }, testTag = "WorkTotal" ), WorkWeek( @@ -50,7 +51,7 @@ enum class AchievementType( iconId = R.drawable.robot, description = { i -> "Work a total of $i hours last week" }, levels = listOf(4, 6, 8, 10), - parameterProvider = { stats -> stats.weekTime.All.time.inWholeHours / 7 }, + parameterProvider = { uiState->uiState.week.All.time.inWholeHours / 7 }, testTag = "WorkWeek" ), TypesToday( @@ -58,7 +59,7 @@ enum class AchievementType( iconId = R.drawable.balance_scale, description = { i -> "Have $i types of task in day" }, levels = listOf(4), - parameterProvider = { stats -> stats.statisticsCalculator.stats.value.typesInDay.toLong() }, + parameterProvider = { uiState->uiState.calculable.typesInDay.toLong() }, testTag = "TypesToday" ); } \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt index 87614a2..6f01b0a 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt @@ -41,6 +41,7 @@ import kotlinx.datetime.LocalTime import org.hse.smartcalendar.data.DailySchedule import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.network.ChatTaskResponse import org.hse.smartcalendar.ui.elements.AudioRecorderButton import org.hse.smartcalendar.ui.theme.SmartCalendarTheme import org.hse.smartcalendar.utility.fromMinutesOfDay @@ -55,7 +56,7 @@ fun BottomSheet( isBottomSheetVisible: MutableState, sheetState: SheetState, onDismiss: () -> Unit, - onRecordStop: () -> DailyTask? = { null }, + onRecordStop: () -> ChatTaskResponse? = { null }, audioFile: MutableState, taskTitle: MutableState, taskType: MutableState, @@ -180,13 +181,13 @@ fun BottomSheet( onStop = { val task = onRecordStop() if (task != null) { - isErrorInRecorder.value = false - taskTitle.value = task.getDailyTaskTitle() - taskDescription.value = task.getDailyTaskDescription() - taskType.value = task.getDailyTaskType() - startTime.value = - LocalTime.toMinutesOfDay(task.getDailyTaskStartTime()) - endTime.value = LocalTime.toMinutesOfDay(task.getDailyTaskEndTime()) + task.applyToUiStates( + taskTitle = taskTitle, + taskDescription = taskDescription, + taskType = taskType, + isErrorInRecorder = isErrorInRecorder, + startTime = startTime, + endTime = endTime) } else { isErrorInRecorder.value = true } diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt index 0430cb1..df7fb69 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt @@ -7,13 +7,14 @@ import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.workDataOf +import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.DatePeriod import kotlinx.datetime.LocalDate -import kotlinx.datetime.LocalTime import kotlinx.datetime.TimeZone import kotlinx.datetime.minus import kotlinx.datetime.plus @@ -24,7 +25,10 @@ import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskAction import org.hse.smartcalendar.data.User import org.hse.smartcalendar.data.WorkManagerHolder +import org.hse.smartcalendar.network.ApiClient +import org.hse.smartcalendar.network.ChatTaskResponse import org.hse.smartcalendar.network.NetworkResponse +import org.hse.smartcalendar.repository.AudioRepository import org.hse.smartcalendar.work.TaskApiWorker import java.io.File @@ -40,7 +44,7 @@ open class AbstractListViewModel(val statisticsManager: StatisticsManager) : Vie .toLocalDateTime(TimeZone.currentSystemDefault()).date ) val dailyTaskList: SnapshotStateList = mutableStateListOf() - private val user: User = User + protected val user: User = User init { loadDailyTasks() } @@ -139,7 +143,7 @@ open class AbstractListViewModel(val statisticsManager: StatisticsManager) : Vie } class ListViewModel(statisticsManager: StatisticsManager) : AbstractListViewModel(statisticsManager) { private val workManager = WorkManagerHolder.getInstance() - + private val audioRepo = AudioRepository(ApiClient.audioApiService) override fun scheduleTaskRequest(task: DailyTask, action: DailyTaskAction.Type) { val taskJson = Json.encodeToString(DailyTaskAction.serializer(), DailyTaskAction(task = task, type = action)) @@ -155,22 +159,20 @@ class ListViewModel(statisticsManager: StatisticsManager) : AbstractListViewMode fun sendAudio( audioFile: MutableState, description: AudioDescription, - ): DailyTask? { - // TODO Надо написать отправку файла и обработку ответа. - - Thread.sleep(1000) - val task: DailyTask = DailyTask( - title = "TODO", - description = "TODO", - start = LocalTime(0, 0), - end = LocalTime(0, 0), - date = DailyTask.defaultDate - ) - val nowDate = this.getScheduleDate() - this.changeDailyTaskSchedule(task.getTaskDate()) - this.addDailyTask(task) - this.changeDailyTaskSchedule(nowDate) - return task + ): ChatTaskResponse? { + val file = audioFile.value + var response: ChatTaskResponse? = null + if (file!=null) { + viewModelScope.launch { + _actionResult.value = NetworkResponse.Loading + val result = audioRepo.sendAudioGetResponse(file) + if (result is NetworkResponse.Success){ + response = result.data + } + _actionResult.value = result + } + } + return response } class NestedTask(val nestedTask: DailyTask) : diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsManager.kt b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsManager.kt index 093f3aa..627ff3d 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsManager.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsManager.kt @@ -5,7 +5,7 @@ import org.hse.smartcalendar.data.DailyTask /** * Класс, передающий изменения ListVM и TaskEditVM в StatisticsVM */ -class StatisticsManager(private val viewModel: AbstractStatisticsViewModel){ +class StatisticsManager(private val viewModel: StatisticsViewModel){ fun changeTaskCompletion(task: DailyTask, isCompleted: Boolean){ viewModel.changeTaskCompletion(task, isCompleted) } diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt index 063c898..29fba10 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/StatisticsViewModel.kt @@ -4,33 +4,22 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.workDataOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import kotlinx.datetime.LocalDate -import kotlinx.serialization.json.Json import org.hse.smartcalendar.data.DailyTask -import org.hse.smartcalendar.data.TotalTimeTaskTypes -import org.hse.smartcalendar.data.WorkManagerHolder -import org.hse.smartcalendar.network.ApiClient import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.network.StatisticsDTO -import org.hse.smartcalendar.work.StatisticsUploadWorker -import org.hse.smartcalendar.repository.StatisticsRepository import org.hse.smartcalendar.store.StatisticsStore -import org.hse.smartcalendar.view.model.state.DayPeriod -import org.hse.smartcalendar.view.model.state.DaysAmount -import org.hse.smartcalendar.view.model.state.StatisticsCalculator -import org.hse.smartcalendar.view.model.state.TimePeriod -import org.hse.smartcalendar.view.model.state.AverageDayTimeVars -import org.hse.smartcalendar.view.model.state.TodayTimeVars -import org.hse.smartcalendar.view.model.state.WeekTime +import org.hse.smartcalendar.view.model.state.StatisticsUiState import kotlin.math.roundToInt -open class AbstractStatisticsViewModel():ViewModel() { +open class StatisticsViewModel():ViewModel() { var _initResult = MutableLiveData>() val initResult:LiveData> = _initResult + private val _uiState = MutableStateFlow(StatisticsUiState()) + val uiState: StateFlow = _uiState.asStateFlow() companion object { fun getPercent(part: Long, all: Long): Float { if (all == 0L) return 25.0f @@ -43,30 +32,37 @@ open class AbstractStatisticsViewModel():ViewModel() { return (part * 1000).roundToInt().toFloat() / 10 } } + fun updateStatistics(){ + _uiState.value = StatisticsUiState( + total = StatisticsStore.totalTime, + week = StatisticsStore.weekTime, + averageDay = StatisticsStore.averageDayTime, + today = StatisticsStore.todayTime, + calculable = StatisticsStore.calculator.stats.value + ) + } fun init(){ viewModelScope.launch { _initResult.value = NetworkResponse.Loading - _initResult.value = StatisticsStore.init() + val response = StatisticsStore.init() + if (response is NetworkResponse.Success){ + updateStatistics() + } + _initResult.value = response } } - open fun uploadStatistics(){ + fun createOrDeleteTask(task: DailyTask, dailyTaskList: List, isCreate: Boolean) { + StatisticsStore.createOrDeleteTask(task, dailyTaskList, isCreate) + updateStatistics() } -} -class StatisticsViewModel(): AbstractStatisticsViewModel(){ - override fun uploadStatistics() { - val workManager = WorkManagerHolder.getInstance() - val statsDTO = StatisticsDTO.fromViewModel(this) - val json = Json.encodeToString(StatisticsDTO.serializer(), statsDTO) - - val workRequest = OneTimeWorkRequestBuilder() - .setInputData(workDataOf("statistics_json" to json)) - .build() + fun changeTaskCompletion(task: DailyTask, isComplete: Boolean, isUploadStats: Boolean = true) { + StatisticsStore.changeTaskCompletion(task, isComplete, isUploadStats) + updateStatistics() + } - workManager.enqueueUniqueWork( - "upload_stats", - ExistingWorkPolicy.REPLACE, - workRequest - ) + fun changeTask(task: DailyTask, newTask: DailyTask) { + StatisticsStore.changeTask(task, newTask) + updateStatistics() } } \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsState.kt b/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsState.kt index b26bb23..d3dbae4 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsState.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsState.kt @@ -2,10 +2,19 @@ package org.hse.smartcalendar.view.model.state import kotlinx.datetime.LocalDate import kotlinx.datetime.daysUntil +import org.hse.smartcalendar.data.TotalTimeTaskTypes import org.hse.smartcalendar.network.AverageDayTime import org.hse.smartcalendar.network.TodayTime import org.hse.smartcalendar.utility.TimeUtils +data class StatisticsUiState( + val total: TotalTimeTaskTypes = TotalTimeTaskTypes(0,0,0,0), + val week: WeekTime = WeekTime(0), + val averageDay: AverageDayTimeVars = AverageDayTimeVars(firstDay = LocalDate(1970,1,1), totalWorkMinutes = 0), + val today: TodayTimeVars = TodayTimeVars(0,0), + val calculable: StatisticsCalculableData = StatisticsCalculableData(0, 0, 0) +) + class TodayTimeVars(planned: Long, completed: Long){ val Planned: DayPeriod = DayPeriod(planned) var Completed: DayPeriod = DayPeriod(completed) diff --git a/app/src/main/java/org/hse/smartcalendar/work/AudioWorker.kt b/app/src/main/java/org/hse/smartcalendar/work/AudioWorker.kt deleted file mode 100644 index daf5277..0000000 --- a/app/src/main/java/org/hse/smartcalendar/work/AudioWorker.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.hse.smartcalendar.work - -import android.content.Context -import androidx.work.CoroutineWorker -import androidx.work.WorkerParameters -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.MultipartBody -import okhttp3.RequestBody -import org.hse.smartcalendar.repository.AudioRepository -import java.io.File -import org.hse.smartcalendar.data.User -import org.hse.smartcalendar.network.ApiClient -import org.hse.smartcalendar.network.NetworkResponse - -class AudioWorker( - context: Context, - params: WorkerParameters -) : CoroutineWorker(context, params) { - private val repo = AudioRepository(ApiClient.audioApiService) - - override suspend fun doWork(): Result { - val path = inputData.getString("audio_path") ?: return Result.failure() - val descJson = inputData.getString("audio_desc") ?: return Result.failure() - val file = File(path) - val requestFile = RequestBody.create("audio/*".toMediaType(), file) - val part = MultipartBody.Part.createFormData("file", file.name, requestFile) - val response = repo.sendAudio(part) - return if (response is NetworkResponse.Success) { - val newTasks = response.data.map{it.toDailyTask()}; - for (task in newTasks) { - User.getSchedule().getOrCreateDailySchedule(task.getTaskDate()).addDailyTask(task) - } - Result.success() - } else { - Result.retry() - } - } -} \ No newline at end of file diff --git a/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt b/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt index 8fdddb3..d4bde24 100644 --- a/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt +++ b/app/src/test/kotlin/org/hse/smartcalendar/view/model/StatisticsTest.kt @@ -6,7 +6,9 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import org.hse.smartcalendar.data.DailyTask +import org.hse.smartcalendar.data.DailyTaskType import org.hse.smartcalendar.data.User +import org.hse.smartcalendar.store.StatisticsStore import org.hse.smartcalendar.utility.TimeUtils import org.hse.smartcalendar.view.model.state.CalcState import org.junit.jupiter.api.AfterAll @@ -23,7 +25,7 @@ import kotlin.test.assertEquals @OptIn(ExperimentalCoroutinesApi::class) class StatisticsTest { private val testDispatcher = StandardTestDispatcher() - private lateinit var statisticsViewModel: AbstractStatisticsViewModel + private lateinit var statisticsViewModel: StatisticsViewModel private lateinit var listViewModel: AbstractListViewModel private lateinit var todayWorkTask: DailyTask private lateinit var todayCommonTask: DailyTask @@ -47,9 +49,9 @@ class StatisticsTest { listViewModel.changeDailyTaskSchedule(TimeUtils.getCurrentDateTime().date) } fun assertCalculatorState(calcState: CalcState){ - Assertions.assertEquals(calcState.currentStreak, statisticsViewModel.statisticsCalculator.stats.value.continuesCurrent) - Assertions.assertEquals(calcState.maxStreak, statisticsViewModel.statisticsCalculator.stats.value.continuesTotal) - Assertions.assertEquals(calcState.types, statisticsViewModel.statisticsCalculator.stats.value.typesInDay) + Assertions.assertEquals(calcState.currentStreak, statisticsViewModel.uiState.value.calculable.continuesCurrent) + Assertions.assertEquals(calcState.maxStreak, statisticsViewModel.uiState.value.calculable.continuesTotal) + Assertions.assertEquals(calcState.types, statisticsViewModel.uiState.value.calculable.typesInDay) } fun setTasks(){ todayWorkTask = TaskProvider.TodayWorkTask.provide() @@ -58,9 +60,16 @@ class StatisticsTest { weekFitnessTask = TaskProvider.WeekFitnessTask.provide() yesterdayTask = TaskProvider.YesterdayTask.provide() } + fun assertPercent(type: DailyTaskType, percent: Float){ + assertEquals( + percent, + statisticsViewModel.uiState.value.total.getPercentByType(type), + absoluteTolerance = 0.1f) + } @BeforeAll fun setUp(){//Global Init, ONE call before ALL tests Dispatchers.setMain(testDispatcher) + StatisticsStore.uploader = {} } @AfterAll//Global Clear, ONE call before ALL tests @@ -69,13 +78,14 @@ class StatisticsTest { } @BeforeEach//Init, call before EACH test fun init(){ - statisticsViewModel = AbstractStatisticsViewModel() + statisticsViewModel = StatisticsViewModel() listViewModel = AbstractListViewModel(StatisticsManager(statisticsViewModel)) setTasks() } @AfterEach//Clear, call after EACH test fun clear(){ User.clearSchedule() + StatisticsStore.clear() } /** * check state in BeforeAll in WithPreAddedTasks: Add 4 tasks @@ -87,22 +97,22 @@ class StatisticsTest { listViewModel.addDailyTask(todayWorkTask) assertEquals( this@StatisticsTest.todayWorkTask.getMinutesLength().toLong(), - statisticsViewModel.getTodayPlannedTime().time.inWholeMinutes, + statisticsViewModel.uiState.value.today.Planned.time.inWholeMinutes, ) listViewModel.addDailyTask(todayCommonTask) assertEquals( (todayWorkTask.getMinutesLength() + todayCommonTask.getMinutesLength()).toLong(), - statisticsViewModel.getTodayPlannedTime().time.inWholeMinutes, + statisticsViewModel.uiState.value.today.Planned.time.inWholeMinutes, ) addTaskInDay(tomorrowTask) assertEquals( (todayWorkTask.getMinutesLength() + todayCommonTask.getMinutesLength()).toLong(), - statisticsViewModel.getTodayPlannedTime().time.inWholeMinutes, + statisticsViewModel.uiState.value.today.Planned.time.inWholeMinutes, ) addTaskInDay(weekFitnessTask) assertEquals( (todayWorkTask.getMinutesLength() + todayCommonTask.getMinutesLength()).toLong(), - statisticsViewModel.getTodayPlannedTime().time.inWholeMinutes, + statisticsViewModel.uiState.value.today.Planned.time.inWholeMinutes, ) } @Test @@ -119,6 +129,10 @@ class StatisticsTest { assertCalculatorState(CalcState(types = 1, currentStreak = 0, maxStreak = 0)) } } + + /** + * до каждого теста добавлены 4 задания + */ @Nested inner class WithPreAddedTasks{ @BeforeEach @@ -133,7 +147,7 @@ class StatisticsTest { listViewModel.removeDailyTask(todayWorkTask) assertEquals( (todayCommonTask.getMinutesLength()).toLong(), - statisticsViewModel.getTodayPlannedTime().time.inWholeMinutes + statisticsViewModel.uiState.value.today.Planned.time.inWholeMinutes ) } fun assertTaskTimeEquals(task: DailyTask, parameter: Long){ @@ -161,49 +175,49 @@ class StatisticsTest { listViewModel.changeTaskCompletion(todayWorkTask, true) assertTaskTimeEquals( todayWorkTask, - listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes, - statisticsViewModel.getTodayCompletedTime().time.inWholeMinutes, - statisticsViewModel.getTotalWorkTime().time.inWholeMinutes, - statisticsViewModel.getTotalTimeActivityTypes().Work.time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.week.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.today.Completed.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.Work.time.inWholeMinutes) ) assertEquals( 100.0f, - statisticsViewModel.getTotalTimeActivityTypes().WorkPercent + statisticsViewModel.uiState.value.total.WorkPercent ) listViewModel.changeTaskCompletion(todayWorkTask, true) assertTaskTimeEquals( todayWorkTask, - listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes, - statisticsViewModel.getTodayCompletedTime().time.inWholeMinutes, - statisticsViewModel.getTotalWorkTime().time.inWholeMinutes, - statisticsViewModel.getTotalTimeActivityTypes().Work.time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.week.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.today.Completed.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.Work.time.inWholeMinutes) ) assertEquals( 100.0f, - statisticsViewModel.getTotalTimeActivityTypes().WorkPercent + statisticsViewModel.uiState.value.total.WorkPercent ) listViewModel.changeTaskCompletion(todayCommonTask, true) assertTaskTimeEquals( listOf(todayWorkTask, todayCommonTask), - listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes, - statisticsViewModel.getTodayCompletedTime().time.inWholeMinutes, - statisticsViewModel.getTotalWorkTime().time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.week.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.today.Completed.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.All.time.inWholeMinutes) ) assertTaskTimeEquals( todayWorkTask, - statisticsViewModel.getTotalTimeActivityTypes().Work.time.inWholeMinutes + statisticsViewModel.uiState.value.total.Work.time.inWholeMinutes ) assertTaskTimeEquals( todayCommonTask, - statisticsViewModel.getTotalTimeActivityTypes().Common.time.inWholeMinutes + statisticsViewModel.uiState.value.total.Common.time.inWholeMinutes ) listViewModel.changeTaskCompletion(todayCommonTask, false) assertTaskTimeEquals( todayWorkTask, - listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes, - statisticsViewModel.getTodayCompletedTime().time.inWholeMinutes, - statisticsViewModel.getTotalWorkTime().time.inWholeMinutes, - statisticsViewModel.getTotalTimeActivityTypes().Work.time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.week.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.today.Completed.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.Work.time.inWholeMinutes) ) } @Test @@ -211,27 +225,27 @@ class StatisticsTest { changeTaskInDay(tomorrowTask, true) assertTaskTimeEquals( listOf(), - listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes, - statisticsViewModel.getTodayCompletedTime().time.inWholeMinutes, - statisticsViewModel.getTotalTimeActivityTypes().Work.time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.week.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.today.Completed.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.Work.time.inWholeMinutes) ) assertTaskTimeEquals( listOf(tomorrowTask), - listOf(statisticsViewModel.getTotalWorkTime().time.inWholeMinutes, - statisticsViewModel.getTotalTimeActivityTypes().Common.time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.total.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.Common.time.inWholeMinutes) ) changeTaskInDay(weekFitnessTask, true) assertTaskTimeEquals( weekFitnessTask, - listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes, - statisticsViewModel.getTotalTimeActivityTypes().Fitness.time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.week.All.time.inWholeMinutes, + statisticsViewModel.uiState.value.total.Fitness.time.inWholeMinutes) ) assertTaskTimeEquals( listOf(tomorrowTask, weekFitnessTask), - listOf(statisticsViewModel.getTotalWorkTime().time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.total.All.time.inWholeMinutes) ) assertEquals(50.0f, - statisticsViewModel.getTotalTimeActivityTypes().FitnessPercent) + statisticsViewModel.uiState.value.total.FitnessPercent) } /** @@ -246,37 +260,48 @@ class StatisticsTest { changeTaskInDay(weekFitnessTask, true) assertTaskTimeEquals( listOf(weekFitnessTask, todayWorkTask), - listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.week.All.time.inWholeMinutes) ) assertEquals(100.0f/3, - statisticsViewModel.getTotalTimeActivityTypes().FitnessPercent, + statisticsViewModel.uiState.value.total.FitnessPercent, absoluteTolerance = 0.1f) listViewModel.removeDailyTask(todayWorkTask) assertTaskTimeEquals( listOf(), - listOf(statisticsViewModel.getTodayCompletedTime().time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.today.Completed.time.inWholeMinutes) ) assertTaskTimeEquals( listOf(weekFitnessTask), - listOf(statisticsViewModel.getWeekWorkTime().time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.week.All.time.inWholeMinutes) ) assertTaskTimeEquals( listOf(weekFitnessTask, tomorrowTask), - listOf(statisticsViewModel.getTotalWorkTime().time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.total.All.time.inWholeMinutes) ) changeTaskInDay(tomorrowTask, false) assertTaskTimeEquals( listOf(weekFitnessTask), - listOf(statisticsViewModel.getTotalWorkTime().time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.total.All.time.inWholeMinutes) ) removeTaskInDay(tomorrowTask) assertTaskTimeEquals( listOf(weekFitnessTask), - listOf(statisticsViewModel.getTotalWorkTime().time.inWholeMinutes) + listOf(statisticsViewModel.uiState.value.total.All.time.inWholeMinutes) ) - assertEquals(100.0f, - statisticsViewModel.getTotalTimeActivityTypes().FitnessPercent, - absoluteTolerance = 0.1f) + assertPercent(DailyTaskType.FITNESS, 100.0f) + } + @Test + fun percentTest(){ + changeTaskInDay(weekFitnessTask, true) + assertPercent(DailyTaskType.FITNESS, 100.0f) + changeTaskInDay(todayWorkTask, true) + assertPercent(DailyTaskType.WORK, 50.0f) + changeTaskInDay(todayWorkTask, false) + assertPercent(DailyTaskType.FITNESS, 100.0f) + changeTaskInDay(todayWorkTask, true) + assertPercent(DailyTaskType.FITNESS, 50.0f) + removeTaskInDay(todayWorkTask) + assertPercent(DailyTaskType.FITNESS, 100.0f) } /** @@ -286,9 +311,9 @@ class StatisticsTest { fun calculateTest(){ assertCalculatorState(CalcState(types = 2, currentStreak = 0, maxStreak = 0)) changeTaskInDay(tomorrowTask, true) - assertEquals(0, statisticsViewModel.statisticsCalculator.stats.value.continuesCurrent) + assertEquals(0, statisticsViewModel.uiState.value.calculable.continuesCurrent) changeTaskInDay(todayWorkTask, true) - assertEquals(0, statisticsViewModel.statisticsCalculator.stats.value.continuesCurrent) + assertEquals(0, statisticsViewModel.uiState.value.calculable.continuesCurrent) changeTaskInDay(todayCommonTask, true) assertCalculatorState(CalcState(types = 2, currentStreak = 1, maxStreak = 1)) addTaskInDay(yesterdayTask) From 0db5f94c1042eb9b2618c2771fccba0bae9ca2cb Mon Sep 17 00:00:00 2001 From: UsatovPavel Date: Wed, 11 Jun 2025 18:34:55 +0300 Subject: [PATCH 11/11] Statistics: updateNextDay Recieve date from server and if it not today recalc week and today stats. Remove debug info button&token from AuthScreen --- .../data/store/StatisticsStore.kt | 36 ++++++++++++++++++- .../hse/smartcalendar/network/ApiClient.kt | 6 ++++ .../smartcalendar/network/StatisticsData.kt | 11 ++++-- .../hse/smartcalendar/network/TypeAdapter.kt | 18 ++++++++++ .../repository/AudioRepository.kt | 1 - .../smartcalendar/ui/screens/AuthScreen.kt | 10 +----- .../ui/screens/StatisticsScreen.kt | 2 +- .../ui/task/DailyTaskCreationWindow.kt | 18 ++++++++-- .../smartcalendar/utility/TimePeriodUtils.kt | 3 ++ .../smartcalendar/view/model/ListViewModel.kt | 1 + 10 files changed, 89 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/hse/smartcalendar/data/store/StatisticsStore.kt b/app/src/main/java/org/hse/smartcalendar/data/store/StatisticsStore.kt index f062215..23cb014 100644 --- a/app/src/main/java/org/hse/smartcalendar/data/store/StatisticsStore.kt +++ b/app/src/main/java/org/hse/smartcalendar/data/store/StatisticsStore.kt @@ -8,19 +8,24 @@ import androidx.work.OneTimeWorkRequestBuilder import androidx.work.workDataOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.LocalDate +import kotlinx.datetime.minus +import kotlinx.datetime.toLocalDateTime import kotlinx.serialization.json.Json import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.TotalTimeTaskTypes +import org.hse.smartcalendar.data.User import org.hse.smartcalendar.data.WorkManagerHolder import org.hse.smartcalendar.network.ApiClient import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.network.StatisticsDTO import org.hse.smartcalendar.repository.StatisticsRepository +import org.hse.smartcalendar.utility.TimeUtils import org.hse.smartcalendar.view.model.state.AverageDayTimeVars +import org.hse.smartcalendar.view.model.state.StatisticsCalculator import org.hse.smartcalendar.view.model.state.TodayTimeVars import org.hse.smartcalendar.view.model.state.WeekTime -import org.hse.smartcalendar.view.model.state.StatisticsCalculator import org.hse.smartcalendar.work.StatisticsUploadWorker object StatisticsStore { @@ -43,6 +48,32 @@ object StatisticsStore { calculator = StatisticsCalculator() } var uploader: () -> Unit = { uploadStatistics() } + private fun updateNextDay(oldDate: LocalDate){ + val currentDate = TimeUtils.getCurrentDateTime().date + if (oldDate == currentDate){ + return + } + val schedule = User.getSchedule() + val todayTasks = schedule + .getOrCreateDailySchedule(currentDate) + .getDailyTaskList() + val todayPlanned = todayTasks.sumOf { it.getMinutesLength().toLong() } + val todayCompleted = todayTasks + .filter { it.isComplete() } + .sumOf { it.getMinutesLength().toLong() } + todayTime = TodayTimeVars(planned = todayPlanned, completed = todayCompleted) + var weekSum = 0L + for (offset in 0L..6L) { + val date = currentDate.minus(offset, DateTimeUnit.DAY) + val tasks = schedule + .getOrCreateDailySchedule(date) + .getDailyTaskList() + weekSum += tasks + .filter { it.isComplete() } + .sumOf { it.getMinutesLength().toLong() } + } + weekTime = WeekTime(weekSum) + } suspend fun init(): NetworkResponse = withContext(Dispatchers.IO) { when(val resp = repository.getStatistics()) { is NetworkResponse.Success -> { @@ -52,6 +83,9 @@ object StatisticsStore { todayTime = TodayTimeVars.fromTodayTimeDTO(d.todayTime) weekTime = WeekTime(d.weekTime) calculator.init(d) + val oldDate = d.jsonDate + .toLocalDateTime(TimeUtils.getCurrentTimezone()).date + updateNextDay(oldDate) resp } else -> resp diff --git a/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt b/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt index 1dc8b20..083c964 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt @@ -1,10 +1,13 @@ package org.hse.smartcalendar.network import DailyTaskTypeAdapter +import InstantTimeAdapter import LocalDateAdapter import LocalDateTimeAdapter import LocalTimeAdapter +import OffsetDateTimeAdapter import com.google.gson.GsonBuilder +import kotlinx.datetime.Instant import kotlinx.datetime.LocalTime import okhttp3.Interceptor import okhttp3.OkHttpClient @@ -13,6 +16,7 @@ import okhttp3.logging.HttpLoggingInterceptor import org.hse.smartcalendar.data.DailyTaskType import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.time.OffsetDateTime object ApiClient { private const val SERVER_BASE_URL = "http://10.0.2.2:8080/" @@ -26,6 +30,8 @@ object ApiClient { .registerTypeAdapter(LocalTime::class.java, LocalTimeAdapter()) .registerTypeAdapter(kotlinx.datetime.LocalDateTime::class.java, LocalDateTimeAdapter()) .registerTypeAdapter(DailyTaskType::class.java, DailyTaskTypeAdapter()) + .registerTypeAdapter(OffsetDateTime::class.java, OffsetDateTimeAdapter()) + .registerTypeAdapter(Instant::class.java, InstantTimeAdapter()) .create() private val retrofit = Retrofit.Builder() .baseUrl(SERVER_BASE_URL) diff --git a/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt b/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt index eaf2c6f..5930c27 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/StatisticsData.kt @@ -1,9 +1,14 @@ package org.hse.smartcalendar.network +import kotlinx.datetime.Clock +import kotlinx.datetime.Clock.System +import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate +import kotlinx.datetime.toInstant import kotlinx.serialization.Serializable import org.hse.smartcalendar.data.TotalTimeTaskTypes import org.hse.smartcalendar.store.StatisticsStore +import org.hse.smartcalendar.utility.TimeUtils @Serializable data class StatisticsDTO( @@ -11,7 +16,8 @@ data class StatisticsDTO( val weekTime: Long, val todayTime: TodayTime, val continuesSuccessDays: ContinuesSuccessDays, - val averageDayTime: AverageDayTime + val averageDayTime: AverageDayTime, + val jsonDate: Instant ) { companion object { fun fromStore(): StatisticsDTO { @@ -35,7 +41,8 @@ data class StatisticsDTO( averageDayTime = AverageDayTime( totalWorkMinutes = StatisticsStore.averageDayTime.All.time.inWholeMinutes, firstDay = StatisticsStore.averageDayTime.firstDay - ) + ), + jsonDate = System.now() ) } } diff --git a/app/src/main/java/org/hse/smartcalendar/network/TypeAdapter.kt b/app/src/main/java/org/hse/smartcalendar/network/TypeAdapter.kt index c2fd578..9a2af02 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/TypeAdapter.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/TypeAdapter.kt @@ -1,10 +1,12 @@ import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter +import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalTime import org.hse.smartcalendar.data.DailyTaskType +import java.time.OffsetDateTime class LocalTimeAdapter : TypeAdapter() { override fun write(out: JsonWriter, value: LocalTime) { @@ -43,3 +45,19 @@ class DailyTaskTypeAdapter : TypeAdapter() { return DailyTaskType.fromString(reader.nextString().uppercase()) } } +class OffsetDateTimeAdapter: TypeAdapter() { + override fun write(out: JsonWriter, value: OffsetDateTime) { + out.value(value.toString()) + } + override fun read(reader: JsonReader): OffsetDateTime { + return OffsetDateTime.parse(reader.nextString()) + } +} +class InstantTimeAdapter: TypeAdapter() { + override fun write(out: JsonWriter, value: Instant) { + out.value(value.toString()) // ISO-8601 with Z + } + override fun read(reader: JsonReader): Instant { + return Instant.parse(reader.nextString()) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/repository/AudioRepository.kt b/app/src/main/java/org/hse/smartcalendar/repository/AudioRepository.kt index cdab988..b0e2d31 100644 --- a/app/src/main/java/org/hse/smartcalendar/repository/AudioRepository.kt +++ b/app/src/main/java/org/hse/smartcalendar/repository/AudioRepository.kt @@ -64,7 +64,6 @@ class AudioRepository(private val api: AudioApiInterface) : BaseRepository() { } } - // existing method suspend fun sendAudio(filePart: MultipartBody.Part): NetworkResponse> { return withSupplierRequest { api.processAudio(filePart) } } diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/AuthScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/AuthScreen.kt index 165dcfb..7191f32 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/AuthScreen.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/AuthScreen.kt @@ -51,14 +51,6 @@ fun AuthScreen(navigation: Navigation, viewModel: AuthViewModel, authType:AuthTy verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { - Button( - onClick = { - navigation.navigateToMainApp(Screens.CALENDAR.route) - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Calendar") - } TextField( value = username, onValueChange = { username = it }, @@ -106,7 +98,7 @@ fun AuthScreen(navigation: Navigation, viewModel: AuthViewModel, authType:AuthTy CircularProgressIndicator() } is NetworkResponse.Success -> { - Text("Login successful! Token: ${state.data.token}") + Text("Login successful!") navigation.navigateTo(Screens.LOADING.route) } is NetworkResponse.Error -> { diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt index a9ccefe..185a346 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/StatisticsScreen.kt @@ -140,7 +140,7 @@ fun SafeProgressBox( modifier: Modifier = Modifier ) { Box(modifier = modifier, contentAlignment = Alignment.Center) { - if (dividend() == 0L) { + if (divisor() == 0L) { Box(modifier = modifier.then(Modifier.size(100.dp)), contentAlignment = Alignment.Center) { Text("No data") } diff --git a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt index 6f01b0a..fc1b949 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt @@ -19,8 +19,10 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -42,6 +44,7 @@ import org.hse.smartcalendar.data.DailySchedule import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType import org.hse.smartcalendar.network.ChatTaskResponse +import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.ui.elements.AudioRecorderButton import org.hse.smartcalendar.ui.theme.SmartCalendarTheme import org.hse.smartcalendar.utility.fromMinutesOfDay @@ -72,9 +75,13 @@ fun BottomSheet( val isNestedTask = rememberSaveable { mutableStateOf(false) } val fstFiledHasFormatError = rememberSaveable { mutableStateOf(false) } val sndFiledHasFormatError = rememberSaveable { mutableStateOf(false) } - + val actionResult by viewModel.actionResult.observeAsState() val isErrorInRecorder = rememberSaveable { mutableStateOf(false) } - + LaunchedEffect(isBottomSheetVisible.value) { + if (isBottomSheetVisible.value){ + isErrorInRecorder.value = false + } + } if (isBottomSheetVisible.value) { ModalBottomSheet( onDismissRequest = { @@ -155,9 +162,14 @@ fun BottomSheet( Spacer(modifier = Modifier.padding(12.dp)) Box(Modifier.align(Alignment.CenterHorizontally)) { + val errorMsg = when (actionResult) { + is NetworkResponse.Error -> (actionResult as NetworkResponse.Error).message + is NetworkResponse.NetworkError -> (actionResult as NetworkResponse.NetworkError).exceptionMessage + else -> "Unknown error" + } if (isErrorInRecorder.value) { Text( - text = "Error in Audio", + text = "Error in Audio:$errorMsg", color = MaterialTheme.colorScheme.error, fontSize = 12.sp, modifier = Modifier.padding(top = 4.dp) diff --git a/app/src/main/java/org/hse/smartcalendar/utility/TimePeriodUtils.kt b/app/src/main/java/org/hse/smartcalendar/utility/TimePeriodUtils.kt index 9281a68..5622510 100644 --- a/app/src/main/java/org/hse/smartcalendar/utility/TimePeriodUtils.kt +++ b/app/src/main/java/org/hse/smartcalendar/utility/TimePeriodUtils.kt @@ -12,6 +12,9 @@ class TimeUtils { fun getCurrentDateTime(): LocalDateTime { return Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()); } + fun getCurrentTimezone(): TimeZone{ + return TimeZone.currentSystemDefault() + } fun numberToWord(amount: Int, item: String): String { if (amount != 0) { diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt index df7fb69..f1b9831 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.viewModelScope import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.workDataOf +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.DatePeriod