From c5b54562d371b7f593f72e32d8f5ae992c3cd3cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC=20=D0=91=D0=B0=D1=82=D1=8B?= =?UTF-8?q?=D0=BB=D0=BA=D0=B8=D0=BD?= Date: Tue, 12 Dec 2023 02:03:27 +0400 Subject: [PATCH] Cool --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 2 +- .../ru/ulstu/is/pmu/StudentApplication.kt | 2 +- .../ru/ulstu/is/pmu/database/AppContainer.kt | 20 +- .../ru/ulstu/is/pmu/database/AppDatabase.kt | 50 ++--- .../is/pmu/database/student/dao/GroupDao.kt | 23 -- .../is/pmu/database/student/dao/StudentDao.kt | 27 --- .../is/pmu/database/student/dao/TaskDao.kt | 30 +++ .../is/pmu/database/student/dao/UserDao.kt | 23 ++ .../is/pmu/database/student/model/Student.kt | 78 ------- .../is/pmu/database/student/model/Task.kt | 80 +++++++ .../student/model/{Group.kt => User.kt} | 19 +- .../student/repository/GroupRepository.kt | 7 - .../repository/OfflineGroupRepository.kt | 8 - .../repository/OfflineStudentRepository.kt | 17 -- .../repository/OfflineTaskRepository.kt | 21 ++ .../repository/OfflineUserRepository.kt | 8 + .../student/repository/StudentRepository.kt | 12 - .../student/repository/TaskRepository.kt | 13 ++ .../student/repository/UserRepository.kt | 8 + .../ulstu/is/pmu/ui/AppViewModelProvider.kt | 20 +- .../java/ru/ulstu/is/pmu/ui/about/About.kt | 4 +- .../ulstu/is/pmu/ui/navigation/MainNavbar.kt | 16 +- .../ru/ulstu/is/pmu/ui/navigation/Screen.kt | 20 +- .../ui/student/edit/GroupDropDownViewModel.kt | 46 ---- .../ui/student/edit/StudentEditViewModel.kt | 97 -------- .../edit/{StudentEdit.kt => TaskEdit.kt} | 146 ++++++------ .../pmu/ui/student/edit/TaskEditViewModel.kt | 96 ++++++++ .../ui/student/edit/UserDropDownViewModel.kt | 46 ++++ .../ui/student/list/StudentListViewModel.kt | 29 --- .../ulstu/is/pmu/ui/student/list/TaskList.kt | 212 ++++++++++++++++++ .../is/pmu/ui/student/list/TaskListEndDate.kt | 136 +++++++++++ .../{StudentList.kt => TaskListFavorite.kt} | 97 +++----- .../pmu/ui/student/list/TaskListViewModel.kt | 60 +++++ app/src/main/res/values/strings.xml | 29 +-- 35 files changed, 940 insertions(+), 563 deletions(-) delete mode 100644 app/src/main/java/ru/ulstu/is/pmu/database/student/dao/GroupDao.kt delete mode 100644 app/src/main/java/ru/ulstu/is/pmu/database/student/dao/StudentDao.kt create mode 100644 app/src/main/java/ru/ulstu/is/pmu/database/student/dao/TaskDao.kt create mode 100644 app/src/main/java/ru/ulstu/is/pmu/database/student/dao/UserDao.kt delete mode 100644 app/src/main/java/ru/ulstu/is/pmu/database/student/model/Student.kt create mode 100644 app/src/main/java/ru/ulstu/is/pmu/database/student/model/Task.kt rename app/src/main/java/ru/ulstu/is/pmu/database/student/model/{Group.kt => User.kt} (62%) delete mode 100644 app/src/main/java/ru/ulstu/is/pmu/database/student/repository/GroupRepository.kt delete mode 100644 app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineGroupRepository.kt delete mode 100644 app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineStudentRepository.kt create mode 100644 app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineTaskRepository.kt create mode 100644 app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineUserRepository.kt delete mode 100644 app/src/main/java/ru/ulstu/is/pmu/database/student/repository/StudentRepository.kt create mode 100644 app/src/main/java/ru/ulstu/is/pmu/database/student/repository/TaskRepository.kt create mode 100644 app/src/main/java/ru/ulstu/is/pmu/database/student/repository/UserRepository.kt delete mode 100644 app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/GroupDropDownViewModel.kt delete mode 100644 app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/StudentEditViewModel.kt rename app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/{StudentEdit.kt => TaskEdit.kt} (52%) create mode 100644 app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/TaskEditViewModel.kt create mode 100644 app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/UserDropDownViewModel.kt delete mode 100644 app/src/main/java/ru/ulstu/is/pmu/ui/student/list/StudentListViewModel.kt create mode 100644 app/src/main/java/ru/ulstu/is/pmu/ui/student/list/TaskList.kt create mode 100644 app/src/main/java/ru/ulstu/is/pmu/ui/student/list/TaskListEndDate.kt rename app/src/main/java/ru/ulstu/is/pmu/ui/student/list/{StudentList.kt => TaskListFavorite.kt} (65%) create mode 100644 app/src/main/java/ru/ulstu/is/pmu/ui/student/list/TaskListViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 96dbbdc..15a3f62 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -66,6 +66,7 @@ dependencies { implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.material3:material3:1.1.2") + implementation("com.google.firebase:protolite-well-known-types:18.0.0") // Room val room_version = "2.5.2" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0c0317d..3f92304 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> - // Groups - val groupDao = database.groupDao() - val group1 = Group(1, "Группа 1") - val group2 = Group(2, "Группа 2") - val group3 = Group(3, "Группа 3") - groupDao.insert(group1) - groupDao.insert(group2) - groupDao.insert(group3) - // Students - val studentDao = database.studentDao() - val student1 = Student("First1", "Last1", group1, "+79998887761", "st1@m.ru") - val student2 = Student("First2", "Last2", group2, "+79998887762", "st2@m.ru") - val student3 = Student("First3", "Last3", group3, "+79998887763", "st3@m.ru") - val student4 = Student("First4", "Last4", group3, "+79998887764", "st4@m.ru") - val student5 = Student("First5", "Last5", group2, "+79998887765", "st5@m.ru") - studentDao.insert(student1) - studentDao.insert(student2) - studentDao.insert(student3) - studentDao.insert(student4) - studentDao.insert(student5) + // Users + val userDao = database.userDao() + val user1 = User(1, "Sergey", "brook.sergey@gmail.com") + userDao.insert(user1) + // Tasks + val taskDao = database.taskDao() + val task1 = Task("First1", "Last1","12.12.2023",false, user1) + val task2 = Task("First2", "Last2","15.12.2023",false, user1) + val task3 = Task("First3", "Last3","10.12.2023",false, user1) + val task4 = Task("First4", "Last4","31.12.2023",false, user1) + val task5 = Task("First5", "Last5","05.12.2023",false, user1) + taskDao.insert(task1) + taskDao.insert(task2) + taskDao.insert(task3) + taskDao.insert(task4) + taskDao.insert(task5) } } diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/dao/GroupDao.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/dao/GroupDao.kt deleted file mode 100644 index dc0bb5c..0000000 --- a/app/src/main/java/ru/ulstu/is/pmu/database/student/dao/GroupDao.kt +++ /dev/null @@ -1,23 +0,0 @@ -package ru.ulstu.`is`.pmu.database.student.dao - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.Query -import androidx.room.Update -import ru.ulstu.`is`.pmu.database.student.model.Group - -@Dao -interface GroupDao { - @Query("select * from groups order by group_name collate nocase asc") - suspend fun getAll(): List - - @Insert - suspend fun insert(group: Group) - - @Update - suspend fun update(group: Group) - - @Delete - suspend fun delete(group: Group) -} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/dao/StudentDao.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/dao/StudentDao.kt deleted file mode 100644 index c9ff7fb..0000000 --- a/app/src/main/java/ru/ulstu/is/pmu/database/student/dao/StudentDao.kt +++ /dev/null @@ -1,27 +0,0 @@ -package ru.ulstu.`is`.pmu.database.student.dao - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.Query -import androidx.room.Update -import kotlinx.coroutines.flow.Flow -import ru.ulstu.`is`.pmu.database.student.model.Student - -@Dao -interface StudentDao { - @Query("select * from students order by last_name collate nocase asc") - fun getAll(): Flow> - - @Query("select * from students where students.uid = :uid") - fun getByUid(uid: Int): Flow - - @Insert - suspend fun insert(student: Student) - - @Update - suspend fun update(student: Student) - - @Delete - suspend fun delete(student: Student) -} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/dao/TaskDao.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/dao/TaskDao.kt new file mode 100644 index 0000000..65e9982 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/student/dao/TaskDao.kt @@ -0,0 +1,30 @@ +package ru.ulstu.`is`.pmu.database.task.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.pmu.database.task.model.Task + +@Dao +interface TaskDao { + @Query("select * from tasks order by name collate nocase asc") + fun getAll(): Flow> + + @Query("select * from tasks where tasks.uid = :uid") + fun getByUid(uid: Int): Flow + + @Insert + suspend fun insert(task: Task) + + @Update + suspend fun update(task: Task) + + @Delete + suspend fun delete(task: Task) + + @Query("UPDATE tasks SET favorite = :favorite WHERE uid = :uid") + suspend fun updateFavorite(uid: Int, favorite: Boolean) +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/dao/UserDao.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/dao/UserDao.kt new file mode 100644 index 0000000..fb02a8d --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/student/dao/UserDao.kt @@ -0,0 +1,23 @@ +package ru.ulstu.`is`.pmu.database.task.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import ru.ulstu.`is`.pmu.database.task.model.User + +@Dao +interface UserDao { + @Query("select * from users order by login collate nocase asc") + suspend fun getAll(): List + + @Insert + suspend fun insert(user: User) + + @Update + suspend fun update(user: User) + + @Delete + suspend fun delete(user: User) +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/model/Student.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/model/Student.kt deleted file mode 100644 index 3df64b5..0000000 --- a/app/src/main/java/ru/ulstu/is/pmu/database/student/model/Student.kt +++ /dev/null @@ -1,78 +0,0 @@ -package ru.ulstu.`is`.pmu.database.student.model - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.Ignore -import androidx.room.PrimaryKey - -@Entity( - tableName = "students", foreignKeys = [ - ForeignKey( - entity = Group::class, - parentColumns = ["uid"], - childColumns = ["group_id"], - onDelete = ForeignKey.RESTRICT, - onUpdate = ForeignKey.RESTRICT - ) - ] -) -data class Student( - @PrimaryKey(autoGenerate = true) - val uid: Int = 0, - @ColumnInfo(name = "first_name") - val firstName: String, - @ColumnInfo(name = "last_name") - val lastName: String, - @ColumnInfo(name = "group_id", index = true) - val groupId: Int, - val phone: String, - val email: String -) { - - @Ignore - constructor( - firstName: String, - lastName: String, - group: Group, - phone: String, - email: String - ) : this(0, firstName, lastName, group.uid, phone, email) - - - companion object { - fun getStudent(index: Int = 0): Student { - return Student( - index, - "first", - "last", - 0, - "8 999 777 65 65", - "email@mail.ru" - ) - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - other as Student - if (uid != other.uid) return false - if (firstName != other.firstName) return false - if (lastName != other.lastName) return false - if (groupId != other.groupId) return false - if (phone != other.phone) return false - if (email != other.email) return false - return true - } - - override fun hashCode(): Int { - var result = uid - result = 31 * result + firstName.hashCode() - result = 31 * result + lastName.hashCode() - result = 31 * result + groupId - result = 31 * result + phone.hashCode() - result = 31 * result + email.hashCode() - return result - } -} diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/model/Task.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/model/Task.kt new file mode 100644 index 0000000..863437c --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/student/model/Task.kt @@ -0,0 +1,80 @@ +package ru.ulstu.`is`.pmu.database.task.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Ignore +import androidx.room.PrimaryKey + +@Entity( + tableName = "tasks", foreignKeys = [ + ForeignKey( + entity = User::class, + parentColumns = ["uid"], + childColumns = ["user_id"], + onDelete = ForeignKey.RESTRICT, + onUpdate = ForeignKey.RESTRICT + ) + ] +) +data class Task( + @PrimaryKey(autoGenerate = true) + val uid: Int = 0, + @ColumnInfo(name = "name") + val name: String, + @ColumnInfo(name = "description") + val description: String, + @ColumnInfo(name = "endDate") + val endDate: String, + @ColumnInfo(name = "favorite") + val favorite: Boolean, + @ColumnInfo(name = "user_id", index = true) + val userId: Int, +) { + + @Ignore + constructor( + name: String, + description: String, + endDate: String, + favorite: Boolean, + user: User, + ) : this(0, name, description, endDate, favorite, user.uid) + + + companion object { + fun getTask(index: Int = 0): Task { + return Task( + index, + "Test1234567", + "Test1235", + "11.12.2023", + true, + 0, + ) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as Task + if (uid != other.uid) return false + if (name != other.name) return false + if (description != other.description) return false + if (endDate != other.endDate) return false + if (favorite != other.favorite) return false + if (userId != other.userId) return false + return true + } + + override fun hashCode(): Int { + var result = uid + result = 31 * result + name.hashCode() + result = 31 * result + description.hashCode() + result = 31 * result + endDate.hashCode() + result = 31 * result + favorite.hashCode() + result = 31 * result + userId + return result + } +} diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/model/Group.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/model/User.kt similarity index 62% rename from app/src/main/java/ru/ulstu/is/pmu/database/student/model/Group.kt rename to app/src/main/java/ru/ulstu/is/pmu/database/student/model/User.kt index ce75fa1..d3232ef 100644 --- a/app/src/main/java/ru/ulstu/is/pmu/database/student/model/Group.kt +++ b/app/src/main/java/ru/ulstu/is/pmu/database/student/model/User.kt @@ -1,20 +1,22 @@ -package ru.ulstu.`is`.pmu.database.student.model +package ru.ulstu.`is`.pmu.database.task.model import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -@Entity(tableName = "groups") -data class Group( +@Entity(tableName = "users") +data class User( @PrimaryKey(autoGenerate = true) val uid: Int = 0, - @ColumnInfo(name = "group_name") - val name: String + @ColumnInfo(name = "user") + val name: String, + @ColumnInfo(name = "login") + val login: String ) { override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false - other as Group + other as User if (uid != other.uid) return false return true } @@ -24,9 +26,10 @@ data class Group( } companion object { - val DEMO_GROUP = Group( + val DEMO_User = User( 0, - "Группа 1" + "Sergey", + "Serega" ) } } diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/GroupRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/GroupRepository.kt deleted file mode 100644 index ff7f9eb..0000000 --- a/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/GroupRepository.kt +++ /dev/null @@ -1,7 +0,0 @@ -package ru.ulstu.`is`.pmu.database.student.repository - -import ru.ulstu.`is`.pmu.database.student.model.Group - -interface GroupRepository { - suspend fun getAllGroups(): List -} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineGroupRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineGroupRepository.kt deleted file mode 100644 index 6566004..0000000 --- a/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineGroupRepository.kt +++ /dev/null @@ -1,8 +0,0 @@ -package ru.ulstu.`is`.pmu.database.student.repository - -import ru.ulstu.`is`.pmu.database.student.dao.GroupDao -import ru.ulstu.`is`.pmu.database.student.model.Group - -class OfflineGroupRepository(private val groupDao: GroupDao) : GroupRepository { - override suspend fun getAllGroups(): List = groupDao.getAll() -} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineStudentRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineStudentRepository.kt deleted file mode 100644 index db28e8e..0000000 --- a/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineStudentRepository.kt +++ /dev/null @@ -1,17 +0,0 @@ -package ru.ulstu.`is`.pmu.database.student.repository - -import kotlinx.coroutines.flow.Flow -import ru.ulstu.`is`.pmu.database.student.dao.StudentDao -import ru.ulstu.`is`.pmu.database.student.model.Student - -class OfflineStudentRepository(private val studentDao: StudentDao) : StudentRepository { - override fun getAllStudents(): Flow> = studentDao.getAll() - - override fun getStudent(uid: Int): Flow = studentDao.getByUid(uid) - - override suspend fun insertStudent(student: Student) = studentDao.insert(student) - - override suspend fun updateStudent(student: Student) = studentDao.update(student) - - override suspend fun deleteStudent(student: Student) = studentDao.delete(student) -} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineTaskRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineTaskRepository.kt new file mode 100644 index 0000000..b8f39ed --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineTaskRepository.kt @@ -0,0 +1,21 @@ +package ru.ulstu.`is`.pmu.database.task.repository + +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.pmu.database.task.dao.TaskDao +import ru.ulstu.`is`.pmu.database.task.model.Task + +class OfflineTaskRepository(private val taskDao: TaskDao) : TaskRepository { + override fun getAllTasks(): Flow> = taskDao.getAll() + + override fun getTask(uid: Int): Flow = taskDao.getByUid(uid) + + override suspend fun insertTask(task: Task) = taskDao.insert(task) + + override suspend fun updateTask(task: Task) = taskDao.update(task) + + override suspend fun deleteTask(task: Task) = taskDao.delete(task) + + override suspend fun setTaskFavorite(uid: Int, favorite: Boolean) { + taskDao.updateFavorite(uid, favorite) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineUserRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineUserRepository.kt new file mode 100644 index 0000000..a932bf6 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineUserRepository.kt @@ -0,0 +1,8 @@ +package ru.ulstu.`is`.pmu.database.task.repository + +import ru.ulstu.`is`.pmu.database.task.dao.UserDao +import ru.ulstu.`is`.pmu.database.task.model.User + +class OfflineUserRepository(private val userDao: UserDao) : UserRepository { + override suspend fun getAllUsers(): List = userDao.getAll() +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/StudentRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/StudentRepository.kt deleted file mode 100644 index c1be644..0000000 --- a/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/StudentRepository.kt +++ /dev/null @@ -1,12 +0,0 @@ -package ru.ulstu.`is`.pmu.database.student.repository - -import kotlinx.coroutines.flow.Flow -import ru.ulstu.`is`.pmu.database.student.model.Student - -interface StudentRepository { - fun getAllStudents(): Flow> - fun getStudent(uid: Int): Flow - suspend fun insertStudent(student: Student) - suspend fun updateStudent(student: Student) - suspend fun deleteStudent(student: Student) -} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/TaskRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/TaskRepository.kt new file mode 100644 index 0000000..f92bf69 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/TaskRepository.kt @@ -0,0 +1,13 @@ +package ru.ulstu.`is`.pmu.database.task.repository + +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.pmu.database.task.model.Task + +interface TaskRepository { + fun getAllTasks(): Flow> + fun getTask(uid: Int): Flow + suspend fun insertTask(task: Task) + suspend fun updateTask(task: Task) + suspend fun deleteTask(task: Task) + suspend fun setTaskFavorite(uid: Int, favorite: Boolean = true) +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/UserRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/UserRepository.kt new file mode 100644 index 0000000..e34d4ff --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/UserRepository.kt @@ -0,0 +1,8 @@ +package ru.ulstu.`is`.pmu.database.task.repository + +import ru.ulstu.`is`.pmu.database.task.dao.UserDao +import ru.ulstu.`is`.pmu.database.task.model.User + +interface UserRepository { + suspend fun getAllUsers(): List +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/AppViewModelProvider.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/AppViewModelProvider.kt index a893e7a..a964855 100644 --- a/app/src/main/java/ru/ulstu/is/pmu/ui/AppViewModelProvider.kt +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/AppViewModelProvider.kt @@ -5,27 +5,27 @@ import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory -import ru.ulstu.`is`.pmu.StudentApplication -import ru.ulstu.`is`.pmu.ui.student.edit.GroupDropDownViewModel -import ru.ulstu.`is`.pmu.ui.student.edit.StudentEditViewModel -import ru.ulstu.`is`.pmu.ui.student.list.StudentListViewModel +import ru.ulstu.`is`.pmu.TaskApplication +import ru.ulstu.`is`.pmu.ui.task.edit.UserDropDownViewModel +import ru.ulstu.`is`.pmu.ui.task.edit.TaskEditViewModel +import ru.ulstu.`is`.pmu.ui.task.list.TaskListViewModel object AppViewModelProvider { val Factory = viewModelFactory { initializer { - StudentListViewModel(studentApplication().container.studentRepository) + TaskListViewModel(taskApplication().container.taskRepository) } initializer { - StudentEditViewModel( + TaskEditViewModel( this.createSavedStateHandle(), - studentApplication().container.studentRepository + taskApplication().container.taskRepository ) } initializer { - GroupDropDownViewModel(studentApplication().container.groupRepository) + UserDropDownViewModel(taskApplication().container.userRepository) } } } -fun CreationExtras.studentApplication(): StudentApplication = - (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as StudentApplication) \ No newline at end of file +fun CreationExtras.taskApplication(): TaskApplication = + (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as TaskApplication) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/about/About.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/about/About.kt index f06726e..f26722f 100644 --- a/app/src/main/java/ru/ulstu/is/pmu/ui/about/About.kt +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/about/About.kt @@ -30,7 +30,7 @@ fun About() { val urlOnClick = { val openURL = Intent(Intent.ACTION_VIEW) - openURL.data = Uri.parse("https://ulstu.ru/") + openURL.data = Uri.parse("https://vk.com/stranni20k") localContext.startActivity(openURL) } @@ -47,7 +47,7 @@ fun About() { modifier = Modifier.fillMaxWidth(), onClick = urlOnClick ) { - Text(stringResource(id = R.string.about_title)) + Text(stringResource(id = R.string.creator)) } } } diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/navigation/MainNavbar.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/navigation/MainNavbar.kt index 11e7b51..50fdfe8 100644 --- a/app/src/main/java/ru/ulstu/is/pmu/ui/navigation/MainNavbar.kt +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/navigation/MainNavbar.kt @@ -30,8 +30,10 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import ru.ulstu.`is`.pmu.R import ru.ulstu.`is`.pmu.ui.about.About -import ru.ulstu.`is`.pmu.ui.student.edit.StudentEdit -import ru.ulstu.`is`.pmu.ui.student.list.StudentList +import ru.ulstu.`is`.pmu.ui.task.edit.TaskEdit +import ru.ulstu.`is`.pmu.ui.task.list.TaskList +import ru.ulstu.`is`.pmu.ui.task.list.TaskListEndDate +import ru.ulstu.`is`.pmu.ui.task.list.TaskListFavorite @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -98,16 +100,18 @@ fun Navhost( ) { NavHost( navController, - startDestination = Screen.StudentList.route, + startDestination = Screen.TaskList.route, modifier.padding(innerPadding) ) { - composable(Screen.StudentList.route) { StudentList(navController) } + composable(Screen.TaskList.route) { TaskList(navController) } + composable(Screen.TaskListEndDate.route) { TaskListEndDate(navController) } + composable(Screen.TaskListFavorite.route) { TaskListFavorite(navController) } composable(Screen.About.route) { About() } composable( - Screen.StudentEdit.route, + Screen.TaskEdit.route, arguments = listOf(navArgument("id") { type = NavType.IntType }) ) { - StudentEdit(navController) + TaskEdit(navController) } } } diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/navigation/Screen.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/navigation/Screen.kt index 7e8daf9..178a352 100644 --- a/app/src/main/java/ru/ulstu/is/pmu/ui/navigation/Screen.kt +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/navigation/Screen.kt @@ -2,9 +2,11 @@ package ru.ulstu.`is`.pmu.ui.navigation import androidx.annotation.StringRes import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DateRange import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.List +import androidx.compose.material.icons.filled.Person import androidx.compose.ui.graphics.vector.ImageVector import ru.ulstu.`is`.pmu.R @@ -14,19 +16,27 @@ enum class Screen( val icon: ImageVector = Icons.Filled.Favorite, val showInBottomBar: Boolean = true ) { - StudentList( - "student-list", R.string.student_main_title, Icons.Filled.List + TaskList( + "task-list", R.string.task_main_title, Icons.Filled.List + ), + TaskListEndDate( + "task-list-end-date", R.string.task_date_title, Icons.Filled.DateRange + ), + TaskListFavorite( + "task-list-favorite", R.string.task_favorite, Icons.Filled.Favorite ), About( "about", R.string.about_title, Icons.Filled.Info ), - StudentEdit( - "student-edit/{id}", R.string.student_view_title, showInBottomBar = false + TaskEdit( + "task-edit/{id}", R.string.task_view_title, showInBottomBar = false ); companion object { val bottomBarItems = listOf( - StudentList, + TaskList, + TaskListEndDate, + TaskListFavorite, About, ) diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/GroupDropDownViewModel.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/GroupDropDownViewModel.kt deleted file mode 100644 index dde27cc..0000000 --- a/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/GroupDropDownViewModel.kt +++ /dev/null @@ -1,46 +0,0 @@ -package ru.ulstu.`is`.pmu.ui.student.edit - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.launch -import ru.ulstu.`is`.pmu.database.student.model.Group -import ru.ulstu.`is`.pmu.database.student.repository.GroupRepository - -class GroupDropDownViewModel( - private val groupRepository: GroupRepository -) : ViewModel() { - var groupsListUiState by mutableStateOf(GroupsListUiState()) - private set - - var groupUiState by mutableStateOf(GroupUiState()) - private set - - init { - viewModelScope.launch { - groupsListUiState = GroupsListUiState(groupRepository.getAllGroups()) - } - } - - fun setCurrentGroup(groupId: Int) { - val group: Group? = - groupsListUiState.groupList.firstOrNull { group -> group.uid == groupId } - group?.let { updateUiState(it) } - } - - fun updateUiState(group: Group) { - groupUiState = GroupUiState( - group = group - ) - } -} - -data class GroupsListUiState(val groupList: List = listOf()) - -data class GroupUiState( - val group: Group? = null -) - -fun Group.toUiState() = GroupUiState(group = Group(uid = uid, name = name)) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/StudentEditViewModel.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/StudentEditViewModel.kt deleted file mode 100644 index e9a1f19..0000000 --- a/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/StudentEditViewModel.kt +++ /dev/null @@ -1,97 +0,0 @@ -package ru.ulstu.`is`.pmu.ui.student.edit - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import ru.ulstu.`is`.pmu.database.student.model.Student -import ru.ulstu.`is`.pmu.database.student.repository.StudentRepository - -class StudentEditViewModel( - savedStateHandle: SavedStateHandle, - private val studentRepository: StudentRepository -) : ViewModel() { - - var studentUiState by mutableStateOf(StudentUiState()) - private set - - private val studentUid: Int = checkNotNull(savedStateHandle["id"]) - - init { - viewModelScope.launch { - if (studentUid > 0) { - studentUiState = studentRepository.getStudent(studentUid) - .filterNotNull() - .first() - .toUiState(true) - } - } - } - - fun updateUiState(studentDetails: StudentDetails) { - studentUiState = StudentUiState( - studentDetails = studentDetails, - isEntryValid = validateInput(studentDetails) - ) - } - - suspend fun saveStudent() { - if (validateInput()) { - if (studentUid > 0) { - studentRepository.updateStudent(studentUiState.studentDetails.toStudent(studentUid)) - } else { - studentRepository.insertStudent(studentUiState.studentDetails.toStudent()) - } - } - } - - private fun validateInput(uiState: StudentDetails = studentUiState.studentDetails): Boolean { - return with(uiState) { - firstName.isNotBlank() - && lastName.isNotBlank() - && phone.isNotBlank() - && email.isNotBlank() - && groupId > 0 - } - } -} - -data class StudentUiState( - val studentDetails: StudentDetails = StudentDetails(), - val isEntryValid: Boolean = false -) - -data class StudentDetails( - val firstName: String = "", - val lastName: String = "", - val phone: String = "", - val email: String = "", - val groupId: Int = 0 -) - -fun StudentDetails.toStudent(uid: Int = 0): Student = Student( - uid = uid, - firstName = firstName, - lastName = lastName, - phone = phone, - email = email, - groupId = groupId -) - -fun Student.toDetails(): StudentDetails = StudentDetails( - firstName = firstName, - lastName = lastName, - phone = phone, - email = email, - groupId = groupId -) - -fun Student.toUiState(isEntryValid: Boolean = false): StudentUiState = StudentUiState( - studentDetails = this.toDetails(), - isEntryValid = isEntryValid -) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/StudentEdit.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/TaskEdit.kt similarity index 52% rename from app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/StudentEdit.kt rename to app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/TaskEdit.kt index 4945a17..083cafb 100644 --- a/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/StudentEdit.kt +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/TaskEdit.kt @@ -1,11 +1,13 @@ -package ru.ulstu.`is`.pmu.ui.student.edit +package ru.ulstu.`is`.pmu.ui.task.edit import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem @@ -31,42 +33,43 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController +import java.util.Date import kotlinx.coroutines.launch import ru.ulstu.`is`.pmu.R -import ru.ulstu.`is`.pmu.database.student.model.Group -import ru.ulstu.`is`.pmu.database.student.model.Student +import ru.ulstu.`is`.pmu.database.task.model.User +import ru.ulstu.`is`.pmu.database.task.model.Task import ru.ulstu.`is`.pmu.ui.AppViewModelProvider import ru.ulstu.`is`.pmu.ui.theme.PmudemoTheme @Composable -fun StudentEdit( +fun TaskEdit( navController: NavController, - viewModel: StudentEditViewModel = viewModel(factory = AppViewModelProvider.Factory), - groupViewModel: GroupDropDownViewModel = viewModel(factory = AppViewModelProvider.Factory) + viewModel: TaskEditViewModel = viewModel(factory = AppViewModelProvider.Factory), + userViewModel: UserDropDownViewModel = viewModel(factory = AppViewModelProvider.Factory) ) { val coroutineScope = rememberCoroutineScope() - groupViewModel.setCurrentGroup(viewModel.studentUiState.studentDetails.groupId) - StudentEdit( - studentUiState = viewModel.studentUiState, - groupUiState = groupViewModel.groupUiState, - groupsListUiState = groupViewModel.groupsListUiState, + userViewModel.setCurrentUser(viewModel.taskUiState.taskDetails.userId) + TaskEdit( + taskUiState = viewModel.taskUiState, + userUiState = userViewModel.userUiState, + usersListUiState = userViewModel.usersListUiState, onClick = { coroutineScope.launch { - viewModel.saveStudent() + viewModel.saveTask() navController.popBackStack() } }, onUpdate = viewModel::updateUiState, - onGroupUpdate = groupViewModel::updateUiState + onUserUpdate = userViewModel::updateUiState ) } @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun GroupDropDown( - groupUiState: GroupUiState, - groupsListUiState: GroupsListUiState, - onGroupUpdate: (Group) -> Unit +private fun UserDropDown( + userUiState: UserUiState, + usersListUiState: UsersListUiState, + onUserUpdate: (User) -> Unit ) { var expanded: Boolean by remember { mutableStateOf(false) } ExposedDropdownMenuBox( @@ -78,8 +81,8 @@ private fun GroupDropDown( } ) { TextField( - value = groupUiState.group?.name - ?: stringResource(id = R.string.student_group_not_select), + value = userUiState.user?.name + ?: stringResource(id = R.string.task_user_not_select), onValueChange = {}, readOnly = true, trailingIcon = { @@ -96,13 +99,13 @@ private fun GroupDropDown( .background(Color.White) .exposedDropdownSize() ) { - groupsListUiState.groupList.forEach { group -> + usersListUiState.userList.forEach { user -> DropdownMenuItem( text = { - Text(text = group.name) + Text(text = user.name) }, onClick = { - onGroupUpdate(group) + onUserUpdate(user) expanded = false } ) @@ -113,14 +116,15 @@ private fun GroupDropDown( @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun StudentEdit( - studentUiState: StudentUiState, - groupUiState: GroupUiState, - groupsListUiState: GroupsListUiState, +private fun TaskEdit( + taskUiState: TaskUiState, + userUiState: UserUiState, + usersListUiState: UsersListUiState, onClick: () -> Unit, - onUpdate: (StudentDetails) -> Unit, - onGroupUpdate: (Group) -> Unit + onUpdate: (TaskDetails) -> Unit, + onUserUpdate: (User) -> Unit ) { + var showInvalidDateDialog by remember { mutableStateOf(false) } Column( Modifier .fillMaxWidth() @@ -128,68 +132,84 @@ private fun StudentEdit( ) { OutlinedTextField( modifier = Modifier.fillMaxWidth(), - value = studentUiState.studentDetails.firstName, - onValueChange = { onUpdate(studentUiState.studentDetails.copy(firstName = it)) }, - label = { Text(stringResource(id = R.string.student_firstname)) }, + value = taskUiState.taskDetails.name, + onValueChange = { onUpdate(taskUiState.taskDetails.copy(name = it)) }, + label = { Text(stringResource(id = R.string.task_firstname)) }, singleLine = true ) OutlinedTextField( modifier = Modifier.fillMaxWidth(), - value = studentUiState.studentDetails.lastName, - onValueChange = { onUpdate(studentUiState.studentDetails.copy(lastName = it)) }, - label = { Text(stringResource(id = R.string.student_lastname)) }, + value = taskUiState.taskDetails.description, + onValueChange = { onUpdate(taskUiState.taskDetails.copy(description = it)) }, + label = { Text(stringResource(id = R.string.task_lastname)) }, singleLine = true ) - GroupDropDown( - groupUiState = groupUiState, - groupsListUiState = groupsListUiState, - onGroupUpdate = { - onUpdate(studentUiState.studentDetails.copy(groupId = it.uid)) - onGroupUpdate(it) - } - ) OutlinedTextField( modifier = Modifier.fillMaxWidth(), - value = studentUiState.studentDetails.phone, - onValueChange = { onUpdate(studentUiState.studentDetails.copy(phone = it)) }, - label = { Text(stringResource(id = R.string.student_phone)) }, + value = taskUiState.taskDetails.endDate, + onValueChange = { newDate -> onUpdate(taskUiState.taskDetails.copy(endDate = newDate)) }, + label = { Text(stringResource(id = R.string.task_endDate)) }, singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone) - ) - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = studentUiState.studentDetails.email, - onValueChange = { onUpdate(studentUiState.studentDetails.copy(email = it)) }, - label = { Text(stringResource(id = R.string.student_email)) }, - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email) + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) ) +// UserDropDown( +// userUiState = userUiState, +// usersListUiState = usersListUiState, +// onUserUpdate = { +// onUpdate(taskUiState.taskDetails.copy(userId = it.uid)) +// onUserUpdate(it) +// } +// ) Button( - onClick = onClick, - enabled = studentUiState.isEntryValid, + onClick = { + if (!isValidDate(taskUiState.taskDetails.endDate)) { + showInvalidDateDialog = true + } else { + onClick() + } + }, + enabled = taskUiState.isEntryValid, shape = MaterialTheme.shapes.small, modifier = Modifier.fillMaxWidth() ) { - Text(text = stringResource(R.string.student_save_button)) + Text(text = stringResource(R.string.task_save_button)) } } + + if (showInvalidDateDialog) { + AlertDialog( + onDismissRequest = { showInvalidDateDialog = false }, + title = { Text("Неверный формат даты") }, + text = { Text("Введите дату по шаблону: 01.12.2023") }, + confirmButton = { + Button(onClick = { showInvalidDateDialog = false }) { + Text("ОК") + } + } + ) + } +} + +fun isValidDate(date: String): Boolean { + val regex = Regex("""^\d{2}\.\d{2}\.\d{4}$""") + return regex.matches(date) } @Preview(name = "Light Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "Dark Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -fun StudentEditPreview() { +fun TaskEditPreview() { PmudemoTheme { Surface( color = MaterialTheme.colorScheme.background ) { - StudentEdit( - studentUiState = Student.getStudent().toUiState(true), - groupUiState = Group.DEMO_GROUP.toUiState(), - groupsListUiState = GroupsListUiState(listOf()), + TaskEdit( + taskUiState = Task.getTask().toUiState(true), + userUiState = User.DEMO_User.toUiState(), + usersListUiState = UsersListUiState(listOf()), onClick = {}, onUpdate = {}, - onGroupUpdate = {} + onUserUpdate = {} ) } } diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/TaskEditViewModel.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/TaskEditViewModel.kt new file mode 100644 index 0000000..58896bd --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/TaskEditViewModel.kt @@ -0,0 +1,96 @@ +package ru.ulstu.`is`.pmu.ui.task.edit + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import ru.ulstu.`is`.pmu.database.task.model.Task +import ru.ulstu.`is`.pmu.database.task.repository.TaskRepository + +class TaskEditViewModel( + savedStateHandle: SavedStateHandle, + private val taskRepository: TaskRepository +) : ViewModel() { + + var taskUiState by mutableStateOf(TaskUiState()) + private set + + private val taskUid: Int = checkNotNull(savedStateHandle["id"]) + + init { + viewModelScope.launch { + if (taskUid > 0) { + taskUiState = taskRepository.getTask(taskUid) + .filterNotNull() + .first() + .toUiState(true) + } + } + } + + fun updateUiState(taskDetails: TaskDetails) { + taskUiState = TaskUiState( + taskDetails = taskDetails, + isEntryValid = validateInput(taskDetails) + ) + } + + suspend fun saveTask() { + if (validateInput()) { + if (taskUid > 0) { + taskRepository.updateTask(taskUiState.taskDetails.toTask(taskUid)) + } else { + taskRepository.insertTask(taskUiState.taskDetails.toTask()) + } + } + } + + private fun validateInput(uiState: TaskDetails = taskUiState.taskDetails): Boolean { + return with(uiState) { + name.isNotBlank() + && description.isNotBlank() + && endDate.isNotBlank() + && userId > 0 + } + } +} + +data class TaskUiState( + val taskDetails: TaskDetails = TaskDetails(), + val isEntryValid: Boolean = false +) + +data class TaskDetails( + val name: String = "", + val description: String = "", + val endDate: String = "", + val favorite: Boolean = false, + val userId: Int = 1 +) + +fun TaskDetails.toTask(uid: Int = 0): Task = Task( + uid = uid, + name = name, + description = description, + endDate = endDate, + favorite = favorite, + userId = 1 +) + +fun Task.toDetails(): TaskDetails = TaskDetails( + name = name, + description = description, + endDate = endDate, + favorite = favorite, + userId = 1 +) + +fun Task.toUiState(isEntryValid: Boolean = false): TaskUiState = TaskUiState( + taskDetails = this.toDetails(), + isEntryValid = isEntryValid +) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/UserDropDownViewModel.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/UserDropDownViewModel.kt new file mode 100644 index 0000000..0cccaa9 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/UserDropDownViewModel.kt @@ -0,0 +1,46 @@ +package ru.ulstu.`is`.pmu.ui.task.edit + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import ru.ulstu.`is`.pmu.database.task.model.User +import ru.ulstu.`is`.pmu.database.task.repository.UserRepository + +class UserDropDownViewModel( + private val userRepository: UserRepository +) : ViewModel() { + var usersListUiState by mutableStateOf(UsersListUiState()) + private set + + var userUiState by mutableStateOf(UserUiState()) + private set + + init { + viewModelScope.launch { + usersListUiState = UsersListUiState(userRepository.getAllUsers()) + } + } + + fun setCurrentUser(userId: Int) { + val user: User? = + usersListUiState.userList.firstOrNull { user -> user.uid == userId } + user?.let { updateUiState(it) } + } + + fun updateUiState(user: User) { + userUiState = UserUiState( + user = user + ) + } +} + +data class UsersListUiState(val userList: List = listOf()) + +data class UserUiState( + val user: User? = null +) + +fun User.toUiState() = UserUiState(user = User(uid = uid, name = name, login = login)) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/StudentListViewModel.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/StudentListViewModel.kt deleted file mode 100644 index e4c8f4d..0000000 --- a/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/StudentListViewModel.kt +++ /dev/null @@ -1,29 +0,0 @@ -package ru.ulstu.`is`.pmu.ui.student.list - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import ru.ulstu.`is`.pmu.database.AppDataContainer -import ru.ulstu.`is`.pmu.database.student.model.Student -import ru.ulstu.`is`.pmu.database.student.repository.StudentRepository - -class StudentListViewModel( - private val studentRepository: StudentRepository -) : ViewModel() { - val studentListUiState: StateFlow = studentRepository.getAllStudents().map { - StudentListUiState(it) - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(stopTimeoutMillis = AppDataContainer.TIMEOUT), - initialValue = StudentListUiState() - ) - - suspend fun deleteStudent(student: Student) { - studentRepository.deleteStudent(student) - } -} - -data class StudentListUiState(val studentList: List = listOf()) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/TaskList.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/TaskList.kt new file mode 100644 index 0000000..b0cbec9 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/TaskList.kt @@ -0,0 +1,212 @@ +package ru.ulstu.`is`.pmu.ui.task.list + +import android.content.res.Configuration +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Star +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DismissDirection +import androidx.compose.material3.DismissState +import androidx.compose.material3.DismissValue +import androidx.compose.material3.DismissValue.DismissedToStart +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.SwipeToDismiss +import androidx.compose.material3.Text +import androidx.compose.material3.rememberDismissState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import kotlinx.coroutines.launch +import ru.ulstu.`is`.pmu.R +import ru.ulstu.`is`.pmu.database.task.model.Task +import ru.ulstu.`is`.pmu.ui.AppViewModelProvider +import ru.ulstu.`is`.pmu.ui.navigation.Screen +import ru.ulstu.`is`.pmu.ui.theme.PmudemoTheme + +@Composable +fun TaskList( + navController: NavController, + viewModel: TaskListViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + val coroutineScope = rememberCoroutineScope() + val taskListUiState by viewModel.taskListUiState.collectAsState() + Scaffold( + topBar = {}, + floatingActionButton = { + FloatingActionButton( + onClick = { + val route = Screen.TaskEdit.route.replace("{id}", "0") + navController.navigate(route) + }, + ) { + Icon(Icons.Filled.Add, contentDescription = "Добавить") + } + } + ) { innerPadding -> + TaskListContent( // Изменил имя, чтобы избежать путаницы с функцией TaskList + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + taskList = taskListUiState.taskList, + onClick = { uid: Int -> + val route = Screen.TaskEdit.route.replace("{id}", uid.toString()) + navController.navigate(route) + }, + onSwipeLeft = { task: Task -> // Обработка свайпа влево (удаление) + coroutineScope.launch { + viewModel.deleteTask(task) + } + }, + onSwipeRight = { task: Task -> // Обработка свайпа вправо (добавить в избранное) + coroutineScope.launch { + viewModel.favoriteTask(task) + } + } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SwipeToAction( + dismissState: DismissState, + task: Task, + onClick: (uid: Int) -> Unit, + onSwipeRight: (task: Task) -> Unit, // Для добавления в избранное + onSwipeLeft: (task: Task) -> Unit // Для удаления +) { + SwipeToDismiss( + state = dismissState, + directions = setOf( + DismissDirection.EndToStart, // Для удаления + DismissDirection.StartToEnd // Для добавления в избранное + ), + background = { + val backgroundColor by animateColorAsState( + when (dismissState.targetValue) { + DismissedToStart -> Color.Red.copy(alpha = 0.8f) + DismissValue.DismissedToEnd -> Color.Green.copy(alpha = 0.8f) // Цвет фона для избранного + else -> Color.White + }, label = "" + ) + val iconScale by animateFloatAsState( + targetValue = if (dismissState.targetValue == DismissedToStart || dismissState.targetValue == DismissValue.DismissedToEnd) 1.3f else 0.5f, + label = "" + ) + Box( + Modifier + .fillMaxSize() + .background(color = backgroundColor) + .padding(end = 16.dp, start = 16.dp), + contentAlignment = if (dismissState.targetValue == DismissedToStart) Alignment.CenterEnd else Alignment.CenterStart + ) { + Icon( + modifier = Modifier.scale(iconScale), + imageVector = if (dismissState.targetValue == DismissedToStart) Icons.Outlined.Delete else Icons.Default.Star, // Иконка для избранного + contentDescription = if (dismissState.targetValue == DismissedToStart) "Delete" else "Favorite", + tint = Color.White + ) + } + }, + dismissContent = { + TaskListItem(task = task, + modifier = Modifier + .padding(vertical = 7.dp) + .clickable { onClick(task.uid) }) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TaskListContent( + modifier: Modifier = Modifier, + taskList: List, + onClick: (uid: Int) -> Unit, + onSwipeLeft: (task: Task) -> Unit, + onSwipeRight: (task: Task) -> Unit +) { + Column( + modifier = modifier + ) { + if (taskList.isEmpty()) { + // Код для отображения пустого списка + } else { + LazyColumn(modifier = Modifier.padding(all = 10.dp)) { + items(items = taskList, key = { it.uid }) { task -> + val dismissState = rememberDismissState() + + if (dismissState.isDismissed(DismissDirection.EndToStart)) { + LaunchedEffect(task) { + onSwipeLeft(task) + dismissState.reset() // Сбросить состояние после обработки свайпа + } + } + + if (dismissState.isDismissed(DismissDirection.StartToEnd)) { + LaunchedEffect(task) { + onSwipeRight(task) + dismissState.reset() // Сбросить состояние после обработки свайпа + } + } + + SwipeToAction( + dismissState = dismissState, + task = task, + onClick = onClick, + onSwipeRight = onSwipeRight, + onSwipeLeft = onSwipeLeft + ) + } + } + } + } +} + +@Composable +private fun TaskListItem( + task: Task, modifier: Modifier = Modifier +) { + Card( + modifier = modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = modifier.padding(all = 10.dp) + ) { + Text( + text = String.format("%s%n%s", task.name, task.description) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/TaskListEndDate.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/TaskListEndDate.kt new file mode 100644 index 0000000..2ddf8ea --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/TaskListEndDate.kt @@ -0,0 +1,136 @@ +package ru.ulstu.`is`.pmu.ui.task.list + +import android.content.res.Configuration +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DismissDirection +import androidx.compose.material3.DismissState +import androidx.compose.material3.DismissValue.DismissedToStart +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.SwipeToDismiss +import androidx.compose.material3.Text +import androidx.compose.material3.rememberDismissState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import kotlinx.coroutines.launch +import ru.ulstu.`is`.pmu.R +import ru.ulstu.`is`.pmu.database.task.model.Task +import ru.ulstu.`is`.pmu.ui.AppViewModelProvider +import ru.ulstu.`is`.pmu.ui.navigation.Screen +import ru.ulstu.`is`.pmu.ui.theme.PmudemoTheme +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale + +@Composable +fun TaskListEndDate( + navController: NavController, + viewModel: TaskListViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + val taskListEndDateUiState by viewModel.taskListEndDateUiState.collectAsState() + Scaffold( + topBar = {}, + ) { innerPadding -> + TaskListEndDate( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + taskList = taskListEndDateUiState.taskList, + onClick = { uid: Int -> + val route = Screen.TaskEdit.route.replace("{id}", uid.toString()) + navController.navigate(route) + } + ) + } +} + +fun parseDate(dateString: String): Calendar? { + return try { + val formatter = SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()) + val calendar = Calendar.getInstance() + calendar.time = formatter.parse(dateString) + calendar + } catch (e: Exception) { + null + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TaskListEndDate( + modifier: Modifier = Modifier, + taskList: List, + onClick: (uid: Int) -> Unit +) { + Column( + modifier = modifier + ) { + if (taskList.isEmpty()) { + Text( + text = stringResource(R.string.task_empty_description), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge + ) + } else { + LazyColumn(modifier = Modifier.padding(all = 10.dp)) { + items(items = taskList.reversed(), key = { it.uid }) { task -> + val dismissState: DismissState = rememberDismissState( + positionalThreshold = { 200.dp.toPx() } + ) + + TaskListItem(task = task, modifier = Modifier + .padding(vertical = 7.dp)) + } + } + } + } +} + +@Composable +private fun TaskListItem( + task: Task, modifier: Modifier = Modifier +) { + Card( + modifier = modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = modifier.padding(all = 10.dp) + ) { + Text( + text = String.format("%s%n%s%n%s", task.name, task.description, task.endDate) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/StudentList.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/TaskListFavorite.kt similarity index 65% rename from app/src/main/java/ru/ulstu/is/pmu/ui/student/list/StudentList.kt rename to app/src/main/java/ru/ulstu/is/pmu/ui/student/list/TaskListFavorite.kt index 971f40a..2c06e1b 100644 --- a/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/StudentList.kt +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/TaskListFavorite.kt @@ -1,4 +1,4 @@ -package ru.ulstu.`is`.pmu.ui.student.list +package ru.ulstu.`is`.pmu.ui.task.list import android.content.res.Configuration import androidx.compose.animation.animateColorAsState @@ -45,43 +45,33 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import kotlinx.coroutines.launch import ru.ulstu.`is`.pmu.R -import ru.ulstu.`is`.pmu.database.student.model.Student +import ru.ulstu.`is`.pmu.database.task.model.Task import ru.ulstu.`is`.pmu.ui.AppViewModelProvider import ru.ulstu.`is`.pmu.ui.navigation.Screen import ru.ulstu.`is`.pmu.ui.theme.PmudemoTheme @Composable -fun StudentList( +fun TaskListFavorite( navController: NavController, - viewModel: StudentListViewModel = viewModel(factory = AppViewModelProvider.Factory) + viewModel: TaskListViewModel = viewModel(factory = AppViewModelProvider.Factory) ) { val coroutineScope = rememberCoroutineScope() - val studentListUiState by viewModel.studentListUiState.collectAsState() + val taskListFavoriteUiState by viewModel.taskListFavoriteUiState.collectAsState() Scaffold( - topBar = {}, - floatingActionButton = { - FloatingActionButton( - onClick = { - val route = Screen.StudentEdit.route.replace("{id}", 0.toString()) - navController.navigate(route) - }, - ) { - Icon(Icons.Filled.Add, "Добавить") - } - } + topBar = {} ) { innerPadding -> - StudentList( + TaskListFavorite( modifier = Modifier .padding(innerPadding) .fillMaxSize(), - studentList = studentListUiState.studentList, + taskList = taskListFavoriteUiState.taskList, onClick = { uid: Int -> - val route = Screen.StudentEdit.route.replace("{id}", uid.toString()) + val route = Screen.TaskEdit.route.replace("{id}", uid.toString()) navController.navigate(route) }, - onSwipe = { student: Student -> + onSwipe = { task: Task -> coroutineScope.launch { - viewModel.deleteStudent(student) + viewModel.deletefavoriteTask(task) } } ) @@ -92,8 +82,7 @@ fun StudentList( @Composable private fun SwipeToDelete( dismissState: DismissState, - student: Student, - onClick: (uid: Int) -> Unit + task: Task ) { SwipeToDismiss( state = dismissState, @@ -127,46 +116,44 @@ private fun SwipeToDelete( } }, dismissContent = { - StudentListItem(student = student, + TaskListItem(task = task, modifier = Modifier - .padding(vertical = 7.dp) - .clickable { onClick(student.uid) }) + .padding(vertical = 7.dp)) } ) } @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun StudentList( +private fun TaskListFavorite( modifier: Modifier = Modifier, - studentList: List, + taskList: List, onClick: (uid: Int) -> Unit, - onSwipe: (student: Student) -> Unit + onSwipe: (task: Task) -> Unit ) { Column( modifier = modifier ) { - if (studentList.isEmpty()) { + if (taskList.isEmpty()) { Text( - text = stringResource(R.string.student_empty_description), + text = stringResource(R.string.task_empty_description), textAlign = TextAlign.Center, style = MaterialTheme.typography.titleLarge ) } else { LazyColumn(modifier = Modifier.padding(all = 10.dp)) { - items(items = studentList, key = { it.uid }) { student -> + items(items = taskList, key = { it.uid }) { task -> val dismissState: DismissState = rememberDismissState( positionalThreshold = { 200.dp.toPx() } ) if (dismissState.isDismissed(direction = DismissDirection.EndToStart)) { - onSwipe(student) + onSwipe(task) } SwipeToDelete( dismissState = dismissState, - student = student, - onClick = onClick + task = task ) } } @@ -175,8 +162,8 @@ private fun StudentList( } @Composable -private fun StudentListItem( - student: Student, modifier: Modifier = Modifier +private fun TaskListItem( + task: Task, modifier: Modifier = Modifier ) { Card( modifier = modifier.fillMaxWidth(), @@ -186,41 +173,7 @@ private fun StudentListItem( modifier = modifier.padding(all = 10.dp) ) { Text( - text = String.format("%s %s", student.firstName, student.lastName) - ) - } - } -} - -@Preview(name = "Light Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO) -@Preview(name = "Dark Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -fun StudentListPreview() { - PmudemoTheme { - Surface( - color = MaterialTheme.colorScheme.background - ) { - StudentList( - studentList = (1..20).map { i -> Student.getStudent(i) }, - onClick = {}, - onSwipe = {} - ) - } - } -} - -@Preview(name = "Light Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO) -@Preview(name = "Dark Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -fun StudentEmptyListPreview() { - PmudemoTheme { - Surface( - color = MaterialTheme.colorScheme.background - ) { - StudentList( - studentList = listOf(), - onClick = {}, - onSwipe = {} + text = String.format("%s%n%s", task.name, task.description) ) } } diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/TaskListViewModel.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/TaskListViewModel.kt new file mode 100644 index 0000000..1511d52 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/TaskListViewModel.kt @@ -0,0 +1,60 @@ +package ru.ulstu.`is`.pmu.ui.task.list + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import ru.ulstu.`is`.pmu.database.AppDataContainer +import ru.ulstu.`is`.pmu.database.task.model.Task +import ru.ulstu.`is`.pmu.database.task.repository.TaskRepository + +class TaskListViewModel( + private val taskRepository: TaskRepository +) : ViewModel() { + + val taskListFavoriteUiState: StateFlow = taskRepository.getAllTasks() + .map { tasks -> + val filteredTasks = tasks.filter { task -> + task.favorite // Оставить только задачи, у которых favorite = true + } + val sortedTasks = filteredTasks.sortedByDescending { task -> + parseDate(task.endDate)?.timeInMillis ?: Long.MIN_VALUE + } + TaskListUiState(sortedTasks) + } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(AppDataContainer.TIMEOUT), TaskListUiState()) + + // Для TaskListEndDate (Сортировка по endDate) + val taskListEndDateUiState: StateFlow = taskRepository.getAllTasks() + .map { tasks -> + val sortedTasks = tasks.sortedByDescending { task -> + parseDate(task.endDate)?.timeInMillis ?: Long.MIN_VALUE + } + TaskListUiState(sortedTasks) + } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(AppDataContainer.TIMEOUT), TaskListUiState()) + + // Для TaskList (Сортировка по uid) + val taskListUiState: StateFlow = taskRepository.getAllTasks() + .map { tasks -> + val sortedTasks = tasks.sortedBy { task -> + task.uid + } + TaskListUiState(sortedTasks) + } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(AppDataContainer.TIMEOUT), TaskListUiState()) + + suspend fun deleteTask(task: Task) { + taskRepository.deleteTask(task) + } + suspend fun favoriteTask(task: Task) { + taskRepository.setTaskFavorite(task.uid, true) + } + suspend fun deletefavoriteTask(task: Task) { + taskRepository.setTaskFavorite(task.uid, false) + } +} + +data class TaskListUiState(val taskList: List = listOf()) \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f0974ac..b618dd7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,19 +1,20 @@ - pmu-demo - Имя - Фамилия - Группа - Телефон - e-mail - Список студентов - Профиль студента - Записи о студентах отсутствуют - Группа не указана - Сохранить + PMU-ToDoList + Название + Описание + Список задач + Ближайшие + Избранные + Дата окончания задачи + Задача + Задачи отсутствуют + Пользователь не указан + Сохранить О нас + Связаться с разработчиком -

Это текст о нас!

\n\n -

Здесь могла быть Ваша реклама!

\n\n -

Наш сайт ulstu.ru

+

Задачник - ваш личный помощник в организации задач!

\n\n +

Создавайте списки дел, отмечайте важные задачи и следите за своим прогрессом. Простой и интуитивно понятный интерфейс поможет вам организовать ваш день эффективно.

\n\n +

Планируйте, реализуйте и достигайте своих целей с нашим задачником

\ No newline at end of file