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.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.serialization.json.Json 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") 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") 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) val json = Json.encodeToString(map)
context.attendanceDataStore.edit { prefs -> context.attendanceDataStore.edit { prefs ->
prefs[ATTENDANCE_KEY] = json prefs[ATTENDANCE_KEY] = json
} }
} }
override fun attendanceMapFlow(): Flow<Map<String, Attendance>> { override fun attendanceMapFlow(): Flow<Map<Int, AttendanceView>> {
return context.attendanceDataStore.data.map { prefs -> return context.attendanceDataStore.data.map { prefs ->
prefs[ATTENDANCE_KEY]?.let { prefs[ATTENDANCE_KEY]?.let {
Json.decodeFromString<Map<String, Attendance>>(it) Json.decodeFromString<Map<Int, AttendanceView>>(it)
} ?: emptyMap() } ?: 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.Context
import android.content.SharedPreferences import android.content.SharedPreferences

View File

@ -2,9 +2,8 @@ package org.example.presenceapp
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import cafe.adriel.voyager.navigator.Navigator 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.presenceapp.ui.theme.AppTheme
import org.example.project.ui.login.LoginScreen
import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable @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.AttendanceRequestDto
import org.example.presenceapp.data.common.dto.attendance.AttendanceResponseDto 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.PresettingRequestDto
import org.example.presenceapp.data.common.dto.attendance.PresettingResponseDto import org.example.presenceapp.data.common.dto.attendance.PresettingResponseDto
import org.example.presenceapp.data.common.dto.auth.AuthRequestDto 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.ResponsibleTypeDto
import org.example.presenceapp.data.common.dto.auth.RoleResponseDto 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.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.ScheduleRequestDto
import org.example.presenceapp.data.common.dto.schedule.ScheduleResponseDto import org.example.presenceapp.data.common.dto.schedule.ScheduleResponseDto
import org.example.presenceapp.data.common.dto.schedule.SubjectResponseDto 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.AddAttendanceCommand
import org.example.presenceapp.domain.command.attendance.AddPresettingCommand import org.example.presenceapp.domain.command.attendance.AddPresettingCommand
import org.example.presenceapp.domain.command.GroupCommand import org.example.presenceapp.domain.command.schedule.GetStudentsByGroupIdCommand
import org.example.presenceapp.domain.command.LoginCommand 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.Attendance
import org.example.presenceapp.domain.entities.AttendanceType 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.GroupResponse
import org.example.presenceapp.domain.entities.LoginResponse import org.example.presenceapp.domain.entities.LoginResponse
import org.example.presenceapp.domain.entities.Presetting 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.ResponsibleType
import org.example.presenceapp.domain.entities.RoleResponse import org.example.presenceapp.domain.entities.RoleResponse
import org.example.presenceapp.domain.entities.Schedule 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.Subject
import org.example.presenceapp.domain.entities.UserResponse import org.example.presenceapp.domain.entities.UserResponse
@ -37,6 +45,10 @@ fun LoginCommand.toDto(): AuthRequestDto = AuthRequestDto(
fun GroupCommand.toDto(): ScheduleRequestDto = ScheduleRequestDto(groupId) fun GroupCommand.toDto(): ScheduleRequestDto = ScheduleRequestDto(groupId)
fun GetStudentsByGroupIdCommand.toDto(): StudentRequestDto = StudentRequestDto(
id = groupId
)
fun AddAttendanceCommand.toDto(): AttendanceRequestDto = AttendanceRequestDto( fun AddAttendanceCommand.toDto(): AttendanceRequestDto = AttendanceRequestDto(
studentId = studentId, studentId = studentId,
scheduleId = scheduleId, scheduleId = scheduleId,
@ -50,6 +62,27 @@ fun AddPresettingCommand.toDto(): PresettingRequestDto = PresettingRequestDto(
endAt = endAt 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( fun ScheduleResponseDto.toEntity(): Schedule = Schedule(
@ -60,6 +93,15 @@ fun ScheduleResponseDto.toEntity(): Schedule = Schedule(
id = id id = id
) )
fun StudentResponseDto.toEntity(): Student = Student(
id = studentId,
uuid = uuid,
fio = fio,
role = "",
enrollDate = enrollDate,
expulsionDate = expulsionDate
)
fun SubjectResponseDto.toEntity(): Subject = Subject( fun SubjectResponseDto.toEntity(): Subject = Subject(
id = id, id = id,
name = name name = name
@ -78,22 +120,18 @@ fun UserResponseDto.toEntity(): UserResponse = UserResponse(
role = role.toEntity(), role = role.toEntity(),
responsible = responsible.map { it.toEntity() } responsible = responsible.map { it.toEntity() }
) )
fun ResponsibleDto.toEntity(): Responsible = Responsible( fun ResponsibleDto.toEntity(): Responsible = Responsible(
group = group.toEntity(), group = group.toEntity(),
responsibleType = responsibleType.toEntity() responsibleType = responsibleType.toEntity()
) )
fun GroupDto.toEntity(): GroupResponse = GroupResponse( fun GroupDto.toEntity(): GroupResponse = GroupResponse(
id = id, id = id,
name = name name = name
) )
fun ResponsibleTypeDto.toEntity(): ResponsibleType = ResponsibleType( fun ResponsibleTypeDto.toEntity(): ResponsibleType = ResponsibleType(
id = id, id = id,
name = name name = name
) )
fun RoleResponseDto.toEntity(): RoleResponse = RoleResponse( fun RoleResponseDto.toEntity(): RoleResponse = RoleResponse(
id = id, id = id,
name = name name = name
@ -107,6 +145,11 @@ fun AttendanceResponseDto.toEntity(): Attendance = Attendance(
type = attendanceTypeId type = attendanceTypeId
) )
fun AttendanceTypeResponseDto.toAttendanceType(): AttendanceType = AttendanceType(
id = id,
name = name
)
fun AttendanceResponseDto.toAttendanceType(): AttendanceType = AttendanceType( fun AttendanceResponseDto.toAttendanceType(): AttendanceType = AttendanceType(
id = attendanceTypeId, id = attendanceTypeId,
name = "" name = ""
@ -117,4 +160,27 @@ fun PresettingResponseDto.toEntity(): Presetting = Presetting(
studentId = studentId, studentId = studentId,
startAt = startAt, startAt = startAt,
endAt = endAt 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

@ -9,11 +9,4 @@ data class AttendanceRequestDto(
val scheduleId: Int, val scheduleId: Int,
val attendanceTypeId: Int, val attendanceTypeId: Int,
val attendanceDate: LocalDate 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 @Serializable
data class AttendanceTypeResponseDto( data class AttendanceTypeResponseDto(
val id: Int, val id: Int,
val name: String, val name: String
)
@Serializable
data class PresettingResponseDto(
val id: Int,
val attendanceType: AttendanceResponseDto,
val studentId: Int,
val startAt: LocalDate,
val endAt: LocalDate,
) )

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

@ -5,4 +5,4 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class ScheduleRequestDto( data class ScheduleRequestDto(
val groupId: Int val groupId: Int
) )

View File

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

View File

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

View File

@ -12,14 +12,14 @@ import org.example.presenceapp.domain.command.attendance.AddPresettingCommand
interface AttendanceApi { interface AttendanceApi {
@POST("api/v1/presence") @POST("api/v1/presence")
suspend fun addAttendance(@Body commands: List<AttendanceRequestDto>): List<AttendanceResponseDto> suspend fun addAttendance(@Body command: List<AttendanceRequestDto>): List<AttendanceResponseDto>
@GET("api/v1/presence/dictionary/attendance_type")
suspend fun getAttendanceTypes(): List<AttendanceTypeResponseDto>
@GET("api/v1/presence/{groupId}") @GET("api/v1/presence/{groupId}")
suspend fun getAttendance(@Path("groupId") groupId: Int): List<AttendanceResponseDto> 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") @POST("api/v1/presence/presetting")
suspend fun addPresetting(@Body command: AddPresettingCommand): Boolean 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.attendance.AttendanceResponseDto
import org.example.presenceapp.data.common.dto.group.StudentResponseDto import org.example.presenceapp.data.common.dto.group.StudentResponseDto
import org.example.presenceapp.data.common.dto.schedule.ScheduleResponseDto import org.example.presenceapp.data.common.dto.schedule.ScheduleResponseDto
import org.example.presenceapp.domain.entities.Group
interface GroupApi { interface GroupApi {
@GET("api/v1/group/{id}/schedule") @GET("api/v1/group/{id}/schedule")
suspend fun getSchedule(@Path id: Int): List<ScheduleResponseDto> suspend fun getSchedule(@Path id: Int): List<ScheduleResponseDto>
@GET("api/v1/group/{id}/students") @GET("api/v1/group/{id}/students")
suspend fun getStudents(@Path id: Int): List<StudentResponseDto> suspend fun getStudentsByGroupId(@Path id: Int): List<StudentResponseDto>
@GET("api/v1/group/{id}/presence")
suspend fun getPresence(@Path id: Int): List<AttendanceResponseDto>
} }

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

View File

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

View File

@ -3,35 +3,33 @@ package org.example.presenceapp.data.repository
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map 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.common.toEntity
import org.example.presenceapp.data.local.LocalDataSource import org.example.presenceapp.data.local.LocalDataSource
import org.example.presenceapp.data.remote.impl.AttendanceApiImpl import org.example.presenceapp.data.remote.impl.AttendanceApiImpl
import org.example.presenceapp.domain.command.attendance.AddAttendanceCommand import org.example.presenceapp.domain.command.attendance.AddAttendanceCommand
import org.example.presenceapp.domain.command.attendance.AddPresettingCommand import org.example.presenceapp.domain.command.attendance.AddPresettingCommand
import org.example.presenceapp.domain.command.attendance.GetAttendanceCommand 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.command.attendance.GetPresettingCommand
import org.example.presenceapp.domain.entities.Attendance 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.entities.Presetting
import org.example.presenceapp.domain.repo.attendance.AttendanceRepository import org.example.presenceapp.domain.repo.AttendanceRepository
import org.example.presenceapp.domain.repo.attendance.PresettingRepository
class AttendanceNetRepository( class AttendanceNetRepository(
private val localDataSource: LocalDataSource, private val localDataSource: LocalDataSource,
private val attendanceApiImpl: AttendanceApiImpl private val attendanceApiImpl: AttendanceApiImpl
): AttendanceRepository, PresettingRepository { ): AttendanceRepository {
suspend fun saveAttendanceLocally(attendance: Map<String, Attendance>) { override suspend fun saveAttendanceLocally(attendance: Map<Int, AttendanceView>) {
localDataSource.saveAttendance(attendance) localDataSource.saveAttendance(attendance)
} }
fun observeLocalAttendance(): Flow<Map<String, Attendance>> { override fun observeLocalAttendance(): Flow<Map<Int, AttendanceView>> {
return localDataSource.observeAttendance() return localDataSource.observeAttendance()
.map { attendanceMap -> .map { attendanceMap -> attendanceMap }
attendanceMap .catch { e -> emit(emptyMap()) }
}
.catch { e ->
emit(emptyMap())
}
} }
override suspend fun addAttendance(addAttendanceCommand: AddAttendanceCommand): List<Attendance> { override suspend fun addAttendance(addAttendanceCommand: AddAttendanceCommand): List<Attendance> {
@ -44,6 +42,11 @@ class AttendanceNetRepository(
return result.map { it.toEntity() } 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 { override suspend fun addPresetting(addPresettingCommand: AddPresettingCommand): Boolean {
return attendanceApiImpl.addPresetting(addPresettingCommand) 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.toDto
import org.example.presenceapp.data.common.toEntity import org.example.presenceapp.data.common.toEntity
import org.example.presenceapp.data.remote.impl.ScheduleApiImpl 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.Schedule
import org.example.presenceapp.domain.entities.Student
import org.example.presenceapp.domain.repo.ScheduleRepository import org.example.presenceapp.domain.repo.ScheduleRepository
class ScheduleNetRepository( class ScheduleNetRepository(
@ -14,4 +16,10 @@ class ScheduleNetRepository(
val result = scheduleApiImpl.getSchedule(groupCommand.toDto()) val result = scheduleApiImpl.getSchedule(groupCommand.toDto())
return result.map { it.toEntity() } 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 package org.example.presenceapp.data.repository.settings
import org.example.presenceapp.domain.entities.AttendanceType import org.example.presenceapp.domain.entities.AttendanceTypeView
interface SettingsRepository { interface SettingsRepository {
suspend fun getDefaultAttendanceStatus(): AttendanceType suspend fun getDefaultAttendanceStatus(): AttendanceTypeView
suspend fun setDefaultAttendanceStatus(type: AttendanceType) suspend fun setDefaultAttendanceStatus(type: AttendanceTypeView)
} }

View File

@ -1,21 +1,21 @@
package org.example.presenceapp.data.repository.settings 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.data.local.storage.SettingsStorage
import org.example.presenceapp.domain.entities.AttendanceType
class SettingsRepositoryImpl( class SettingsRepositoryImpl(
private val settingsStorage: SettingsStorage private val settingsStorage: SettingsStorage
) : SettingsRepository { ) : SettingsRepository {
override suspend fun getDefaultAttendanceStatus(): AttendanceType { override suspend fun getDefaultAttendanceStatus(): AttendanceTypeView {
val statusString = settingsStorage.get( val statusString = settingsStorage.get(
key = "default_attendance_status", key = "default_attendance_status",
defaultValue = AttendanceType.ABSENT.name defaultValue = AttendanceTypeView.ABSENT.name
) )
return enumValueOf(statusString) return enumValueOf(statusString)
} }
override suspend fun setDefaultAttendanceStatus(type: AttendanceType) { override suspend fun setDefaultAttendanceStatus(type: AttendanceTypeView) {
settingsStorage.put( settingsStorage.put(
key = "default_attendance_status", key = "default_attendance_status",
value = type.name 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.LoginRepository
import org.example.presenceapp.domain.repo.ScheduleRepository import org.example.presenceapp.domain.repo.ScheduleRepository
import org.example.presenceapp.domain.usecases.LoginUseCase 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 import org.koin.dsl.module
val networkModule = module { val networkModule = module {
@ -29,5 +29,5 @@ val networkModule = module {
single { ScheduleApiImpl(get()) } single { ScheduleApiImpl(get()) }
single<ScheduleRepository> { ScheduleNetRepository (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 import org.example.presenceapp.domain.entities.Schedule
data class Student( //class SomeStudents {
val id: String, // val students = listOf(
val name: String // Student(id = 1, name = "Васильев Кирилл"),
) // Student(id = 2, name = "Игнатова Вероника"),
// Student(id = 3, name = "Латышева Екатерина"),
class SomeStudents { // Student(id = 4, name = "Ермолаев Егор"),
val students = listOf( // Student(id = 5, name = "Фролов Владимир"),
Student(id = "1", name = "Васильев Кирилл"), // Student(id = 6, name = "Чеботарева Анастасия"),
Student(id = "2", name = "Игнатова Вероника"), // Student(id = 7, name = "Попова Виктория"),
Student(id = "3", name = "Латышева Екатерина"), // Student(id = 8, name = "Соловьева Лейла"),
Student(id = "4", name = "Ермолаев Егор"), // Student(id = 9, name = "Орлова Анжелика"),
Student(id = "5", name = "Фролов Владимир"), // Student(id = 10, name = "Осипова Татьяна"),
Student(id = "6", name = "Чеботарева Анастасия"), // Student(id = 11, name = "Николаева Ева"),
Student(id = "7", name = "Попова Виктория"), // Student(id = 12, name = "Федосеева Майя")
Student(id = "8", name = "Соловьева Лейла"), // )
Student(id = "9", name = "Орлова Анжелика"), //}
Student(id = "10", name = "Осипова Татьяна"),
Student(id = "11", name = "Николаева Ева"),
Student(id = "12", name = "Федосеева Майя")
)
}
object SampleData { object SampleData {
val lessonTimes = listOf("9:00", "9:55", "10:50", "11:55", "13:00", "14:00", "14:55", "15:45") 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.LocalDate
import kotlinx.datetime.Month import kotlinx.datetime.Month
import kotlinx.datetime.number import kotlinx.datetime.number
import org.example.presenceapp.domain.entities.Week
fun Week.formatWeek(): String { fun Week.formatWeek(): String {

View File

@ -15,5 +15,5 @@ data class Attendance(
@Serializable @Serializable
data class AttendanceType( data class AttendanceType(
val id: Int, 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( data class DayData(
val date: LocalDate, val date: LocalDate,
val isCurrentMonth: Boolean, 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.LocalDate
import kotlinx.datetime.Month import kotlinx.datetime.Month

View File

@ -8,10 +8,4 @@ sealed class ResponseState {
sealed class Either<A, B> { sealed class Either<A, B> {
class Left<A, B>(val value: A): Either<A, B>() class Left<A, B>(val value: A): Either<A, B>()
class Right<A, B>(val value: B): 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 package org.example.presenceapp.domain.entities
import kotlinx.datetime.LocalDate
data class Schedule( data class Schedule(
val id: Int, val id: Int,
val lessonNumber: Int, val lessonNumber: Int,
@ -7,9 +9,23 @@ data class Schedule(
val subject: Subject, val subject: Subject,
val dayOfWeek: Int, val dayOfWeek: Int,
) )
data class Subject( data class Subject(
val id: Int, val id: Int,
val name: String 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 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.Schedule
import org.example.presenceapp.domain.entities.Student
interface ScheduleRepository { interface ScheduleRepository {
suspend fun getSchedule(groupCommand: GroupCommand): List<Schedule> 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,50 +2,140 @@ package org.example.presenceapp.domain.usecases
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
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.AddAttendanceCommand
import org.example.presenceapp.domain.command.attendance.AddPresettingCommand import org.example.presenceapp.domain.command.attendance.AddPresettingCommand
import org.example.presenceapp.domain.command.attendance.GetAttendanceCommand 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.command.attendance.GetPresettingCommand
import org.example.presenceapp.domain.entities.Attendance 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.Either
import org.example.presenceapp.domain.entities.Presetting import org.example.presenceapp.domain.entities.Presetting
import org.example.presenceapp.domain.repo.attendance.AttendanceRepository import org.example.presenceapp.domain.repo.AttendanceRepository
class AttendanceUseCase( class AttendanceUseCase(
private val attendanceRepository: AttendanceRepository private val attendanceRepository: AttendanceRepository
) { ) {
fun addAttendance(addAttendanceCommand: AddAttendanceCommand): Flow<Either<Exception, Attendance>> = flow { private fun Attendance.toView(
return@flow try { 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) val result = attendanceRepository.addAttendance(addAttendanceCommand)
emit(Either.Right(result)) emit(Either.Right(result))
} catch (e:Exception) { } catch (e: Exception) {
emit(Either.Left(e))
}
}
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)) emit(Either.Left(e))
} }
} }
fun getAttendance(getAttendanceCommand: GetAttendanceCommand): Flow<Either<Exception, List<Attendance>>> = flow { fun getAttendance(getAttendanceCommand: GetAttendanceCommand): Flow<Either<Exception, List<Attendance>>> = flow {
return@flow try { try {
val result = attendanceRepository.getAttendance(getAttendanceCommand) val result = attendanceRepository.getAttendance(getAttendanceCommand)
emit(Either.Right(result)) emit(Either.Right(result))
} catch (e:Exception) { } catch (e: Exception) {
emit(Either.Left(e)) emit(Either.Left(e))
} }
} }
fun addPresetting(addPresettingCommand: AddPresettingCommand): Flow<Either<Exception, Presetting>> = flow { fun getAttendanceView(
return@flow try { 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) val result = attendanceRepository.addPresetting(addPresettingCommand)
emit(Either.Right(result)) emit(Either.Right(result))
} catch (e:Exception) { } catch (e: Exception) {
emit(Either.Left(e)) emit(Either.Left(e))
} }
} }
fun getPresetting(getPresettingCommand: GetPresettingCommand): Flow<Either<Exception, List<Presetting>>> = flow { fun getPresetting(getPresettingCommand: GetPresettingCommand): Flow<Either<Exception, List<Presetting>>> = flow {
return@flow try { try {
val result = attendanceRepository.getPresetting(getPresettingCommand) val result = attendanceRepository.getPresetting(getPresettingCommand)
emit(Either.Right(result)) emit(Either.Right(result))
} catch (e:Exception) { } catch (e: Exception) {
emit(Either.Left(e)) emit(Either.Left(e))
} }
} }

View File

@ -2,9 +2,12 @@ package org.example.presenceapp.domain.usecases
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
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.Either
import org.example.presenceapp.domain.entities.Schedule import org.example.presenceapp.domain.entities.Schedule
import org.example.presenceapp.domain.entities.Student
import org.example.presenceapp.domain.repo.ScheduleRepository import org.example.presenceapp.domain.repo.ScheduleRepository
class ScheduleUseCase( class ScheduleUseCase(
@ -18,4 +21,13 @@ class ScheduleUseCase(
emit(Either.Left(e)) 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
)
}
}

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