This commit is contained in:
Nana 2025-05-15 11:21:46 +03:00
parent 9703a42d64
commit 47a0e7dc3c
157 changed files with 4047 additions and 154 deletions

View File

@ -7,7 +7,7 @@ import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.json.Json
import org.example.presenceapp.domain.entities.Attendance
import org.example.presenceapp.domain.entities.AttendanceView
private val Context.attendanceDataStore by preferencesDataStore(name = "attendance_prefs")
@ -15,17 +15,17 @@ class AttendanceStorageAndroid(private val context: Context): AttendanceStorage
private val ATTENDANCE_KEY = stringPreferencesKey("attendance_map")
override suspend fun saveAttendanceMap(map: Map<String, Attendance>) {
override suspend fun saveAttendanceMap(map: Map<Int, AttendanceView>) {
val json = Json.encodeToString(map)
context.attendanceDataStore.edit { prefs ->
prefs[ATTENDANCE_KEY] = json
}
}
override fun attendanceMapFlow(): Flow<Map<String, Attendance>> {
override fun attendanceMapFlow(): Flow<Map<Int, AttendanceView>> {
return context.attendanceDataStore.data.map { prefs ->
prefs[ATTENDANCE_KEY]?.let {
Json.decodeFromString<Map<String, Attendance>>(it)
Json.decodeFromString<Map<Int, AttendanceView>>(it)
} ?: emptyMap()
}
}

View File

@ -1,4 +1,4 @@
package org.example.presenceapp.ui.settings
package org.example.presenceapp.ui.feature.settings
import android.content.Context
import android.content.SharedPreferences

View File

@ -2,9 +2,8 @@ package org.example.presenceapp
import androidx.compose.runtime.Composable
import cafe.adriel.voyager.navigator.Navigator
import org.example.presenceapp.ui.schedule.ScheduleScreen
import org.example.presenceapp.ui.feature.login.LoginScreen
import org.example.presenceapp.ui.theme.AppTheme
import org.example.project.ui.login.LoginScreen
import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable

View File

@ -2,6 +2,7 @@ package org.example.presenceapp.data.common
import org.example.presenceapp.data.common.dto.attendance.AttendanceRequestDto
import org.example.presenceapp.data.common.dto.attendance.AttendanceResponseDto
import org.example.presenceapp.data.common.dto.attendance.AttendanceTypeResponseDto
import org.example.presenceapp.data.common.dto.attendance.PresettingRequestDto
import org.example.presenceapp.data.common.dto.attendance.PresettingResponseDto
import org.example.presenceapp.data.common.dto.auth.AuthRequestDto
@ -11,15 +12,21 @@ import org.example.presenceapp.data.common.dto.auth.ResponsibleDto
import org.example.presenceapp.data.common.dto.auth.ResponsibleTypeDto
import org.example.presenceapp.data.common.dto.auth.RoleResponseDto
import org.example.presenceapp.data.common.dto.auth.UserResponseDto
import org.example.presenceapp.data.common.dto.group.StudentRequestDto
import org.example.presenceapp.data.common.dto.group.StudentResponseDto
import org.example.presenceapp.data.common.dto.schedule.ScheduleRequestDto
import org.example.presenceapp.data.common.dto.schedule.ScheduleResponseDto
import org.example.presenceapp.data.common.dto.schedule.SubjectResponseDto
import org.example.presenceapp.domain.command.LoginCommand
import org.example.presenceapp.domain.command.attendance.AddAttendanceCommand
import org.example.presenceapp.domain.command.attendance.AddPresettingCommand
import org.example.presenceapp.domain.command.GroupCommand
import org.example.presenceapp.domain.command.LoginCommand
import org.example.presenceapp.domain.command.schedule.GetStudentsByGroupIdCommand
import org.example.presenceapp.domain.command.schedule.GroupCommand
import org.example.presenceapp.domain.entities.AbsenceReason
import org.example.presenceapp.domain.entities.Attendance
import org.example.presenceapp.domain.entities.AttendanceType
import org.example.presenceapp.domain.entities.AttendanceTypeView
import org.example.presenceapp.domain.entities.AttendanceView
import org.example.presenceapp.domain.entities.GroupResponse
import org.example.presenceapp.domain.entities.LoginResponse
import org.example.presenceapp.domain.entities.Presetting
@ -27,6 +34,7 @@ import org.example.presenceapp.domain.entities.Responsible
import org.example.presenceapp.domain.entities.ResponsibleType
import org.example.presenceapp.domain.entities.RoleResponse
import org.example.presenceapp.domain.entities.Schedule
import org.example.presenceapp.domain.entities.Student
import org.example.presenceapp.domain.entities.Subject
import org.example.presenceapp.domain.entities.UserResponse
@ -37,6 +45,10 @@ fun LoginCommand.toDto(): AuthRequestDto = AuthRequestDto(
fun GroupCommand.toDto(): ScheduleRequestDto = ScheduleRequestDto(groupId)
fun GetStudentsByGroupIdCommand.toDto(): StudentRequestDto = StudentRequestDto(
id = groupId
)
fun AddAttendanceCommand.toDto(): AttendanceRequestDto = AttendanceRequestDto(
studentId = studentId,
scheduleId = scheduleId,
@ -50,6 +62,27 @@ fun AddPresettingCommand.toDto(): PresettingRequestDto = PresettingRequestDto(
endAt = endAt
)
fun AttendanceView.toDto(
scheduleId: Int,
absenceReason: String?
): Attendance {
return Attendance(
studentId = this.studentId,
attendanceTypeId = this.type.toDto(absenceReason)
?: throw IllegalArgumentException("Unknown AttendanceTypeView: $type with reason: $absenceReason"),
scheduleId = scheduleId,
date = this.date,
type = this.type.ordinal
)
}
fun AttendanceTypeView.toDto(absenceReason: String?): Int? {
return when (this) {
AttendanceTypeView.PRESENT -> 4
AttendanceTypeView.ABSENT -> absenceReason?.let { AbsenceReason.valueOf(it).id }
}
}
fun ScheduleResponseDto.toEntity(): Schedule = Schedule(
@ -60,6 +93,15 @@ fun ScheduleResponseDto.toEntity(): Schedule = Schedule(
id = id
)
fun StudentResponseDto.toEntity(): Student = Student(
id = studentId,
uuid = uuid,
fio = fio,
role = "",
enrollDate = enrollDate,
expulsionDate = expulsionDate
)
fun SubjectResponseDto.toEntity(): Subject = Subject(
id = id,
name = name
@ -78,22 +120,18 @@ fun UserResponseDto.toEntity(): UserResponse = UserResponse(
role = role.toEntity(),
responsible = responsible.map { it.toEntity() }
)
fun ResponsibleDto.toEntity(): Responsible = Responsible(
group = group.toEntity(),
responsibleType = responsibleType.toEntity()
)
fun GroupDto.toEntity(): GroupResponse = GroupResponse(
id = id,
name = name
)
fun ResponsibleTypeDto.toEntity(): ResponsibleType = ResponsibleType(
id = id,
name = name
)
fun RoleResponseDto.toEntity(): RoleResponse = RoleResponse(
id = id,
name = name
@ -107,6 +145,11 @@ fun AttendanceResponseDto.toEntity(): Attendance = Attendance(
type = attendanceTypeId
)
fun AttendanceTypeResponseDto.toAttendanceType(): AttendanceType = AttendanceType(
id = id,
name = name
)
fun AttendanceResponseDto.toAttendanceType(): AttendanceType = AttendanceType(
id = attendanceTypeId,
name = ""
@ -118,3 +161,26 @@ fun PresettingResponseDto.toEntity(): Presetting = Presetting(
startAt = startAt,
endAt = endAt
)
fun Attendance.toEntity(
attendanceTypeMapper: (Int) -> AttendanceTypeView,
absenceReasonProvider: (Int, Int) -> String?
): AttendanceView {
return AttendanceView(
studentId = this.studentId,
date = this.date,
type = attendanceTypeMapper(this.attendanceTypeId),
isModified = false,
absenceReason = absenceReasonProvider(this.studentId, this.attendanceTypeId)
)
}
fun AttendanceType.toEntity(id: Int): Pair<AttendanceTypeView, AbsenceReason?> {
return when (id) {
4 -> AttendanceTypeView.PRESENT to null
1 -> AttendanceTypeView.ABSENT to AbsenceReason.SICK
2 -> AttendanceTypeView.ABSENT to AbsenceReason.COMPETITION
3 -> AttendanceTypeView.ABSENT to AbsenceReason.SKIP
else -> throw IllegalArgumentException("Unknown attendance id: $id")
}
}

View File

@ -10,10 +10,3 @@ data class AttendanceRequestDto(
val attendanceTypeId: Int,
val attendanceDate: LocalDate
)
@Serializable
data class PresettingRequestDto(
val attendanceTypeId: Int,
val startAt: LocalDate,
val endAt: LocalDate?,
)

View File

@ -15,14 +15,5 @@ data class AttendanceResponseDto(
@Serializable
data class AttendanceTypeResponseDto(
val id: Int,
val name: String,
)
@Serializable
data class PresettingResponseDto(
val id: Int,
val attendanceType: AttendanceResponseDto,
val studentId: Int,
val startAt: LocalDate,
val endAt: LocalDate,
val name: String
)

View File

@ -0,0 +1,11 @@
package org.example.presenceapp.data.common.dto.attendance
import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
@Serializable
data class PresettingRequestDto(
val attendanceTypeId: Int,
val startAt: LocalDate,
val endAt: LocalDate?
)

View File

@ -0,0 +1,13 @@
package org.example.presenceapp.data.common.dto.attendance
import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
@Serializable
data class PresettingResponseDto(
val id: Int,
val attendanceType: AttendanceResponseDto,
val studentId: Int,
val startAt: LocalDate,
val endAt: LocalDate
)

View File

@ -0,0 +1,8 @@
package org.example.presenceapp.data.common.dto.group
import kotlinx.serialization.Serializable
@Serializable
data class StudentRequestDto(
val id: Int
)

View File

@ -2,19 +2,16 @@ package org.example.presenceapp.data.local
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import org.example.presenceapp.domain.entities.AttendanceView
import org.example.presenceapp.data.local.storage.attendance.AttendanceStorage
import org.example.presenceapp.domain.entities.AbsenceReason
import org.example.presenceapp.domain.entities.Attendance
import org.example.presenceapp.domain.entities.AttendanceType
class LocalDataSource(private val attendanceStorage: AttendanceStorage) {
suspend fun saveAttendance(map: Map<String, Attendance>) {
suspend fun saveAttendance(map: Map<Int, AttendanceView>) {
attendanceStorage.saveAttendanceMap(map)
}
fun observeAttendance(): Flow<Map<String, Attendance>> {
fun observeAttendance(): Flow<Map<Int, AttendanceView>> {
return attendanceStorage.attendanceMapFlow()
.map { attendanceMap ->
attendanceMap

View File

@ -1,9 +1,9 @@
package org.example.presenceapp.data.local.storage.attendance
import kotlinx.coroutines.flow.Flow
import org.example.presenceapp.domain.entities.Attendance
import org.example.presenceapp.domain.entities.AttendanceView
interface AttendanceStorage {
suspend fun saveAttendanceMap(map: Map<String, Attendance>)
fun attendanceMapFlow(): Flow<Map<String, Attendance>>
suspend fun saveAttendanceMap(map: Map<Int, AttendanceView>)
fun attendanceMapFlow(): Flow<Map<Int, AttendanceView>>
}

View File

@ -12,14 +12,14 @@ import org.example.presenceapp.domain.command.attendance.AddPresettingCommand
interface AttendanceApi {
@POST("api/v1/presence")
suspend fun addAttendance(@Body commands: List<AttendanceRequestDto>): List<AttendanceResponseDto>
@GET("api/v1/presence/dictionary/attendance_type")
suspend fun getAttendanceTypes(): List<AttendanceTypeResponseDto>
suspend fun addAttendance(@Body command: List<AttendanceRequestDto>): List<AttendanceResponseDto>
@GET("api/v1/presence/{groupId}")
suspend fun getAttendance(@Path("groupId") groupId: Int): List<AttendanceResponseDto>
@GET("api/v1/presence/dictionary/attendance_type/{typeId}")
suspend fun getAttendanceTypes(@Path("typeId") typeId: Int): List<AttendanceTypeResponseDto>
@POST("api/v1/presence/presetting")
suspend fun addPresetting(@Body command: AddPresettingCommand): Boolean

View File

@ -5,14 +5,12 @@ import de.jensklingenberg.ktorfit.http.Path
import org.example.presenceapp.data.common.dto.attendance.AttendanceResponseDto
import org.example.presenceapp.data.common.dto.group.StudentResponseDto
import org.example.presenceapp.data.common.dto.schedule.ScheduleResponseDto
import org.example.presenceapp.domain.entities.Group
interface GroupApi {
@GET("api/v1/group/{id}/schedule")
suspend fun getSchedule(@Path id: Int): List<ScheduleResponseDto>
@GET("api/v1/group/{id}/students")
suspend fun getStudents(@Path id: Int): List<StudentResponseDto>
@GET("api/v1/group/{id}/presence")
suspend fun getPresence(@Path id: Int): List<AttendanceResponseDto>
suspend fun getStudentsByGroupId(@Path id: Int): List<StudentResponseDto>
}

View File

@ -7,16 +7,17 @@ import org.example.presenceapp.data.common.toDto
import org.example.presenceapp.data.remote.api.AttendanceApi
import org.example.presenceapp.domain.command.attendance.AddAttendanceCommand
import org.example.presenceapp.domain.command.attendance.AddPresettingCommand
import org.example.presenceapp.domain.command.attendance.GetAttendanceTypesCommand
class AttendanceApiImpl(private val attendanceApi: AttendanceApi) {
suspend fun addAttendance(commands: List<AddAttendanceCommand>): List<AttendanceResponseDto> =
attendanceApi.addAttendance(commands.map { it.toDto() })
suspend fun addAttendance(command: List<AddAttendanceCommand>): List<AttendanceResponseDto> =
attendanceApi.addAttendance(command.map { it.toDto() })
suspend fun getAttendance(groupId: Int): List<AttendanceResponseDto> =
attendanceApi.getAttendance(groupId)
suspend fun getAttendanceTypes(): List<AttendanceTypeResponseDto> =
attendanceApi.getAttendanceTypes()
suspend fun getAttendanceTypes(typeId: Int): List<AttendanceTypeResponseDto> =
attendanceApi.getAttendanceTypes(typeId)
suspend fun addPresetting(command: AddPresettingCommand): Boolean =
attendanceApi.addPresetting(command)

View File

@ -1,13 +1,13 @@
package org.example.presenceapp.data.remote.impl
import org.example.presenceapp.data.common.dto.attendance.AttendanceResponseDto
import org.example.presenceapp.data.common.dto.group.StudentRequestDto
import org.example.presenceapp.data.common.dto.group.StudentResponseDto
import org.example.presenceapp.data.common.dto.schedule.ScheduleRequestDto
import org.example.presenceapp.data.common.dto.schedule.ScheduleResponseDto
import org.example.presenceapp.data.remote.api.GroupApi
import org.example.presenceapp.domain.entities.Group
class ScheduleApiImpl(private val groupApi: GroupApi) {
suspend fun getSchedule(scheduleRequestDto: ScheduleRequestDto): List<ScheduleResponseDto> = groupApi.getSchedule(scheduleRequestDto.groupId)
suspend fun getStudent(scheduleRequestDto: ScheduleRequestDto): List<StudentResponseDto> = groupApi.getStudents(scheduleRequestDto.groupId)
suspend fun getAttendance(scheduleRequestDto: ScheduleRequestDto): List<AttendanceResponseDto> = groupApi.getPresence(scheduleRequestDto.groupId)
suspend fun getStudentsByGroupId(studentRequestDto: StudentRequestDto): List<StudentResponseDto> = groupApi.getStudentsByGroupId(studentRequestDto.id)
}

View File

@ -3,35 +3,33 @@ package org.example.presenceapp.data.repository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import org.example.presenceapp.data.common.toDto
import org.example.presenceapp.data.common.toAttendanceType
import org.example.presenceapp.data.common.toEntity
import org.example.presenceapp.data.local.LocalDataSource
import org.example.presenceapp.data.remote.impl.AttendanceApiImpl
import org.example.presenceapp.domain.command.attendance.AddAttendanceCommand
import org.example.presenceapp.domain.command.attendance.AddPresettingCommand
import org.example.presenceapp.domain.command.attendance.GetAttendanceCommand
import org.example.presenceapp.domain.command.attendance.GetAttendanceTypesCommand
import org.example.presenceapp.domain.command.attendance.GetPresettingCommand
import org.example.presenceapp.domain.entities.Attendance
import org.example.presenceapp.domain.entities.AttendanceType
import org.example.presenceapp.domain.entities.AttendanceView
import org.example.presenceapp.domain.entities.Presetting
import org.example.presenceapp.domain.repo.attendance.AttendanceRepository
import org.example.presenceapp.domain.repo.attendance.PresettingRepository
import org.example.presenceapp.domain.repo.AttendanceRepository
class AttendanceNetRepository(
private val localDataSource: LocalDataSource,
private val attendanceApiImpl: AttendanceApiImpl
): AttendanceRepository, PresettingRepository {
suspend fun saveAttendanceLocally(attendance: Map<String, Attendance>) {
): AttendanceRepository {
override suspend fun saveAttendanceLocally(attendance: Map<Int, AttendanceView>) {
localDataSource.saveAttendance(attendance)
}
fun observeLocalAttendance(): Flow<Map<String, Attendance>> {
override fun observeLocalAttendance(): Flow<Map<Int, AttendanceView>> {
return localDataSource.observeAttendance()
.map { attendanceMap ->
attendanceMap
}
.catch { e ->
emit(emptyMap())
}
.map { attendanceMap -> attendanceMap }
.catch { e -> emit(emptyMap()) }
}
override suspend fun addAttendance(addAttendanceCommand: AddAttendanceCommand): List<Attendance> {
@ -44,6 +42,11 @@ class AttendanceNetRepository(
return result.map { it.toEntity() }
}
override suspend fun getAttendanceTypes(getAttendanceTypesCommand: GetAttendanceTypesCommand): List<AttendanceType> {
val result = attendanceApiImpl.getAttendanceTypes(getAttendanceTypesCommand.typeId)
return result.map { it.toAttendanceType() }
}
override suspend fun addPresetting(addPresettingCommand: AddPresettingCommand): Boolean {
return attendanceApiImpl.addPresetting(addPresettingCommand)
}

View File

@ -3,8 +3,10 @@ package org.example.presenceapp.data.repository
import org.example.presenceapp.data.common.toDto
import org.example.presenceapp.data.common.toEntity
import org.example.presenceapp.data.remote.impl.ScheduleApiImpl
import org.example.presenceapp.domain.command.GroupCommand
import org.example.presenceapp.domain.command.schedule.GetStudentsByGroupIdCommand
import org.example.presenceapp.domain.command.schedule.GroupCommand
import org.example.presenceapp.domain.entities.Schedule
import org.example.presenceapp.domain.entities.Student
import org.example.presenceapp.domain.repo.ScheduleRepository
class ScheduleNetRepository(
@ -14,4 +16,10 @@ class ScheduleNetRepository(
val result = scheduleApiImpl.getSchedule(groupCommand.toDto())
return result.map { it.toEntity() }
}
override suspend fun getStudentsByGroupId(getStudentsByGroupIdCommand: GetStudentsByGroupIdCommand): List<Student> {
val result = scheduleApiImpl.getStudentsByGroupId(getStudentsByGroupIdCommand.toDto())
println("Students from API: $result")
return result.map { it.toEntity() }
}
}

View File

@ -1,8 +1,8 @@
package org.example.presenceapp.data.repository.settings
import org.example.presenceapp.domain.entities.AttendanceType
import org.example.presenceapp.domain.entities.AttendanceTypeView
interface SettingsRepository {
suspend fun getDefaultAttendanceStatus(): AttendanceType
suspend fun setDefaultAttendanceStatus(type: AttendanceType)
suspend fun getDefaultAttendanceStatus(): AttendanceTypeView
suspend fun setDefaultAttendanceStatus(type: AttendanceTypeView)
}

View File

@ -1,21 +1,21 @@
package org.example.presenceapp.data.repository.settings
import org.example.presenceapp.domain.entities.AttendanceTypeView
import org.example.presenceapp.data.local.storage.SettingsStorage
import org.example.presenceapp.domain.entities.AttendanceType
class SettingsRepositoryImpl(
private val settingsStorage: SettingsStorage
) : SettingsRepository {
override suspend fun getDefaultAttendanceStatus(): AttendanceType {
override suspend fun getDefaultAttendanceStatus(): AttendanceTypeView {
val statusString = settingsStorage.get(
key = "default_attendance_status",
defaultValue = AttendanceType.ABSENT.name
defaultValue = AttendanceTypeView.ABSENT.name
)
return enumValueOf(statusString)
}
override suspend fun setDefaultAttendanceStatus(type: AttendanceType) {
override suspend fun setDefaultAttendanceStatus(type: AttendanceTypeView) {
settingsStorage.put(
key = "default_attendance_status",
value = type.name

View File

@ -13,7 +13,7 @@ import org.example.presenceapp.data.repository.ScheduleNetRepository
import org.example.presenceapp.domain.repo.LoginRepository
import org.example.presenceapp.domain.repo.ScheduleRepository
import org.example.presenceapp.domain.usecases.LoginUseCase
import org.example.project.ui.login.LoginViewModel
import org.example.presenceapp.ui.feature.login.LoginScreenModel
import org.koin.dsl.module
val networkModule = module {
@ -29,5 +29,5 @@ val networkModule = module {
single { ScheduleApiImpl(get()) }
single<ScheduleRepository> { ScheduleNetRepository (get()) }
factory { LoginViewModel(get(), get()) }
factory { org.example.presenceapp.ui.feature.login.LoginScreenModel(get(), get()) }
}

View File

@ -1,5 +0,0 @@
package org.example.presenceapp.domain.command
data class GroupCommand(
val groupId: Int
)

View File

@ -0,0 +1,6 @@
package org.example.presenceapp.domain.command.attendance
data class GetAttendanceTypesCommand(
val typeId: Int,
val typeName: String
)

View File

@ -0,0 +1,5 @@
package org.example.presenceapp.domain.command.schedule
data class GetStudentsByGroupIdCommand(
val groupId: Int
)

View File

@ -0,0 +1,5 @@
package org.example.presenceapp.domain.command.schedule
data class GroupCommand(
val groupId: Int
)

View File

@ -2,27 +2,22 @@ package org.example.presenceapp.domain.common
import org.example.presenceapp.domain.entities.Schedule
data class Student(
val id: String,
val name: String
)
class SomeStudents {
val students = listOf(
Student(id = "1", name = "Васильев Кирилл"),
Student(id = "2", name = "Игнатова Вероника"),
Student(id = "3", name = "Латышева Екатерина"),
Student(id = "4", name = "Ермолаев Егор"),
Student(id = "5", name = "Фролов Владимир"),
Student(id = "6", name = "Чеботарева Анастасия"),
Student(id = "7", name = "Попова Виктория"),
Student(id = "8", name = "Соловьева Лейла"),
Student(id = "9", name = "Орлова Анжелика"),
Student(id = "10", name = "Осипова Татьяна"),
Student(id = "11", name = "Николаева Ева"),
Student(id = "12", name = "Федосеева Майя")
)
}
//class SomeStudents {
// val students = listOf(
// Student(id = 1, name = "Васильев Кирилл"),
// Student(id = 2, name = "Игнатова Вероника"),
// Student(id = 3, name = "Латышева Екатерина"),
// Student(id = 4, name = "Ермолаев Егор"),
// Student(id = 5, name = "Фролов Владимир"),
// Student(id = 6, name = "Чеботарева Анастасия"),
// Student(id = 7, name = "Попова Виктория"),
// Student(id = 8, name = "Соловьева Лейла"),
// Student(id = 9, name = "Орлова Анжелика"),
// Student(id = 10, name = "Осипова Татьяна"),
// Student(id = 11, name = "Николаева Ева"),
// Student(id = 12, name = "Федосеева Майя")
// )
//}
object SampleData {
val lessonTimes = listOf("9:00", "9:55", "10:50", "11:55", "13:00", "14:00", "14:55", "15:45")

View File

@ -3,6 +3,7 @@ package org.example.project.domain.models
import kotlinx.datetime.LocalDate
import kotlinx.datetime.Month
import kotlinx.datetime.number
import org.example.presenceapp.domain.entities.Week
fun Week.formatWeek(): String {

View File

@ -15,5 +15,5 @@ data class Attendance(
@Serializable
data class AttendanceType(
val id: Int,
var name: String = ""
var name: String
)

View File

@ -0,0 +1,26 @@
package org.example.presenceapp.domain.entities
import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
@Serializable
data class AttendanceView(
val studentId: Int,
val date: LocalDate,
val type: AttendanceTypeView,
val isModified: Boolean,
val absenceReason: String? = null
)
@Serializable
enum class AttendanceTypeView {
PRESENT,
ABSENT
}
@Serializable
enum class AbsenceReason(val id: Int) {
SICK(1),
COMPETITION(2),
SKIP(3)
}

View File

@ -5,5 +5,5 @@ import kotlinx.datetime.LocalDate
data class DayData(
val date: LocalDate,
val isCurrentMonth: Boolean,
val attendance: AttendanceType?
val attendance: AttendanceTypeView?
)

View File

@ -1,4 +1,4 @@
package org.example.project.domain.models
package org.example.presenceapp.domain.entities
import kotlinx.datetime.LocalDate
import kotlinx.datetime.Month

View File

@ -9,9 +9,3 @@ sealed class Either<A, B> {
class Left<A, B>(val value: A): Either<A, B>()
class Right<A, B>(val value: B): Either<A, B>()
}
enum class ServiceError {
NOT_FOUND,
UNAUTHORIZED,
NOT_CREATED
}

View File

@ -1,5 +1,7 @@
package org.example.presenceapp.domain.entities
import kotlinx.datetime.LocalDate
data class Schedule(
val id: Int,
val lessonNumber: Int,
@ -7,9 +9,23 @@ data class Schedule(
val subject: Subject,
val dayOfWeek: Int,
)
data class Subject(
val id: Int,
val name: String
)
data class Student(
val id: Int,
val uuid: String,
val fio: String,
val role: String,
val enrollDate: LocalDate,
var expulsionDate: LocalDate? = null
)
data class Group(
val groupId: Int,
var name: String,
var students: List<Student> = emptyList()
)

View File

@ -0,0 +1,24 @@
package org.example.presenceapp.domain.repo
import kotlinx.coroutines.flow.Flow
import org.example.presenceapp.domain.command.attendance.AddAttendanceCommand
import org.example.presenceapp.domain.command.attendance.AddPresettingCommand
import org.example.presenceapp.domain.command.attendance.GetAttendanceCommand
import org.example.presenceapp.domain.command.attendance.GetAttendanceTypesCommand
import org.example.presenceapp.domain.command.attendance.GetPresettingCommand
import org.example.presenceapp.domain.entities.Attendance
import org.example.presenceapp.domain.entities.AttendanceType
import org.example.presenceapp.domain.entities.AttendanceView
import org.example.presenceapp.domain.entities.Presetting
interface AttendanceRepository {
suspend fun saveAttendanceLocally(attendance: Map<Int, AttendanceView>)
fun observeLocalAttendance(): Flow<Map<Int, AttendanceView>>
suspend fun addAttendance(addAttendanceCommand: AddAttendanceCommand): List<Attendance>
suspend fun getAttendance(getAttendanceCommand: GetAttendanceCommand): List<Attendance>
suspend fun getAttendanceTypes(getAttendanceTypesCommand: GetAttendanceTypesCommand): List<AttendanceType>
suspend fun addPresetting(addPresettingCommand: AddPresettingCommand): Boolean
suspend fun getPresetting(getPresettingCommand: GetPresettingCommand): List<Presetting>
}

View File

@ -1,8 +1,13 @@
package org.example.presenceapp.domain.repo
import org.example.presenceapp.domain.command.GroupCommand
import org.example.presenceapp.data.common.dto.group.StudentResponseDto
import org.example.presenceapp.domain.command.schedule.GetStudentsByGroupIdCommand
import org.example.presenceapp.domain.command.schedule.GroupCommand
import org.example.presenceapp.domain.entities.Group
import org.example.presenceapp.domain.entities.Schedule
import org.example.presenceapp.domain.entities.Student
interface ScheduleRepository {
suspend fun getSchedule(groupCommand: GroupCommand): List<Schedule>
suspend fun getStudentsByGroupId(getStudentsByGroupIdCommand: GetStudentsByGroupIdCommand): List<Student>
}

View File

@ -1,10 +0,0 @@
package org.example.presenceapp.domain.repo.attendance
import org.example.presenceapp.domain.command.attendance.AddAttendanceCommand
import org.example.presenceapp.domain.command.attendance.GetAttendanceCommand
import org.example.presenceapp.domain.entities.Attendance
interface AttendanceRepository {
suspend fun addAttendance(addAttendanceCommand: AddAttendanceCommand): List<Attendance>
suspend fun getAttendance(getAttendanceCommand: GetAttendanceCommand): List<Attendance>
}

View File

@ -1,7 +0,0 @@
package org.example.presenceapp.domain.repo.attendance
import org.example.presenceapp.domain.entities.AttendanceType
interface AttendanceTypeRepository {
suspend fun getAttendanceTypes(): List<AttendanceType>
}

View File

@ -1,10 +0,0 @@
package org.example.presenceapp.domain.repo.attendance
import org.example.presenceapp.domain.command.attendance.AddPresettingCommand
import org.example.presenceapp.domain.command.attendance.GetPresettingCommand
import org.example.presenceapp.domain.entities.Presetting
interface PresettingRepository {
suspend fun addPresetting(addPresettingCommand: AddPresettingCommand): Boolean
suspend fun getPresetting(getPresettingCommand: GetPresettingCommand): List<Presetting>
}

View File

@ -2,20 +2,46 @@ package org.example.presenceapp.domain.usecases
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import org.example.presenceapp.data.common.toDto
import org.example.presenceapp.data.common.toEntity
import org.example.presenceapp.domain.command.attendance.AddAttendanceCommand
import org.example.presenceapp.domain.command.attendance.AddPresettingCommand
import org.example.presenceapp.domain.command.attendance.GetAttendanceCommand
import org.example.presenceapp.domain.command.attendance.GetAttendanceTypesCommand
import org.example.presenceapp.domain.command.attendance.GetPresettingCommand
import org.example.presenceapp.domain.entities.Attendance
import org.example.presenceapp.domain.entities.AttendanceType
import org.example.presenceapp.domain.entities.AttendanceTypeView
import org.example.presenceapp.domain.entities.AttendanceView
import org.example.presenceapp.domain.entities.Either
import org.example.presenceapp.domain.entities.Presetting
import org.example.presenceapp.domain.repo.attendance.AttendanceRepository
import org.example.presenceapp.domain.repo.AttendanceRepository
class AttendanceUseCase(
private val attendanceRepository: AttendanceRepository
) {
fun addAttendance(addAttendanceCommand: AddAttendanceCommand): Flow<Either<Exception, Attendance>> = flow {
return@flow try {
private fun Attendance.toView(
attendanceTypeMapper: (Int) -> AttendanceTypeView,
absenceReasonProvider: (Int, Int) -> String?
): AttendanceView = this.toEntity(attendanceTypeMapper, absenceReasonProvider)
private fun AttendanceView.toApi(
scheduleId: Int,
absenceReason: String?
): Attendance = this.toDto(scheduleId, absenceReason)
suspend fun saveAttendanceLocally(attendance: Map<Int, AttendanceView>) {
attendanceRepository.saveAttendanceLocally(attendance)
}
fun observeLocalAttendance(): Flow<Map<Int, AttendanceView>> {
return attendanceRepository.observeLocalAttendance()
}
fun addAttendance(addAttendanceCommand: AddAttendanceCommand): Flow<Either<Exception, List<Attendance>>> = flow {
try {
val result = attendanceRepository.addAttendance(addAttendanceCommand)
emit(Either.Right(result))
} catch (e: Exception) {
@ -23,8 +49,33 @@ class AttendanceUseCase(
}
}
fun addAttendanceView(
attendanceViews: List<AttendanceView>,
scheduleId: Int,
absenceReasonProvider: (AttendanceView) -> String?
): Flow<Either<Exception, List<Attendance>>> = flow {
try {
val results = mutableListOf<Attendance>()
attendanceViews.forEach { view ->
val absenceReason = absenceReasonProvider(view)
val dto = view.toApi(scheduleId, absenceReason)
val command = AddAttendanceCommand(
attendanceDate = dto.date,
studentId = dto.studentId,
attendanceTypeId = dto.attendanceTypeId,
scheduleId = dto.scheduleId
)
val result = attendanceRepository.addAttendance(command)
results.addAll(result)
}
emit(Either.Right(results))
} catch (e: Exception) {
emit(Either.Left(e))
}
}
fun getAttendance(getAttendanceCommand: GetAttendanceCommand): Flow<Either<Exception, List<Attendance>>> = flow {
return@flow try {
try {
val result = attendanceRepository.getAttendance(getAttendanceCommand)
emit(Either.Right(result))
} catch (e: Exception) {
@ -32,8 +83,47 @@ class AttendanceUseCase(
}
}
fun addPresetting(addPresettingCommand: AddPresettingCommand): Flow<Either<Exception, Presetting>> = flow {
return@flow try {
fun getAttendanceView(
getAttendanceCommand: GetAttendanceCommand,
attendanceTypeMapper: (Int) -> AttendanceTypeView,
absenceReasonProvider: (Int, Int) -> String?
): Flow<Either<Exception, List<AttendanceView>>> = flow {
try {
val result = attendanceRepository.getAttendance(getAttendanceCommand)
.map { it.toView(attendanceTypeMapper, absenceReasonProvider) }
emit(Either.Right(result))
} catch (e: Exception) {
emit(Either.Left(e))
}
}
fun getAttendanceTypes(getAttendanceTypesCommand: GetAttendanceTypesCommand): Flow<Either<Exception, List<AttendanceType>>> = flow {
try {
val result = attendanceRepository.getAttendanceTypes(getAttendanceTypesCommand)
emit(Either.Right(result))
} catch (e: Exception) {
emit(Either.Left(e))
}
}
fun getAttendanceTypesView(
getAttendanceTypesCommand: GetAttendanceTypesCommand,
attendanceTypeMapper: (Int) -> AttendanceTypeView
): Flow<Either<Exception, List<AttendanceTypeView>>> = flow {
try {
val result = attendanceRepository.getAttendanceTypes(getAttendanceTypesCommand)
.map { attendanceType ->
val (attendanceTypeView, _) = attendanceType.toEntity(attendanceType.id)
attendanceTypeView
}
emit(Either.Right(result))
} catch (e: Exception) {
emit(Either.Left(e))
}
}
fun addPresetting(addPresettingCommand: AddPresettingCommand): Flow<Either<Exception, Boolean>> = flow {
try {
val result = attendanceRepository.addPresetting(addPresettingCommand)
emit(Either.Right(result))
} catch (e: Exception) {
@ -42,7 +132,7 @@ class AttendanceUseCase(
}
fun getPresetting(getPresettingCommand: GetPresettingCommand): Flow<Either<Exception, List<Presetting>>> = flow {
return@flow try {
try {
val result = attendanceRepository.getPresetting(getPresettingCommand)
emit(Either.Right(result))
} catch (e: Exception) {

View File

@ -2,9 +2,12 @@ package org.example.presenceapp.domain.usecases
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import org.example.presenceapp.domain.command.GroupCommand
import org.example.presenceapp.data.common.dto.group.StudentResponseDto
import org.example.presenceapp.domain.command.schedule.GetStudentsByGroupIdCommand
import org.example.presenceapp.domain.command.schedule.GroupCommand
import org.example.presenceapp.domain.entities.Either
import org.example.presenceapp.domain.entities.Schedule
import org.example.presenceapp.domain.entities.Student
import org.example.presenceapp.domain.repo.ScheduleRepository
class ScheduleUseCase(
@ -18,4 +21,13 @@ class ScheduleUseCase(
emit(Either.Left(e))
}
}
fun getStudentsByGroupId(getStudentsByGroupIdCommand: GetStudentsByGroupIdCommand): Flow<Either<Exception, List<Student>>> = flow {
return@flow try {
val result = scheduleRepository.getStudentsByGroupId(getStudentsByGroupIdCommand)
emit(Either.Right(result))
} catch (e: Exception) {
emit(Either.Left(e))
}
}
}

View File

@ -0,0 +1,61 @@
package org.example.presenceapp.ui.base
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
interface ViewEvent
interface ViewState
interface ViewSideEffect
const val SIDE_EFFECTS_KEY = "side-effects_key"
abstract class BaseViewModel<Event: ViewEvent, UiState: ViewState, Effect: ViewSideEffect> : ScreenModel {
abstract fun setInitialState(): UiState
abstract fun handleEvents(event: Event)
private val initialState: UiState by lazy { setInitialState() }
private val _viewState = MutableStateFlow(initialState)
val viewState: StateFlow<UiState> = _viewState
private val _event = Channel<Event>(Channel.UNLIMITED)
private val _effect = Channel<Effect>(Channel.UNLIMITED)
val effect = _effect.receiveAsFlow()
init {
subscribeToEvents()
}
private fun subscribeToEvents() {
screenModelScope.launch {
_event.consumeAsFlow().collect { event ->
handleEvents(event)
}
}
}
fun setEvent(event: Event) {
screenModelScope.launch {
_event.send(event)
}
}
protected fun setState(reducer: UiState.() -> UiState) {
_viewState.value = _viewState.value.reducer()
}
protected fun setEffect(builder: () -> Effect) {
screenModelScope.launch {
_effect.send(builder())
}
}
}

View File

@ -0,0 +1,48 @@
package org.example.presenceapp.ui.feature.attendance
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import org.example.presenceapp.domain.entities.AttendanceTypeView
import org.example.presenceapp.domain.entities.AttendanceView
import org.example.presenceapp.domain.entities.Student
import org.example.presenceapp.ui.base.ViewEvent
import org.example.presenceapp.ui.base.ViewSideEffect
import org.example.presenceapp.ui.base.ViewState
object AttendanceContract {
sealed class Event : ViewEvent {
data object LoadStudents : Event()
data object LoadAttendance : Event()
data class UpdateDefaultStatus(val type: AttendanceTypeView) : Event()
data class UpdateAttendanceForSelected(val studentIds: Set<Int>, val type: AttendanceTypeView) : Event()
data class ChangeSortType(val sortType: AttendanceTypeView) : Event()
data class UpdateAbsenceReason(val studentId: Int, val reason: String) : Event()
data class SaveAttendanceToApi(val scheduleId: Int) : Event()
data class LoadAttendanceFromApi(val groupId: Int, val date: LocalDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date) : Event()
data object CheckDialogState : Event()
data object DismissDialog : Event()
}
data class State(
val students: List<Student> = emptyList(),
val attendanceMap: Map<Int, AttendanceView> = emptyMap(),
val sortType: AttendanceTypeView = AttendanceTypeView.PRESENT,
val showDialog: Boolean = false
) : ViewState
sealed class Effect : ViewSideEffect {
data class ShowError(val message: String?) : Effect()
}
}
fun AttendanceContract.State.groupedStudents(): List<Pair<String, List<Student>>> {
val present = students.filter { attendanceMap[it.id]?.type == AttendanceTypeView.PRESENT }.sortedBy { it.fio }
val absent = students.filter { attendanceMap[it.id]?.type == AttendanceTypeView.ABSENT }.sortedBy { it.fio }
return when (sortType) {
AttendanceTypeView.PRESENT -> listOf("Присутствующие" to present, "Отсутствующие" to absent)
AttendanceTypeView.ABSENT -> listOf("Отсутствующие" to absent, "Присутствующие" to present)
}
}

View File

@ -0,0 +1,147 @@
package org.example.presenceapp.ui.feature.attendance
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import org.example.presenceapp.data.local.LocalDataSource
import org.example.presenceapp.data.local.storage.SettingsStorage
import org.example.presenceapp.data.local.storage.attendance.AttendanceStorageProvider
import org.example.presenceapp.data.remote.impl.AttendanceApiImpl
import org.example.presenceapp.data.remote.impl.ScheduleApiImpl
import org.example.presenceapp.data.remote.network.KtorfitClient
import org.example.presenceapp.data.repository.AttendanceNetRepository
import org.example.presenceapp.data.repository.ScheduleNetRepository
import org.example.presenceapp.data.repository.settings.SettingsRepository
import org.example.presenceapp.data.repository.settings.SettingsRepositoryImpl
import org.example.presenceapp.domain.entities.Schedule
import org.example.presenceapp.domain.repo.AttendanceRepository
import org.example.presenceapp.domain.repo.ScheduleRepository
import org.example.presenceapp.domain.usecases.AttendanceUseCase
import org.example.presenceapp.domain.usecases.ScheduleUseCase
import org.example.presenceapp.getPlatformContext
import org.example.presenceapp.ui.base.SIDE_EFFECTS_KEY
import org.example.presenceapp.ui.feature.attendance.composables.AttendanceColumn
import org.example.presenceapp.ui.feature.commons.CommonTopBar
import org.example.presenceapp.ui.feature.settings.Preset
import org.example.presenceapp.ui.feature.settings.SettingsScreenModel
import org.example.presenceapp.ui.feature.settings.getSettingsManager
import org.example.presenceapp.ui.theme.AppTheme
import org.example.presenceapp.ui.types.ScreenType
class AttendanceScreen(private val selectedLesson: Schedule) : Screen {
@Composable
override fun Content() {
val platformContext = getPlatformContext()
val attendanceStorage = AttendanceStorageProvider(platformContext).provide()
val localDataSource = LocalDataSource(attendanceStorage)
val attendanceApi = KtorfitClient.createAttendanceApi()
val attendanceApiImpl = AttendanceApiImpl(attendanceApi)
val scheduleApi = KtorfitClient.createScheduleApi()
val scheduleApiImpl = ScheduleApiImpl(scheduleApi)
val attendanceRepository: AttendanceRepository = AttendanceNetRepository(
localDataSource = localDataSource,
attendanceApiImpl = attendanceApiImpl
)
val scheduleRepository: ScheduleRepository = ScheduleNetRepository(
scheduleApiImpl = scheduleApiImpl
)
val attendanceUseCase = AttendanceUseCase(attendanceRepository)
val scheduleUseCase = ScheduleUseCase(scheduleRepository)
val settingsStorage = SettingsStorage(platformContext)
val settingsRepository: SettingsRepository = SettingsRepositoryImpl(settingsStorage)
val settingsScreenModel = rememberScreenModel { SettingsScreenModel(settingsRepository = settingsRepository) }
val settingsManager = getSettingsManager()
val screenModel = rememberScreenModel {
AttendanceScreenModel(
attendanceUseCase = attendanceUseCase,
scheduleUseCase = scheduleUseCase,
settingsScreenModel = settingsScreenModel,
settingsManager = settingsManager
)
}
LaunchedEffect(SIDE_EFFECTS_KEY) {
screenModel.setEvent(AttendanceContract.Event.LoadStudents)
screenModel.setEvent(AttendanceContract.Event.LoadAttendance)
screenModel.setEvent(
AttendanceContract.Event.LoadAttendanceFromApi(
groupId = selectedLesson.id,
date = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
)
)
}
Attendance(
screenModel = screenModel,
selectedLesson = selectedLesson
)
}
}
@Composable
fun Attendance(
screenModel: AttendanceScreenModel,
selectedLesson: Schedule
) {
val state by screenModel.viewState.collectAsState()
val showDialog = state.showDialog
LaunchedEffect(SIDE_EFFECTS_KEY) {
screenModel.setEvent(AttendanceContract.Event.CheckDialogState)
}
if (showDialog) {
key(showDialog) {
Preset(
onDismiss = { screenModel.setEvent(AttendanceContract.Event.DismissDialog) },
onStatusSelected = { selectedStatus ->
screenModel.setEvent(
AttendanceContract.Event.UpdateDefaultStatus(selectedStatus)
)
}
)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.background(AppTheme.colors.white)
) {
Scaffold(
topBar = {
CommonTopBar(
screenType = ScreenType.GROUP,
text = selectedLesson.subject.name,
onChangeSortType = { newSortType ->
screenModel.setEvent(
AttendanceContract.Event.ChangeSortType(newSortType)
)
}
)
}
) { padding ->
AttendanceColumn(
modifier = Modifier
.padding(padding)
.fillMaxSize(),
state = state,
onEvent = { event -> screenModel.setEvent(event) }
)
}
}
}

View File

@ -0,0 +1,425 @@
package org.example.presenceapp.ui.feature.attendance
import cafe.adriel.voyager.core.model.screenModelScope
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import org.example.presenceapp.domain.command.attendance.GetAttendanceCommand
import org.example.presenceapp.domain.command.schedule.GetStudentsByGroupIdCommand
import org.example.presenceapp.domain.entities.AbsenceReason
import org.example.presenceapp.domain.entities.AttendanceTypeView
import org.example.presenceapp.domain.entities.AttendanceView
import org.example.presenceapp.domain.entities.Either
import org.example.presenceapp.domain.usecases.AttendanceUseCase
import org.example.presenceapp.domain.usecases.ScheduleUseCase
import org.example.presenceapp.ui.base.BaseViewModel
import org.example.presenceapp.ui.feature.attendance.composables.toReadableString
import org.example.presenceapp.ui.feature.settings.SettingsManager
import org.example.presenceapp.ui.feature.settings.SettingsScreenModel
class AttendanceScreenModel(
private val attendanceUseCase: AttendanceUseCase,
private val scheduleUseCase: ScheduleUseCase,
private val settingsScreenModel: SettingsScreenModel,
private val settingsManager: SettingsManager
) : BaseViewModel<AttendanceContract.Event, AttendanceContract.State, AttendanceContract.Effect>() {
private val today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
override fun setInitialState() = AttendanceContract.State()
override fun handleEvents(event: AttendanceContract.Event) {
when (event) {
AttendanceContract.Event.LoadStudents -> loadStudents()
AttendanceContract.Event.LoadAttendance -> loadAttendance()
is AttendanceContract.Event.UpdateDefaultStatus -> updateDefaultStatus(event.type)
is AttendanceContract.Event.UpdateAttendanceForSelected -> updateAttendanceForSelected(event.studentIds, event.type)
is AttendanceContract.Event.ChangeSortType -> setState { copy(sortType = event.sortType) }
is AttendanceContract.Event.UpdateAbsenceReason -> updateAbsenceReason(event.studentId, event.reason)
is AttendanceContract.Event.SaveAttendanceToApi -> saveAttendanceToApi(event.scheduleId)
is AttendanceContract.Event.LoadAttendanceFromApi -> loadAttendanceFromApi(event.groupId, event.date)
AttendanceContract.Event.CheckDialogState -> checkDialogState()
AttendanceContract.Event.DismissDialog -> dismissDialog()
}
}
companion object {
const val PRESENT = "присут"
const val ABSENT = "отсут"
fun AttendanceView.toDisplayStatus(): String {
return when (type) {
AttendanceTypeView.PRESENT -> PRESENT
AttendanceTypeView.ABSENT -> absenceReason?.toReadableString() ?: ABSENT
}
}
}
private fun checkDialogState() {
if (!settingsManager.isDialogShown()) {
setState { copy(showDialog = true) }
}
}
private fun dismissDialog() {
setState { copy(showDialog = false) }
settingsManager.setDialogShown()
}
private fun loadStudents() {
screenModelScope.launch {
scheduleUseCase.getStudentsByGroupId(GetStudentsByGroupIdCommand(groupId = 1))
.collect { result ->
when (result) {
is Either.Right -> setState { copy(students = result.value) }
is Either.Left -> setEffect { AttendanceContract.Effect.ShowError(result.value.message) }
}
}
}
}
private fun loadAttendance() {
screenModelScope.launch {
val defaultStatus = settingsScreenModel.defaultStatus.first()
val students = viewState.value.students
if (students.isEmpty()) return@launch
attendanceUseCase.observeLocalAttendance()
.combine(settingsScreenModel.defaultStatus) { savedAttendance, defaultStatusValue ->
students.associate { student ->
val saved = savedAttendance[student.id]
if (saved != null) {
student.id to saved
} else {
student.id to AttendanceView(
studentId = student.id,
date = today,
type = defaultStatusValue,
isModified = false,
absenceReason = if (defaultStatusValue == AttendanceTypeView.ABSENT) AbsenceReason.SKIP.name else null
)
}
}
}.collect { map ->
setState { copy(attendanceMap = map) }
}
}
}
private fun updateDefaultStatus(type: AttendanceTypeView) {
settingsScreenModel.updateDefaultStatus(type)
val updatedMap = viewState.value.attendanceMap.toMutableMap().apply {
entries.forEach { (studentId, attendance) ->
if (!attendance.isModified) {
this[studentId] = attendance.copy(type = type)
}
}
}
setState { copy(attendanceMap = updatedMap) }
screenModelScope.launch {
attendanceUseCase.saveAttendanceLocally(updatedMap)
}
}
private fun updateAttendanceForSelected(studentIds: Set<Int>, type: AttendanceTypeView) {
val updatedMap = viewState.value.attendanceMap.toMutableMap().apply {
studentIds.forEach { studentId ->
this[studentId] = AttendanceView(
studentId = studentId,
date = today,
type = type,
isModified = true,
absenceReason = if (type == AttendanceTypeView.ABSENT) AbsenceReason.SKIP.name else null
)
}
}
setState { copy(attendanceMap = updatedMap) }
screenModelScope.launch {
attendanceUseCase.saveAttendanceLocally(updatedMap)
}
}
private fun updateAbsenceReason(studentId: Int, reason: String) {
val updatedMap = viewState.value.attendanceMap.toMutableMap().apply {
val attendance = this[studentId]
if (attendance != null && attendance.type == AttendanceTypeView.ABSENT) {
this[studentId] = attendance.copy(absenceReason = reason, isModified = true)
}
}
setState { copy(attendanceMap = updatedMap) }
screenModelScope.launch {
attendanceUseCase.saveAttendanceLocally(updatedMap)
}
}
private fun saveAttendanceToApi(scheduleId: Int) {
screenModelScope.launch {
attendanceUseCase.addAttendanceView(
attendanceViews = viewState.value.attendanceMap.values.toList(),
scheduleId = scheduleId,
absenceReasonProvider = { it.absenceReason }
).collect { result ->
if (result is Either.Left) {
setEffect { AttendanceContract.Effect.ShowError(result.value.message) }
}
}
}
}
private fun loadAttendanceFromApi(groupId: Int, date: LocalDate) {
screenModelScope.launch {
attendanceUseCase.getAttendanceView(
getAttendanceCommand = GetAttendanceCommand(groupId, date),
attendanceTypeMapper = { id -> if (id == 4) AttendanceTypeView.PRESENT else AttendanceTypeView.ABSENT },
absenceReasonProvider = { _, attendanceTypeId -> AbsenceReason.entries.firstOrNull { it.id == attendanceTypeId }?.name }
).collect { result ->
when (result) {
is Either.Right -> setState { copy(attendanceMap = result.value.associateBy { it.studentId }) }
is Either.Left -> setEffect { AttendanceContract.Effect.ShowError(result.value.message) }
}
}
}
}
}
//class AttendanceScreenModel(
// private val attendanceUseCase: AttendanceUseCase,
// private val scheduleUseCase: ScheduleUseCase,
// private val settingsScreenModel: SettingsScreenModel,
// private val settingsManager: SettingsManager
//) : ScreenModel {
// private val _students = MutableStateFlow<List<Student>>(emptyList())
// private val _attendanceMap = MutableStateFlow<Map<Int, AttendanceView>>(emptyMap())
// private val _sortType = MutableStateFlow(AttendanceTypeView.PRESENT)
// private val _defaultStatus: StateFlow<AttendanceTypeView> = settingsScreenModel.defaultStatus
//
// val attendanceMap: StateFlow<Map<Int, AttendanceView>> = _attendanceMap.asStateFlow()
//
// val today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
//
// private val _showDialog = mutableStateOf(false)
// val showDialog: State<Boolean> = _showDialog
//
// fun checkDialogState() {
// if (!settingsManager.isDialogShown()) {
// _showDialog.value = true
// println("Dialog should be shown")
// } else {
// println("Dialog already shown, not showing again")
// }
// }
//
// fun onDialogDismissed() {
// _showDialog.value = false
// settingsManager.setDialogShown()
// }
//
// companion object {
// const val PRESENT = "присут"
// const val ABSENT = "отсут"
//
// fun AttendanceView.toDisplayStatus(): String {
// return when (type) {
// AttendanceTypeView.PRESENT -> PRESENT
// AttendanceTypeView.ABSENT -> absenceReason?.toReadableString() ?: ABSENT
// }
// }
// }
//
// init {
// loadStudents()
// loadAttendance()
// }
//
// private fun loadStudents() {
// screenModelScope.launch {
// scheduleUseCase.getStudentsByGroupId(
// GetStudentsByGroupIdCommand(groupId = 1)
// ).collect { result ->
// when (result) {
// is Either.Right -> {
// _students.value = result.value
// }
//
// is Either.Left -> {
// println("Error loading students: ${result.value.message}")
// _students.value = emptyList()
// }
// }
// }
// }
// }
//
// @OptIn(ExperimentalCoroutinesApi::class)
// private fun loadAttendance() {
// screenModelScope.launch {
// _students
// .filter { it.isNotEmpty() }
// .distinctUntilChanged()
// .flatMapLatest { students ->
// attendanceUseCase.observeLocalAttendance()
// .combine(_defaultStatus) { savedAttendance, defaultStatusValue ->
// students.associate { student ->
// val saved = savedAttendance[student.id]
// if (saved != null) {
// student.id to saved
// } else {
// student.id to AttendanceView(
// studentId = student.id,
// date = today,
// type = defaultStatusValue,
// isModified = false,
// absenceReason = if (defaultStatusValue == AttendanceTypeView.ABSENT) AbsenceReason.SKIP.name else null
// )
// }
// }
// }
// }
// .collect {
// _attendanceMap.value = it
// }
// }
// }
//
// val groupedStudents = combine(
// _students,
// _attendanceMap,
// _sortType
// ) { students, attendance, sortType ->
// val present = students.filter { attendance[it.id]?.type == AttendanceTypeView.PRESENT }
// .sortedBy { it.fio }
// val absent = students.filter { attendance[it.id]?.type == AttendanceTypeView.ABSENT }
// .sortedBy { it.fio }
//
// when (sortType) {
// AttendanceTypeView.PRESENT -> listOf(
// "Присутствующие" to present,
// "Отсутствующие" to absent
// )
//
// AttendanceTypeView.ABSENT -> listOf(
// "Отсутствующие" to absent,
// "Присутствующие" to present
// )
// }
// }
//
// fun updateDefaultStatus(type: AttendanceTypeView) {
// settingsScreenModel.updateDefaultStatus(type)
//
// val updatedMap = _attendanceMap.value.toMutableMap().apply {
// entries.forEach { (studentId, attendance) ->
// if (!attendance.isModified) {
// this[studentId] = attendance.copy(type = type)
// }
// }
// }
// _attendanceMap.value = updatedMap
// screenModelScope.launch {
// saveAttendanceToStorage(updatedMap)
// }
// }
//
// fun updateAttendanceForSelected(studentIds: Set<Int>, type: AttendanceTypeView) {
// val updatedMap = _attendanceMap.value.toMutableMap().apply {
// studentIds.forEach { studentId ->
// this[studentId] = AttendanceView(
// studentId = studentId,
// date = today,
// type = type,
// isModified = true,
// absenceReason = if (type == AttendanceTypeView.ABSENT) AbsenceReason.SKIP.name else null
// )
// }
// }
// _attendanceMap.value = updatedMap
// screenModelScope.launch {
// attendanceUseCase.saveAttendanceLocally(updatedMap)
// }
// }
//
// fun changeSortType(newSortType: AttendanceTypeView) {
// _sortType.value = newSortType
// }
//
// private suspend fun saveAttendanceToStorage(map: Map<Int, AttendanceView>) {
// attendanceUseCase.saveAttendanceLocally(map)
// }
//
// fun updateAbsenceReason(studentId: Int, reason: String) {
// val updatedMap = _attendanceMap.value.toMutableMap().apply {
// val attendance = this[studentId]
// if (attendance != null && attendance.type == AttendanceTypeView.ABSENT) {
// this[studentId] = attendance.copy(absenceReason = reason, isModified = true)
// }
// }
// screenModelScope.launch {
// saveAttendanceToStorage(updatedMap)
// _attendanceMap.emit(updatedMap)
// }
// }
//
//
// fun saveAttendanceToApi(scheduleId: Int) {
// screenModelScope.launch {
// attendanceUseCase.addAttendanceView(
// attendanceViews = _attendanceMap.value.values.toList(),
// scheduleId = scheduleId,
// absenceReasonProvider = { it.absenceReason }
// ).collect { result ->
// when (result) {
// is Either.Right -> {
// println("Attendance saved to API: ${result.value}")
// }
//
// is Either.Left -> {
// println("Error saving attendance to API: ${result.value.message}")
// }
// }
// }
// }
// }
//
// fun loadAttendanceFromApi(groupId: Int, date: LocalDate = today) {
// screenModelScope.launch {
// attendanceUseCase.getAttendanceView(
// getAttendanceCommand = GetAttendanceCommand(
// groupId = groupId,
// beforeAt = date
// ),
// attendanceTypeMapper = { id ->
// when (id) {
// 4 -> AttendanceTypeView.PRESENT
// else -> AttendanceTypeView.ABSENT
// }
// },
// absenceReasonProvider = { _: Int, attendanceTypeId: Int ->
// AbsenceReason.entries.firstOrNull { it.id == attendanceTypeId }?.name
// }
// ).collect { result ->
// when (result) {
// is Either.Right -> {
// _attendanceMap.value = result.value.associateBy { it.studentId }
// println("Attendance loaded from API")
// }
//
// is Either.Left -> {
// println("Error loading attendance from API: ${result.value.message}")
// }
// }
// }
// }
// }
//}

View File

@ -0,0 +1,90 @@
package org.example.presenceapp.ui.feature.attendance.composables
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.example.presenceapp.domain.entities.AttendanceTypeView
import org.example.presenceapp.ui.feature.attendance.AttendanceContract
import org.example.presenceapp.ui.feature.attendance.groupedStudents
import org.example.presenceapp.ui.theme.AppTheme
@Composable
fun AttendanceColumn(
modifier: Modifier,
state: AttendanceContract.State,
onEvent: (AttendanceContract.Event) -> Unit
) {
val groups = state.groupedStudents()
val attendanceMap = state.attendanceMap
var expanded by remember { mutableStateOf<Int?>(null) }
val selectedStudents = remember { mutableStateOf<Set<Int>>(emptySet()) }
var isSelectionMode by remember { mutableStateOf(false) }
val listState = rememberLazyListState()
LaunchedEffect(groups) {
listState.scrollToItem(0)
}
LaunchedEffect(selectedStudents.value) {
if (selectedStudents.value.isEmpty()) {
isSelectionMode = false
}
}
Box(
modifier = modifier
.fillMaxSize()
.background(color = AppTheme.colors.white)
) {
AttendanceList(
groups = groups,
attendanceMap = attendanceMap,
listState = listState,
selectedStudents = selectedStudents,
isSelectionMode = isSelectionMode,
onStudentClicked = { studentId ->
if (!isSelectionMode && attendanceMap[studentId]?.type == AttendanceTypeView.ABSENT) {
expanded = studentId
}
},
onSelectionChanged = { isSelectionMode = it },
modifier = Modifier
.fillMaxSize()
.padding(bottom = if (isSelectionMode) 80.dp else 0.dp)
)
if (isSelectionMode) {
AttendanceSelection(
selectedStudents = selectedStudents.value,
onEvent = onEvent,
onSelectionCleared = {
isSelectionMode = false
selectedStudents.value = emptySet()
},
modifier = Modifier.align(Alignment.BottomCenter)
)
}
expanded?.let { studentId ->
AttendanceDialog(
studentId = studentId,
groups = groups,
onEvent = onEvent,
onDismiss = { expanded = null }
)
}
}
}

View File

@ -0,0 +1,77 @@
package org.example.presenceapp.ui.feature.attendance.composables
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.example.presenceapp.domain.entities.AbsenceReason
import org.example.presenceapp.domain.entities.Student
import org.example.presenceapp.ui.feature.attendance.AttendanceContract
import org.example.presenceapp.ui.feature.commons.CommonButton
import org.example.presenceapp.ui.feature.commons.CommonDialog
import org.example.presenceapp.ui.feature.commons.CommonMediumText
import org.example.presenceapp.ui.feature.commons.CommonRegularText
import org.example.presenceapp.ui.theme.AppTheme
@Composable
fun AttendanceDialog(
studentId: Int,
groups: List<Pair<String, List<Student>>>,
onEvent: (AttendanceContract.Event) -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier
) {
val student = groups
.flatMap { it.second }
.find { it.id == studentId }
if (student != null) {
CommonDialog(
expanded = true,
onDismiss = onDismiss,
content = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(16.dp)
) {
CommonMediumText(
text = "Выберите причину отсутствия",
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(20.dp))
AbsenceReason.entries.forEach { reason ->
CommonButton(
onClick = {
onEvent(AttendanceContract.Event.UpdateAbsenceReason(studentId, reason.name))
onDismiss()
},
content = {
CommonRegularText(
text = reason.toCustomString(),
color = AppTheme.colors.white,
modifier = Modifier
)
},
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
)
}
}
}
)
}
}

View File

@ -0,0 +1,24 @@
package org.example.presenceapp.ui.feature.attendance.composables
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.example.presenceapp.ui.feature.commons.CommonLabel
@Composable
fun AttendanceHeader(
title: String,
modifier: Modifier = Modifier
) {
CommonLabel(
text = title,
modifier = modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
.wrapContentWidth(Alignment.CenterHorizontally)
)
}

View File

@ -0,0 +1,76 @@
package org.example.presenceapp.ui.feature.attendance.composables
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.example.presenceapp.domain.entities.AttendanceView
import org.example.presenceapp.domain.entities.Student
import org.example.presenceapp.ui.feature.attendance.AttendanceScreenModel.Companion.toDisplayStatus
import org.example.presenceapp.ui.feature.commons.CommonRegularText
import org.example.presenceapp.ui.theme.AppTheme
@Composable
fun AttendanceItem(
student: Student,
attendance: AttendanceView?,
isSelected: Boolean,
isSelectionMode: Boolean,
onLongPress: () -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.fillMaxWidth()
.height(72.dp)
.pointerInput(Unit) {
detectTapGestures(
onLongPress = { onLongPress() },
onTap = { onClick() }
)
}
.border(
width = 1.dp,
color = if (isSelected) AppTheme.colors.black else AppTheme.colors.gray,
shape = RoundedCornerShape(16.dp)
)
.padding(12.dp)
) {
CommonRegularText(
text = student.fio,
color = AppTheme.colors.black,
modifier = Modifier
.weight(1f)
.padding(8.dp)
)
Box(
modifier = Modifier
.widthIn(min = 80.dp)
.wrapContentWidth(Alignment.CenterHorizontally)
.padding(8.dp)
) {
attendance?.type?.let {
CommonRegularText(
text = attendance.toDisplayStatus(),
color = AppTheme.colors.black,
textAlign = TextAlign.Center,
modifier = Modifier
)
}
}
}
}

View File

@ -0,0 +1,63 @@
package org.example.presenceapp.ui.feature.attendance.composables
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.example.presenceapp.domain.entities.AttendanceView
import org.example.presenceapp.domain.entities.Student
@Composable
fun AttendanceList(
groups: List<Pair<String, List<Student>>>,
attendanceMap: Map<Int, AttendanceView>,
listState: LazyListState,
selectedStudents: MutableState<Set<Int>>,
isSelectionMode: Boolean,
onStudentClicked: (Int) -> Unit,
onSelectionChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
state = listState,
verticalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(horizontal = 16.dp),
modifier = modifier
) {
groups.forEach { (title, students) ->
if (students.isNotEmpty()) {
item {
AttendanceHeader(title = title)
}
items(students, key = { student -> student.id }) { student ->
AttendanceItem(
student = student,
attendance = attendanceMap[student.id],
isSelected = selectedStudents.value.contains(student.id),
isSelectionMode = isSelectionMode,
onLongPress = {
onSelectionChanged(true)
selectedStudents.value = selectedStudents.value.toMutableSet().apply {
if (contains(student.id)) remove(student.id) else add(student.id)
}
},
onClick = {
if (isSelectionMode) {
selectedStudents.value = selectedStudents.value.toMutableSet().apply {
if (contains(student.id)) remove(student.id) else add(student.id)
}
} else {
onStudentClicked(student.id)
}
}
)
}
}
}
}
}

View File

@ -0,0 +1,52 @@
package org.example.presenceapp.ui.feature.attendance.composables
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.example.presenceapp.domain.entities.AttendanceTypeView
import org.example.presenceapp.ui.feature.attendance.AttendanceContract
import org.example.presenceapp.ui.feature.commons.CommonButton
import org.example.presenceapp.ui.feature.commons.CommonRegularText
import org.example.presenceapp.ui.theme.AppTheme
@Composable
fun AttendanceSelection(
selectedStudents: Set<Int>,
onEvent: (AttendanceContract.Event) -> Unit,
onSelectionCleared: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = modifier
.fillMaxWidth()
.background(color = AppTheme.colors.white)
.padding(vertical = 8.dp, horizontal = 16.dp)
.padding(bottom = 16.dp)
) {
listOf(
AttendanceTypeView.PRESENT to "присут",
AttendanceTypeView.ABSENT to "отсут"
).forEach { (status, label) ->
CommonButton(
onClick = {
onEvent(AttendanceContract.Event.UpdateAttendanceForSelected(selectedStudents, status))
onSelectionCleared()
},
content = {
CommonRegularText(
text = label,
color = AppTheme.colors.white,
modifier = Modifier
)
},
modifier = Modifier.weight(1f).padding(horizontal = 4.dp)
)
}
}
}

View File

@ -0,0 +1,19 @@
package org.example.presenceapp.ui.feature.attendance.composables
import org.example.presenceapp.domain.entities.AbsenceReason
fun AbsenceReason.toCustomString(): String {
return when (this) {
AbsenceReason.SKIP -> "отсут"
AbsenceReason.SICK -> "болезнь"
AbsenceReason.COMPETITION -> "соревнования"
}
}
fun String.toReadableString(): String {
return try {
AbsenceReason.valueOf(this).toCustomString()
} catch (e: IllegalArgumentException) {
this
}
}

View File

@ -0,0 +1,84 @@
package org.example.presenceapp.ui.feature.calendar
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import org.example.presenceapp.ui.feature.calendar.components.CalendarGrid
import org.example.presenceapp.ui.feature.calendar.components.CalendarMonth
import org.example.presenceapp.ui.feature.calendar.components.WeekDaysHeader
import org.example.presenceapp.ui.feature.commons.CommonBottomBar
import org.example.presenceapp.ui.feature.commons.CommonTopBar
import org.example.presenceapp.ui.theme.AppTheme
import org.example.presenceapp.ui.types.ScreenType
class CalendarScreen : Screen {
@Composable
override fun Content() {
val screenModel = remember { CalendarScreenModel() }
Calendar(screenModel = screenModel)
}
}
@Composable
fun Calendar(screenModel: CalendarScreenModel) {
val navigator = LocalNavigator.currentOrThrow
val viewModel = remember { screenModel }
val currentDate = remember { Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date }
val monthData by viewModel.monthData.collectAsState()
LaunchedEffect(currentDate) {
viewModel.loadMonthData(currentDate.year, currentDate.monthNumber)
}
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
CommonTopBar(
screenType = ScreenType.CALENDAR,
text = ""
)
},
bottomBar = { CommonBottomBar() }
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.background(color = AppTheme.colors.white)
.padding(padding)
.padding(horizontal = 16.dp)
) {
CalendarMonth(
month = currentDate.month,
year = currentDate.year,
modifier = Modifier.padding(vertical = 16.dp)
)
WeekDaysHeader(
modifier = Modifier
.padding(bottom = 8.dp)
)
CalendarGrid(
monthData = monthData,
currentDate = currentDate,
onDayClick = { date ->
viewModel.toggleAttendance(date)
}
)
}
}
}

View File

@ -0,0 +1,47 @@
package org.example.presenceapp.ui.feature.calendar
import cafe.adriel.voyager.core.model.ScreenModel
import kotlinx.coroutines.flow.*
import kotlinx.datetime.*
import org.example.presenceapp.domain.entities.AttendanceTypeView
import org.example.presenceapp.domain.entities.DayData
import org.example.presenceapp.ui.feature.calendar.components.CalendarUtils
class CalendarScreenModel: ScreenModel {
private val _monthData = MutableStateFlow<List<DayData>>(emptyList())
val monthData: StateFlow<List<DayData>> = _monthData.asStateFlow()
fun loadMonthData(year: Int, month: Int) {
_monthData.value = CalendarUtils.generateMonthData(
year = year,
month = month,
attendanceData = attendanceData()
)
}
fun toggleAttendance(date: LocalDate) {
val currentStatus = _monthData.value
.firstOrNull { it.date == date }
?.attendance
val newStatus = when (currentStatus) {
null -> AttendanceTypeView.ABSENT
AttendanceTypeView.ABSENT -> AttendanceTypeView.PRESENT
AttendanceTypeView.PRESENT -> null
}
_monthData.update { days ->
days.map { day ->
if (day.date == date) day.copy(attendance = newStatus)
else day
}
}
}
private fun attendanceData(): Map<LocalDate, AttendanceTypeView> {
return mapOf(
LocalDate(2025, 3, 7) to AttendanceTypeView.ABSENT,
LocalDate(2025, 3, 8) to AttendanceTypeView.PRESENT
)
}
}

View File

@ -0,0 +1,60 @@
package org.example.presenceapp.ui.feature.calendar.components
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import org.example.presenceapp.domain.entities.AttendanceTypeView
import org.example.presenceapp.domain.entities.DayData
import org.example.presenceapp.ui.theme.AppTheme
@Composable
fun DayCell(
day: DayData,
isToday: Boolean,
onClick: () -> Unit
) {
val borderColor = when {
isToday -> AppTheme.colors.black
!day.isCurrentMonth -> Color.Transparent
else -> when (day.attendance) {
AttendanceTypeView.PRESENT -> AppTheme.colors.gray
AttendanceTypeView.ABSENT -> AppTheme.colors.red
null -> AppTheme.colors.gray
}
}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.aspectRatio(1f)
.clip(RoundedCornerShape(10.dp))
.border(1.dp, borderColor, RoundedCornerShape(10.dp))
.clickable(
enabled = day.isCurrentMonth,
onClick = onClick
)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = day.date.dayOfMonth.toString(),
color = when {
!day.isCurrentMonth -> AppTheme.colors.black.copy(alpha = 0.3f)
else -> AppTheme.colors.black
},
style = AppTheme.typography.message
)
}
}
}

View File

@ -0,0 +1,31 @@
package org.example.presenceapp.ui.feature.calendar.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import kotlinx.datetime.LocalDate
import org.example.presenceapp.domain.entities.DayData
@Composable
fun CalendarGrid(
monthData: List<DayData>,
currentDate: LocalDate,
onDayClick: (LocalDate) -> Unit
) {
LazyVerticalGrid(
columns = GridCells.Fixed(7),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(monthData) { day ->
DayCell(
day = day,
isToday = day.date == currentDate,
onClick = { onDayClick(day.date) }
)
}
}
}

View File

@ -0,0 +1,20 @@
package org.example.presenceapp.ui.feature.calendar.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import kotlinx.datetime.Month
import org.example.presenceapp.ui.feature.commons.CommonMainText
@Composable
fun CalendarMonth(
month: Month,
year: Int,
modifier: Modifier
) {
CommonMainText(
text = "${month.name.lowercase().replaceFirstChar { it.titlecase() }} $year",
textAlign = TextAlign.Center,
modifier = modifier
)
}

View File

@ -0,0 +1,35 @@
package org.example.presenceapp.ui.feature.calendar.components
import kotlinx.datetime.*
import org.example.presenceapp.domain.entities.AttendanceTypeView
import org.example.presenceapp.domain.entities.DayData
import kotlin.sequences.generateSequence
object CalendarUtils {
fun generateMonthData(
year: Int,
month: Int,
attendanceData: Map<LocalDate, AttendanceTypeView>
): List<DayData> {
val firstDay = LocalDate(year, month, 1)
val lastDay = firstDay.plus(DatePeriod(months = 1)).minus(DatePeriod(days = 1))
val startDate = firstDay.minus(DatePeriod(days = firstDay.dayOfWeek.isoDayNumber - 1))
val endDate = lastDay.plus(DatePeriod(days = 7 - lastDay.dayOfWeek.isoDayNumber))
return generateSequence(startDate) { it.plus(1, DateTimeUnit.DAY) }
.takeWhile { it <= endDate }
.map { date ->
DayData(
date = date,
isCurrentMonth = date.monthNumber == month,
attendance = attendanceData[date]
)
}
.toList()
}
private fun isLeapYear(year: Int): Boolean {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
}

View File

@ -0,0 +1,29 @@
package org.example.presenceapp.ui.feature.calendar.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun WeekDaysHeader(modifier: Modifier = Modifier) {
val daysOfWeek = listOf("Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс")
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
daysOfWeek.forEach { day ->
Text(
text = day,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
modifier = Modifier.weight(1f).wrapContentWidth()
)
}
}
}

View File

@ -0,0 +1,101 @@
package org.example.presenceapp.ui.feature.commons
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemColors
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.presenceapp.composeapp.resources.Res
import com.presenceapp.composeapp.resources.info
import com.presenceapp.composeapp.resources.schedule
import com.presenceapp.composeapp.resources.settings
import org.example.presenceapp.domain.entities.Schedule
import org.example.presenceapp.ui.feature.info.InfoScreen
import org.example.presenceapp.ui.feature.settings.SettingsScreen
import org.example.presenceapp.ui.theme.AppTheme
import org.example.project.ui.weeks.WeeksScreen
import org.jetbrains.compose.resources.painterResource
@Composable
fun CommonBottomBar() {
val navigator = LocalNavigator.currentOrThrow
val currentScreen = navigator.lastItem
val lessons = emptyList<Schedule>()
val routes = listOf(
Triple(Res.drawable.info, "Информация", InfoScreen()),
Triple(Res.drawable.schedule, "Расписание", WeeksScreen(lessons = lessons)),
Triple(Res.drawable.settings, "Настройки", SettingsScreen())
)
NavigationBar(
containerColor = AppTheme.colors.black,
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
) {
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
.fillMaxWidth()
) {
routes.forEach { (icon, label, screen) ->
val isSelected = currentScreen?.key == screen.key
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.weight(1f)
.clickable {
if (currentScreen?.key != screen.key) {
navigator.push(screen)
}
}
) {
this@Row.NavigationBarItem(
selected = isSelected,
onClick = {
if (currentScreen?.key != screen.key) {
navigator.push(screen)
}
},
icon = {
Icon(
painter = painterResource(icon),
contentDescription = null,
modifier = Modifier
.size(24.dp)
)
},
colors = NavigationBarItemColors(
selectedIndicatorColor = AppTheme.colors.white,
selectedIconColor = AppTheme.colors.black,
selectedTextColor = AppTheme.colors.white,
unselectedIconColor = AppTheme.colors.white,
unselectedTextColor = AppTheme.colors.white,
disabledIconColor = AppTheme.colors.white,
disabledTextColor = AppTheme.colors.white,
)
)
Text(
text = label,
color = AppTheme.colors.white,
style = AppTheme.typography.bottomBar
)
}
}
}
}
}

View File

@ -0,0 +1,25 @@
package org.example.presenceapp.ui.feature.commons
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import org.example.presenceapp.ui.theme.AppTheme
@Composable
fun CommonButton(
onClick: () -> Unit,
content: @Composable () -> Unit,
modifier: Modifier
) {
Button(
colors = ButtonDefaults.buttonColors(
containerColor = AppTheme.colors.black,
contentColor = AppTheme.colors.white
),
onClick = onClick,
modifier = modifier
) {
content()
}
}

View File

@ -0,0 +1,22 @@
package org.example.presenceapp.ui.feature.commons
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import org.example.presenceapp.ui.theme.AppTheme
@Composable
fun CommonDataText(
text: String,
textAlign: TextAlign? = null,
modifier: Modifier
) {
Text(
text = text,
color = AppTheme.colors.black,
style = AppTheme.typography.data,
textAlign = textAlign,
modifier = modifier
)
}

View File

@ -0,0 +1,23 @@
package org.example.presenceapp.ui.feature.commons
import androidx.compose.material3.AlertDialog
import androidx.compose.runtime.Composable
import androidx.compose.ui.window.DialogProperties
import org.example.presenceapp.ui.theme.AppTheme
@Composable
fun CommonDialog(
expanded: Boolean,
onDismiss: () -> Unit,
content: @Composable () -> Unit
) {
if (expanded) {
AlertDialog(
containerColor = AppTheme.colors.textField,
onDismissRequest = { onDismiss() },
properties = DialogProperties(dismissOnClickOutside = true),
confirmButton = {},
text = content
)
}
}

View File

@ -0,0 +1,56 @@
package org.example.presenceapp.ui.feature.commons
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.dp
import org.example.presenceapp.ui.types.ButtonType
@Composable
fun CommonIconButton(
background: Color,
icon: Painter,
switchedIcon: Painter? = null,
iconColor: Color,
buttonType: ButtonType,
selectedIconState: State<Boolean>? = null,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val currentIcon = when {
buttonType == ButtonType.SWITCHABLE && selectedIconState != null -> {
if (selectedIconState.value) icon else switchedIcon ?: icon
}
else -> icon
}
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.size(44.dp)
.clip(CircleShape)
.background(background)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) { onClick() }
) {
Icon(
painter = currentIcon,
contentDescription = null,
tint = iconColor
)
}
}

View File

@ -0,0 +1,22 @@
package org.example.presenceapp.ui.feature.commons
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import org.example.presenceapp.ui.theme.AppTheme
@Composable
fun CommonLabel(
text: String,
textAlign: TextAlign? = null,
modifier: Modifier
) {
Text(
text = text,
color = AppTheme.colors.black,
style = AppTheme.typography.name,
textAlign = textAlign,
modifier = modifier
)
}

View File

@ -0,0 +1,24 @@
package org.example.presenceapp.ui.feature.commons
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.example.presenceapp.ui.theme.AppTheme
@Composable
fun CommonMainText(
text: String,
textAlign: TextAlign? = null,
modifier: Modifier
) {
Text(
text = text,
color = AppTheme.colors.black,
style = AppTheme.typography.main,
textAlign = textAlign,
modifier = modifier
)
}

View File

@ -0,0 +1,22 @@
package org.example.presenceapp.ui.feature.commons
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import org.example.presenceapp.ui.theme.AppTheme
@Composable
fun CommonMediumText(
text: String,
textAlign: TextAlign? = null,
modifier: Modifier
) {
Text(
text = text,
color = AppTheme.colors.black,
style = AppTheme.typography.messageFrag,
textAlign = textAlign,
modifier = modifier
)
}

View File

@ -0,0 +1,24 @@
package org.example.presenceapp.ui.feature.commons
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import org.example.presenceapp.ui.theme.AppTheme
@Composable
fun CommonRegularText(
text: String,
color: Color,
textAlign: TextAlign? = null,
modifier: Modifier
) {
Text(
text = text,
color = color,
style = AppTheme.typography.message,
textAlign = textAlign,
modifier = modifier
)
}

View File

@ -0,0 +1,111 @@
package org.example.presenceapp.ui.feature.commons
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.presenceapp.composeapp.resources.Res
import com.presenceapp.composeapp.resources.arrow_back
import com.presenceapp.composeapp.resources.is_here
import com.presenceapp.composeapp.resources.isnt_here
import com.presenceapp.composeapp.resources.settings
import org.example.presenceapp.domain.entities.AttendanceTypeView
import org.example.presenceapp.ui.feature.calendar.CalendarScreen
import org.example.presenceapp.ui.theme.AppTheme
import org.example.presenceapp.ui.types.ButtonType
import org.example.presenceapp.ui.types.ScreenType
import org.jetbrains.compose.resources.painterResource
@Composable
fun CommonTopBar(
screenType: ScreenType,
text: String,
onChangeSortType: ((AttendanceTypeView) -> Unit)? = null
) {
val navigator = LocalNavigator.currentOrThrow
val selectedIconState = remember { mutableStateOf(false) }
if (screenType == ScreenType.WEEKS || screenType == ScreenType.CALENDAR) {
Row(
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.background(AppTheme.colors.white)
.padding(horizontal = 16.dp)
.padding(top = 16.dp)
.height(75.dp)
) {
CommonIconButton(
background = AppTheme.colors.black,
icon = painterResource(Res.drawable.settings),
iconColor = AppTheme.colors.white,
buttonType = ButtonType.notSWITCHABLE,
onClick = {
if (screenType == ScreenType.WEEKS) { navigator.push(CalendarScreen()) }
else { navigator.pop() }
},
modifier = Modifier
)
}
}
else {
Row(
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.background(AppTheme.colors.white)
.padding(horizontal = 16.dp)
.padding(top = 16.dp)
.height(75.dp)
) {
CommonIconButton(
background = AppTheme.colors.black,
icon = painterResource(Res.drawable.arrow_back),
iconColor = AppTheme.colors.white,
buttonType = ButtonType.notSWITCHABLE,
onClick = { navigator.pop() },
modifier = Modifier
)
CommonLabel(
text = text,
textAlign = TextAlign.Center,
modifier = Modifier
.weight(1f)
.padding(end = if (screenType == ScreenType.GROUP) 0.dp else 44.dp)
)
if (screenType == ScreenType.GROUP) {
val newSortType = when (selectedIconState.value) {
false -> AttendanceTypeView.ABSENT
true -> AttendanceTypeView.PRESENT
}
CommonIconButton(
background = AppTheme.colors.black,
icon = painterResource(Res.drawable.isnt_here),
switchedIcon = painterResource(Res.drawable.is_here),
iconColor = AppTheme.colors.white,
buttonType = ButtonType.SWITCHABLE,
selectedIconState = selectedIconState,
onClick = {
selectedIconState.value = !selectedIconState.value
onChangeSortType?.invoke(newSortType)
},
modifier = Modifier
)
}
}
}
}

View File

@ -0,0 +1,36 @@
package org.example.presenceapp.ui.feature.commons
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
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.unit.dp
import androidx.compose.ui.window.Dialog
@Composable
fun ErrorDialog(
onDismiss: ()-> Unit,
text: String
){
Dialog(
onDismissRequest = { onDismiss() }
){
Column(
modifier = Modifier.fillMaxWidth().wrapContentHeight().background(Color.White, RoundedCornerShape(10.dp)).padding(10.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text
)
}
}
}

View File

@ -0,0 +1,71 @@
package org.example.presenceapp.ui.feature.info
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import org.example.presenceapp.ui.feature.commons.CommonBottomBar
import org.example.presenceapp.ui.feature.info.components.InfoCard
import org.example.presenceapp.ui.theme.AppTheme
class InfoScreen(): Screen {
@Composable
override fun Content() {
val viewModel = rememberScreenModel { InfoScreenModel() }
Info(viewModel)
}
@Composable
fun Info(viewModel: InfoScreenModel) {
Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = { CommonBottomBar() }
) { padding ->
Column(
modifier = Modifier.fillMaxSize().background(AppTheme.colors.white).padding(32.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
){
Text(
"Информация",
modifier = Modifier.padding(top = 10.dp),
color = AppTheme.colors.black,
style = AppTheme.typography.main
)
}
Column(
modifier = Modifier.fillMaxWidth().padding(top = 43.dp)
) {
viewModel.user.forEach {
InfoCard(
onClick = {},
text = it.toString()
)
Spacer(Modifier.height(10.dp))
}
}
}
}
}
}

View File

@ -0,0 +1,25 @@
package org.example.presenceapp.ui.feature.info
import cafe.adriel.voyager.core.model.ScreenModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.example.presenceapp.domain.entities.UserInfo
import org.example.presenceapp.domain.entities.UserResponse
class InfoScreenModel: ScreenModel {
val state = MutableStateFlow(org.example.presenceapp.ui.feature.info.InfoScreenState())
fun getUserInfo(userResponse: UserResponse?){
state.update {
it.copy(
userInfo = userResponse
)
}
}
val user = listOf(
UserInfo.userGroup,
UserInfo.userName,
UserInfo.userRole
)
}

View File

@ -0,0 +1,7 @@
package org.example.presenceapp.ui.feature.info
import org.example.presenceapp.domain.entities.UserResponse
data class InfoScreenState(
val userInfo: UserResponse? = null
)

View File

@ -0,0 +1,46 @@
package org.example.presenceapp.ui.feature.info.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
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.unit.dp
import org.example.presenceapp.ui.theme.AppTheme
@Composable
fun InfoCard(
onClick: () -> Unit,
text: String
) {
Card(
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
border = BorderStroke(1.dp, AppTheme.colors.gray),
modifier = Modifier
.fillMaxWidth()
.background(AppTheme.colors.white)
.clickable { onClick() }
) {
Row(
modifier = Modifier
.padding(16.dp)
) {
Text(
text = text,
color = AppTheme.colors.black,
style = AppTheme.typography.name,
modifier = Modifier
.padding(start = 5.dp, end = 21.dp)
.align(Alignment.CenterVertically)
)
}
}
}

View File

@ -0,0 +1,116 @@
package org.example.presenceapp.ui.feature.login
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.koinScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import org.example.presenceapp.ui.feature.commons.ErrorDialog
import org.example.presenceapp.ui.feature.login.components.LoginButton
import org.example.presenceapp.ui.feature.login.components.LoginCheckBox
import org.example.presenceapp.ui.feature.login.components.LoginTextField
import org.example.presenceapp.ui.theme.AppTheme
import org.example.project.ui.weeks.WeeksScreen
class LoginScreen : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val screenModel: org.example.presenceapp.ui.feature.login.LoginScreenModel = koinScreenModel()
val state by screenModel.state.collectAsState()
val effect = screenModel.effect
LaunchedEffect(Unit) {
effect.collect { loginEffect ->
when (loginEffect) {
is LoginEffect.ShowToast -> {
println("TOAST: ${loginEffect.message}")
}
is LoginEffect.NavigateToWeeks -> {
navigator.push(WeeksScreen(loginEffect.lessons))
}
}
}
}
Login(screenModel, state)
}
@Composable
fun Login(
screenModel: org.example.presenceapp.ui.feature.login.LoginScreenModel,
state: LoginScreenState
) {
Column(
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.background(AppTheme.colors.white)
.padding(horizontal = 32.dp)
) {
state.error?.let {
ErrorDialog(
onDismiss = { screenModel.onEvent(LoginEvent.ResetError) },
text = it
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(top = 142.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
"Добро пожаловать!",
color = AppTheme.colors.black,
textAlign = TextAlign.Center,
style = AppTheme.typography.main
)
LoginTextField(
value = state.login,
onValue = { screenModel.onEvent(LoginEvent.EnterLogin(it)) },
placeholder = "xyz",
text = "Логин",
top = 145
)
LoginTextField(
value = state.password,
onValue = { screenModel.onEvent(LoginEvent.EnterPassword(it)) },
placeholder = "********",
text = "Пароль",
top = 18
)
LoginCheckBox(
check = state.check,
onCheck = { screenModel.onEvent(LoginEvent.ToggleCheck) },
top = 24
)
}
LoginButton(
text = "Войти",
onClick = { screenModel.onEvent(LoginEvent.SubmitLogin) },
bottom = 80
)
}
}
}

View File

@ -0,0 +1,100 @@
package org.example.presenceapp.ui.feature.login
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.example.presenceapp.domain.command.schedule.GroupCommand
import org.example.presenceapp.domain.command.LoginCommand
import org.example.presenceapp.domain.entities.UserInfo
import org.example.presenceapp.domain.repo.LoginRepository
import org.example.presenceapp.domain.repo.ScheduleRepository
class LoginScreenModel(
private val loginRepository: LoginRepository,
private val scheduleRepository: ScheduleRepository
) : ScreenModel {
private val _state = MutableStateFlow(_root_ide_package_.org.example.presenceapp.ui.feature.login.LoginScreenState())
val state: StateFlow<org.example.presenceapp.ui.feature.login.LoginScreenState> = _state
private val _effect = MutableSharedFlow<org.example.presenceapp.ui.feature.login.LoginEffect>()
val effect: SharedFlow<org.example.presenceapp.ui.feature.login.LoginEffect> = _effect
fun onEvent(event: org.example.presenceapp.ui.feature.login.LoginEvent) {
when (event) {
is org.example.presenceapp.ui.feature.login.LoginEvent.EnterLogin -> {
_state.update { it.copy(login = event.value) }
}
is _root_ide_package_.org.example.presenceapp.ui.feature.login.LoginEvent.EnterPassword -> {
_state.update { it.copy(password = event.value) }
}
is _root_ide_package_.org.example.presenceapp.ui.feature.login.LoginEvent.ToggleCheck -> {
_state.update { it.copy(check = !it.check) }
}
is _root_ide_package_.org.example.presenceapp.ui.feature.login.LoginEvent.ResetError -> {
_state.update { it.copy(error = null) }
}
is _root_ide_package_.org.example.presenceapp.ui.feature.login.LoginEvent.SubmitLogin -> {
login()
}
}
}
private fun login() {
val currentState = _state.value
val loginCommand = LoginCommand(currentState.login, currentState.password)
screenModelScope.launch {
try {
val loginResponse = loginRepository.login(loginCommand)
val userResponse = loginResponse.user
val groupId = userResponse.responsible.first().group.id
UserInfo.userGroup = userResponse.responsible.first().group.name
UserInfo.userName = userResponse.fio
UserInfo.userRole = userResponse.role.name
getSchedule(groupId)
_state.update {
it.copy(userInfo = userResponse)
}
} catch (e: Exception) {
_state.update { it.copy(error = e.message) }
_effect.emit(
_root_ide_package_.org.example.presenceapp.ui.feature.login.LoginEffect.ShowToast(
e.message ?: "Неизвестная ошибка"
)
)
}
}
}
private suspend fun getSchedule(groupId: Int) {
try {
val scheduleList = scheduleRepository.getSchedule(GroupCommand(groupId))
_state.update { it.copy(lessonsList = scheduleList) }
_effect.emit(
_root_ide_package_.org.example.presenceapp.ui.feature.login.LoginEffect.NavigateToWeeks(
scheduleList
)
)
} catch (e: Exception) {
_effect.emit(
_root_ide_package_.org.example.presenceapp.ui.feature.login.LoginEffect.ShowToast(
e.message ?: "Ошибка"
)
)
}
}
}

View File

@ -0,0 +1,32 @@
package org.example.presenceapp.ui.feature.login
import org.example.presenceapp.domain.entities.AttendanceView
import org.example.presenceapp.domain.entities.Schedule
import org.example.presenceapp.domain.entities.Student
import org.example.presenceapp.domain.entities.UserResponse
sealed interface LoginEvent {
data class EnterLogin(val value: String) : LoginEvent
data class EnterPassword(val value: String) : LoginEvent
object ToggleCheck : LoginEvent
object SubmitLogin : LoginEvent
object ResetError : LoginEvent
}
sealed interface LoginEffect {
data class ShowToast(val message: String) : LoginEffect
data class NavigateToWeeks(val lessons: List<Schedule>) : LoginEffect
}
data class LoginScreenState(
val login: String = "",
val password: String = "",
val error: String? = null,
val success: Boolean = false,
val getAllData: Boolean = false,
val check: Boolean = false,
val userInfo: UserResponse? = null,
val lessonsList: List<Schedule> = emptyList(),
val groupList: List<Student> = emptyList(),
val groupPresence: List<AttendanceView> = emptyList()
)

View File

@ -0,0 +1,36 @@
package org.example.presenceapp.ui.feature.login.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.example.presenceapp.ui.theme.AppTheme
@Composable
fun LoginButton(
text: String,
onClick: () -> Unit,
bottom: Int
){
Button(
modifier = Modifier.padding(bottom = bottom.dp).fillMaxWidth(),
onClick = { onClick() },
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = AppTheme.colors.black,
contentColor = AppTheme.colors.textField,
disabledContentColor = AppTheme.colors.gray,
disabledContainerColor =AppTheme.colors.textField
)
){
Text(
text,
style = AppTheme.typography.message
)
}
}

View File

@ -0,0 +1,50 @@
package org.example.presenceapp.ui.feature.login.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults
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.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.example.presenceapp.ui.feature.commons.CommonRegularText
import org.example.presenceapp.ui.theme.AppTheme
@Composable
fun LoginCheckBox(
check: Boolean,
onCheck: (Boolean) -> Unit,
top: Int
){
Row(
modifier = Modifier.padding(top = top.dp).fillMaxWidth().height(44.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = check,
onCheckedChange = onCheck,
modifier = Modifier.size(18.dp),
colors = CheckboxDefaults.colors(
checkedColor = AppTheme.colors.black,
checkmarkColor = AppTheme.colors.white,
uncheckedColor = AppTheme.colors.black
)
)
CommonRegularText(
text = "Пользовательское\nсоглашение",
color = AppTheme.colors.black,
textAlign = TextAlign.Start,
modifier = Modifier
.padding(start = 16.dp)
)
}
}

Some files were not shown because too many files have changed in this diff Show More