init
This commit is contained in:
parent
9703a42d64
commit
47a0e7dc3c
@ -7,7 +7,7 @@ import androidx.datastore.preferences.preferencesDataStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.example.presenceapp.domain.entities.Attendance
|
||||
import org.example.presenceapp.domain.entities.AttendanceView
|
||||
|
||||
private val Context.attendanceDataStore by preferencesDataStore(name = "attendance_prefs")
|
||||
|
||||
@ -15,17 +15,17 @@ class AttendanceStorageAndroid(private val context: Context): AttendanceStorage
|
||||
|
||||
private val ATTENDANCE_KEY = stringPreferencesKey("attendance_map")
|
||||
|
||||
override suspend fun saveAttendanceMap(map: Map<String, Attendance>) {
|
||||
override suspend fun saveAttendanceMap(map: Map<Int, AttendanceView>) {
|
||||
val json = Json.encodeToString(map)
|
||||
context.attendanceDataStore.edit { prefs ->
|
||||
prefs[ATTENDANCE_KEY] = json
|
||||
}
|
||||
}
|
||||
|
||||
override fun attendanceMapFlow(): Flow<Map<String, Attendance>> {
|
||||
override fun attendanceMapFlow(): Flow<Map<Int, AttendanceView>> {
|
||||
return context.attendanceDataStore.data.map { prefs ->
|
||||
prefs[ATTENDANCE_KEY]?.let {
|
||||
Json.decodeFromString<Map<String, Attendance>>(it)
|
||||
Json.decodeFromString<Map<Int, AttendanceView>>(it)
|
||||
} ?: emptyMap()
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package org.example.presenceapp.ui.settings
|
||||
package org.example.presenceapp.ui.feature.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
@ -2,9 +2,8 @@ package org.example.presenceapp
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import org.example.presenceapp.ui.schedule.ScheduleScreen
|
||||
import org.example.presenceapp.ui.feature.login.LoginScreen
|
||||
import org.example.presenceapp.ui.theme.AppTheme
|
||||
import org.example.project.ui.login.LoginScreen
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
|
||||
@Composable
|
||||
|
@ -2,6 +2,7 @@ package org.example.presenceapp.data.common
|
||||
|
||||
import org.example.presenceapp.data.common.dto.attendance.AttendanceRequestDto
|
||||
import org.example.presenceapp.data.common.dto.attendance.AttendanceResponseDto
|
||||
import org.example.presenceapp.data.common.dto.attendance.AttendanceTypeResponseDto
|
||||
import org.example.presenceapp.data.common.dto.attendance.PresettingRequestDto
|
||||
import org.example.presenceapp.data.common.dto.attendance.PresettingResponseDto
|
||||
import org.example.presenceapp.data.common.dto.auth.AuthRequestDto
|
||||
@ -11,15 +12,21 @@ import org.example.presenceapp.data.common.dto.auth.ResponsibleDto
|
||||
import org.example.presenceapp.data.common.dto.auth.ResponsibleTypeDto
|
||||
import org.example.presenceapp.data.common.dto.auth.RoleResponseDto
|
||||
import org.example.presenceapp.data.common.dto.auth.UserResponseDto
|
||||
import org.example.presenceapp.data.common.dto.group.StudentRequestDto
|
||||
import org.example.presenceapp.data.common.dto.group.StudentResponseDto
|
||||
import org.example.presenceapp.data.common.dto.schedule.ScheduleRequestDto
|
||||
import org.example.presenceapp.data.common.dto.schedule.ScheduleResponseDto
|
||||
import org.example.presenceapp.data.common.dto.schedule.SubjectResponseDto
|
||||
import org.example.presenceapp.domain.command.LoginCommand
|
||||
import org.example.presenceapp.domain.command.attendance.AddAttendanceCommand
|
||||
import org.example.presenceapp.domain.command.attendance.AddPresettingCommand
|
||||
import org.example.presenceapp.domain.command.GroupCommand
|
||||
import org.example.presenceapp.domain.command.LoginCommand
|
||||
import org.example.presenceapp.domain.command.schedule.GetStudentsByGroupIdCommand
|
||||
import org.example.presenceapp.domain.command.schedule.GroupCommand
|
||||
import org.example.presenceapp.domain.entities.AbsenceReason
|
||||
import org.example.presenceapp.domain.entities.Attendance
|
||||
import org.example.presenceapp.domain.entities.AttendanceType
|
||||
import org.example.presenceapp.domain.entities.AttendanceTypeView
|
||||
import org.example.presenceapp.domain.entities.AttendanceView
|
||||
import org.example.presenceapp.domain.entities.GroupResponse
|
||||
import org.example.presenceapp.domain.entities.LoginResponse
|
||||
import org.example.presenceapp.domain.entities.Presetting
|
||||
@ -27,6 +34,7 @@ import org.example.presenceapp.domain.entities.Responsible
|
||||
import org.example.presenceapp.domain.entities.ResponsibleType
|
||||
import org.example.presenceapp.domain.entities.RoleResponse
|
||||
import org.example.presenceapp.domain.entities.Schedule
|
||||
import org.example.presenceapp.domain.entities.Student
|
||||
import org.example.presenceapp.domain.entities.Subject
|
||||
import org.example.presenceapp.domain.entities.UserResponse
|
||||
|
||||
@ -37,6 +45,10 @@ fun LoginCommand.toDto(): AuthRequestDto = AuthRequestDto(
|
||||
|
||||
fun GroupCommand.toDto(): ScheduleRequestDto = ScheduleRequestDto(groupId)
|
||||
|
||||
fun GetStudentsByGroupIdCommand.toDto(): StudentRequestDto = StudentRequestDto(
|
||||
id = groupId
|
||||
)
|
||||
|
||||
fun AddAttendanceCommand.toDto(): AttendanceRequestDto = AttendanceRequestDto(
|
||||
studentId = studentId,
|
||||
scheduleId = scheduleId,
|
||||
@ -50,6 +62,27 @@ fun AddPresettingCommand.toDto(): PresettingRequestDto = PresettingRequestDto(
|
||||
endAt = endAt
|
||||
)
|
||||
|
||||
fun AttendanceView.toDto(
|
||||
scheduleId: Int,
|
||||
absenceReason: String?
|
||||
): Attendance {
|
||||
return Attendance(
|
||||
studentId = this.studentId,
|
||||
attendanceTypeId = this.type.toDto(absenceReason)
|
||||
?: throw IllegalArgumentException("Unknown AttendanceTypeView: $type with reason: $absenceReason"),
|
||||
scheduleId = scheduleId,
|
||||
date = this.date,
|
||||
type = this.type.ordinal
|
||||
)
|
||||
}
|
||||
|
||||
fun AttendanceTypeView.toDto(absenceReason: String?): Int? {
|
||||
return when (this) {
|
||||
AttendanceTypeView.PRESENT -> 4
|
||||
AttendanceTypeView.ABSENT -> absenceReason?.let { AbsenceReason.valueOf(it).id }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun ScheduleResponseDto.toEntity(): Schedule = Schedule(
|
||||
@ -60,6 +93,15 @@ fun ScheduleResponseDto.toEntity(): Schedule = Schedule(
|
||||
id = id
|
||||
)
|
||||
|
||||
fun StudentResponseDto.toEntity(): Student = Student(
|
||||
id = studentId,
|
||||
uuid = uuid,
|
||||
fio = fio,
|
||||
role = "",
|
||||
enrollDate = enrollDate,
|
||||
expulsionDate = expulsionDate
|
||||
)
|
||||
|
||||
fun SubjectResponseDto.toEntity(): Subject = Subject(
|
||||
id = id,
|
||||
name = name
|
||||
@ -78,22 +120,18 @@ fun UserResponseDto.toEntity(): UserResponse = UserResponse(
|
||||
role = role.toEntity(),
|
||||
responsible = responsible.map { it.toEntity() }
|
||||
)
|
||||
|
||||
fun ResponsibleDto.toEntity(): Responsible = Responsible(
|
||||
group = group.toEntity(),
|
||||
responsibleType = responsibleType.toEntity()
|
||||
)
|
||||
|
||||
fun GroupDto.toEntity(): GroupResponse = GroupResponse(
|
||||
id = id,
|
||||
name = name
|
||||
)
|
||||
|
||||
fun ResponsibleTypeDto.toEntity(): ResponsibleType = ResponsibleType(
|
||||
id = id,
|
||||
name = name
|
||||
)
|
||||
|
||||
fun RoleResponseDto.toEntity(): RoleResponse = RoleResponse(
|
||||
id = id,
|
||||
name = name
|
||||
@ -107,6 +145,11 @@ fun AttendanceResponseDto.toEntity(): Attendance = Attendance(
|
||||
type = attendanceTypeId
|
||||
)
|
||||
|
||||
fun AttendanceTypeResponseDto.toAttendanceType(): AttendanceType = AttendanceType(
|
||||
id = id,
|
||||
name = name
|
||||
)
|
||||
|
||||
fun AttendanceResponseDto.toAttendanceType(): AttendanceType = AttendanceType(
|
||||
id = attendanceTypeId,
|
||||
name = ""
|
||||
@ -118,3 +161,26 @@ fun PresettingResponseDto.toEntity(): Presetting = Presetting(
|
||||
startAt = startAt,
|
||||
endAt = endAt
|
||||
)
|
||||
|
||||
fun Attendance.toEntity(
|
||||
attendanceTypeMapper: (Int) -> AttendanceTypeView,
|
||||
absenceReasonProvider: (Int, Int) -> String?
|
||||
): AttendanceView {
|
||||
return AttendanceView(
|
||||
studentId = this.studentId,
|
||||
date = this.date,
|
||||
type = attendanceTypeMapper(this.attendanceTypeId),
|
||||
isModified = false,
|
||||
absenceReason = absenceReasonProvider(this.studentId, this.attendanceTypeId)
|
||||
)
|
||||
}
|
||||
|
||||
fun AttendanceType.toEntity(id: Int): Pair<AttendanceTypeView, AbsenceReason?> {
|
||||
return when (id) {
|
||||
4 -> AttendanceTypeView.PRESENT to null
|
||||
1 -> AttendanceTypeView.ABSENT to AbsenceReason.SICK
|
||||
2 -> AttendanceTypeView.ABSENT to AbsenceReason.COMPETITION
|
||||
3 -> AttendanceTypeView.ABSENT to AbsenceReason.SKIP
|
||||
else -> throw IllegalArgumentException("Unknown attendance id: $id")
|
||||
}
|
||||
}
|
@ -10,10 +10,3 @@ data class AttendanceRequestDto(
|
||||
val attendanceTypeId: Int,
|
||||
val attendanceDate: LocalDate
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PresettingRequestDto(
|
||||
val attendanceTypeId: Int,
|
||||
val startAt: LocalDate,
|
||||
val endAt: LocalDate?,
|
||||
)
|
@ -15,14 +15,5 @@ data class AttendanceResponseDto(
|
||||
@Serializable
|
||||
data class AttendanceTypeResponseDto(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PresettingResponseDto(
|
||||
val id: Int,
|
||||
val attendanceType: AttendanceResponseDto,
|
||||
val studentId: Int,
|
||||
val startAt: LocalDate,
|
||||
val endAt: LocalDate,
|
||||
val name: String
|
||||
)
|
@ -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?
|
||||
)
|
@ -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
|
||||
)
|
@ -0,0 +1,8 @@
|
||||
package org.example.presenceapp.data.common.dto.group
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StudentRequestDto(
|
||||
val id: Int
|
||||
)
|
@ -2,19 +2,16 @@ package org.example.presenceapp.data.local
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.example.presenceapp.domain.entities.AttendanceView
|
||||
import org.example.presenceapp.data.local.storage.attendance.AttendanceStorage
|
||||
import org.example.presenceapp.domain.entities.AbsenceReason
|
||||
import org.example.presenceapp.domain.entities.Attendance
|
||||
import org.example.presenceapp.domain.entities.AttendanceType
|
||||
|
||||
class LocalDataSource(private val attendanceStorage: AttendanceStorage) {
|
||||
suspend fun saveAttendance(map: Map<String, Attendance>) {
|
||||
suspend fun saveAttendance(map: Map<Int, AttendanceView>) {
|
||||
attendanceStorage.saveAttendanceMap(map)
|
||||
}
|
||||
|
||||
fun observeAttendance(): Flow<Map<String, Attendance>> {
|
||||
fun observeAttendance(): Flow<Map<Int, AttendanceView>> {
|
||||
return attendanceStorage.attendanceMapFlow()
|
||||
.map { attendanceMap ->
|
||||
attendanceMap
|
||||
|
@ -1,9 +1,9 @@
|
||||
package org.example.presenceapp.data.local.storage.attendance
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.example.presenceapp.domain.entities.Attendance
|
||||
import org.example.presenceapp.domain.entities.AttendanceView
|
||||
|
||||
interface AttendanceStorage {
|
||||
suspend fun saveAttendanceMap(map: Map<String, Attendance>)
|
||||
fun attendanceMapFlow(): Flow<Map<String, Attendance>>
|
||||
suspend fun saveAttendanceMap(map: Map<Int, AttendanceView>)
|
||||
fun attendanceMapFlow(): Flow<Map<Int, AttendanceView>>
|
||||
}
|
@ -12,14 +12,14 @@ import org.example.presenceapp.domain.command.attendance.AddPresettingCommand
|
||||
|
||||
interface AttendanceApi {
|
||||
@POST("api/v1/presence")
|
||||
suspend fun addAttendance(@Body commands: List<AttendanceRequestDto>): List<AttendanceResponseDto>
|
||||
|
||||
@GET("api/v1/presence/dictionary/attendance_type")
|
||||
suspend fun getAttendanceTypes(): List<AttendanceTypeResponseDto>
|
||||
suspend fun addAttendance(@Body command: List<AttendanceRequestDto>): List<AttendanceResponseDto>
|
||||
|
||||
@GET("api/v1/presence/{groupId}")
|
||||
suspend fun getAttendance(@Path("groupId") groupId: Int): List<AttendanceResponseDto>
|
||||
|
||||
@GET("api/v1/presence/dictionary/attendance_type/{typeId}")
|
||||
suspend fun getAttendanceTypes(@Path("typeId") typeId: Int): List<AttendanceTypeResponseDto>
|
||||
|
||||
@POST("api/v1/presence/presetting")
|
||||
suspend fun addPresetting(@Body command: AddPresettingCommand): Boolean
|
||||
|
||||
|
@ -5,14 +5,12 @@ import de.jensklingenberg.ktorfit.http.Path
|
||||
import org.example.presenceapp.data.common.dto.attendance.AttendanceResponseDto
|
||||
import org.example.presenceapp.data.common.dto.group.StudentResponseDto
|
||||
import org.example.presenceapp.data.common.dto.schedule.ScheduleResponseDto
|
||||
import org.example.presenceapp.domain.entities.Group
|
||||
|
||||
interface GroupApi {
|
||||
@GET("api/v1/group/{id}/schedule")
|
||||
suspend fun getSchedule(@Path id: Int): List<ScheduleResponseDto>
|
||||
|
||||
@GET("api/v1/group/{id}/students")
|
||||
suspend fun getStudents(@Path id: Int): List<StudentResponseDto>
|
||||
|
||||
@GET("api/v1/group/{id}/presence")
|
||||
suspend fun getPresence(@Path id: Int): List<AttendanceResponseDto>
|
||||
suspend fun getStudentsByGroupId(@Path id: Int): List<StudentResponseDto>
|
||||
}
|
@ -7,16 +7,17 @@ import org.example.presenceapp.data.common.toDto
|
||||
import org.example.presenceapp.data.remote.api.AttendanceApi
|
||||
import org.example.presenceapp.domain.command.attendance.AddAttendanceCommand
|
||||
import org.example.presenceapp.domain.command.attendance.AddPresettingCommand
|
||||
import org.example.presenceapp.domain.command.attendance.GetAttendanceTypesCommand
|
||||
|
||||
class AttendanceApiImpl(private val attendanceApi: AttendanceApi) {
|
||||
suspend fun addAttendance(commands: List<AddAttendanceCommand>): List<AttendanceResponseDto> =
|
||||
attendanceApi.addAttendance(commands.map { it.toDto() })
|
||||
suspend fun addAttendance(command: List<AddAttendanceCommand>): List<AttendanceResponseDto> =
|
||||
attendanceApi.addAttendance(command.map { it.toDto() })
|
||||
|
||||
suspend fun getAttendance(groupId: Int): List<AttendanceResponseDto> =
|
||||
attendanceApi.getAttendance(groupId)
|
||||
|
||||
suspend fun getAttendanceTypes(): List<AttendanceTypeResponseDto> =
|
||||
attendanceApi.getAttendanceTypes()
|
||||
suspend fun getAttendanceTypes(typeId: Int): List<AttendanceTypeResponseDto> =
|
||||
attendanceApi.getAttendanceTypes(typeId)
|
||||
|
||||
suspend fun addPresetting(command: AddPresettingCommand): Boolean =
|
||||
attendanceApi.addPresetting(command)
|
||||
|
@ -1,13 +1,13 @@
|
||||
package org.example.presenceapp.data.remote.impl
|
||||
|
||||
import org.example.presenceapp.data.common.dto.attendance.AttendanceResponseDto
|
||||
import org.example.presenceapp.data.common.dto.group.StudentRequestDto
|
||||
import org.example.presenceapp.data.common.dto.group.StudentResponseDto
|
||||
import org.example.presenceapp.data.common.dto.schedule.ScheduleRequestDto
|
||||
import org.example.presenceapp.data.common.dto.schedule.ScheduleResponseDto
|
||||
import org.example.presenceapp.data.remote.api.GroupApi
|
||||
import org.example.presenceapp.domain.entities.Group
|
||||
|
||||
class ScheduleApiImpl(private val groupApi: GroupApi) {
|
||||
suspend fun getSchedule(scheduleRequestDto: ScheduleRequestDto): List<ScheduleResponseDto> = groupApi.getSchedule(scheduleRequestDto.groupId)
|
||||
suspend fun getStudent(scheduleRequestDto: ScheduleRequestDto): List<StudentResponseDto> = groupApi.getStudents(scheduleRequestDto.groupId)
|
||||
suspend fun getAttendance(scheduleRequestDto: ScheduleRequestDto): List<AttendanceResponseDto> = groupApi.getPresence(scheduleRequestDto.groupId)
|
||||
suspend fun getStudentsByGroupId(studentRequestDto: StudentRequestDto): List<StudentResponseDto> = groupApi.getStudentsByGroupId(studentRequestDto.id)
|
||||
}
|
@ -3,35 +3,33 @@ package org.example.presenceapp.data.repository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.example.presenceapp.data.common.toDto
|
||||
import org.example.presenceapp.data.common.toAttendanceType
|
||||
import org.example.presenceapp.data.common.toEntity
|
||||
import org.example.presenceapp.data.local.LocalDataSource
|
||||
import org.example.presenceapp.data.remote.impl.AttendanceApiImpl
|
||||
import org.example.presenceapp.domain.command.attendance.AddAttendanceCommand
|
||||
import org.example.presenceapp.domain.command.attendance.AddPresettingCommand
|
||||
import org.example.presenceapp.domain.command.attendance.GetAttendanceCommand
|
||||
import org.example.presenceapp.domain.command.attendance.GetAttendanceTypesCommand
|
||||
import org.example.presenceapp.domain.command.attendance.GetPresettingCommand
|
||||
import org.example.presenceapp.domain.entities.Attendance
|
||||
import org.example.presenceapp.domain.entities.AttendanceType
|
||||
import org.example.presenceapp.domain.entities.AttendanceView
|
||||
import org.example.presenceapp.domain.entities.Presetting
|
||||
import org.example.presenceapp.domain.repo.attendance.AttendanceRepository
|
||||
import org.example.presenceapp.domain.repo.attendance.PresettingRepository
|
||||
import org.example.presenceapp.domain.repo.AttendanceRepository
|
||||
|
||||
class AttendanceNetRepository(
|
||||
private val localDataSource: LocalDataSource,
|
||||
private val attendanceApiImpl: AttendanceApiImpl
|
||||
): AttendanceRepository, PresettingRepository {
|
||||
suspend fun saveAttendanceLocally(attendance: Map<String, Attendance>) {
|
||||
): AttendanceRepository {
|
||||
override suspend fun saveAttendanceLocally(attendance: Map<Int, AttendanceView>) {
|
||||
localDataSource.saveAttendance(attendance)
|
||||
}
|
||||
|
||||
fun observeLocalAttendance(): Flow<Map<String, Attendance>> {
|
||||
override fun observeLocalAttendance(): Flow<Map<Int, AttendanceView>> {
|
||||
return localDataSource.observeAttendance()
|
||||
.map { attendanceMap ->
|
||||
attendanceMap
|
||||
}
|
||||
.catch { e ->
|
||||
emit(emptyMap())
|
||||
}
|
||||
.map { attendanceMap -> attendanceMap }
|
||||
.catch { e -> emit(emptyMap()) }
|
||||
}
|
||||
|
||||
override suspend fun addAttendance(addAttendanceCommand: AddAttendanceCommand): List<Attendance> {
|
||||
@ -44,6 +42,11 @@ class AttendanceNetRepository(
|
||||
return result.map { it.toEntity() }
|
||||
}
|
||||
|
||||
override suspend fun getAttendanceTypes(getAttendanceTypesCommand: GetAttendanceTypesCommand): List<AttendanceType> {
|
||||
val result = attendanceApiImpl.getAttendanceTypes(getAttendanceTypesCommand.typeId)
|
||||
return result.map { it.toAttendanceType() }
|
||||
}
|
||||
|
||||
override suspend fun addPresetting(addPresettingCommand: AddPresettingCommand): Boolean {
|
||||
return attendanceApiImpl.addPresetting(addPresettingCommand)
|
||||
}
|
||||
|
@ -3,8 +3,10 @@ package org.example.presenceapp.data.repository
|
||||
import org.example.presenceapp.data.common.toDto
|
||||
import org.example.presenceapp.data.common.toEntity
|
||||
import org.example.presenceapp.data.remote.impl.ScheduleApiImpl
|
||||
import org.example.presenceapp.domain.command.GroupCommand
|
||||
import org.example.presenceapp.domain.command.schedule.GetStudentsByGroupIdCommand
|
||||
import org.example.presenceapp.domain.command.schedule.GroupCommand
|
||||
import org.example.presenceapp.domain.entities.Schedule
|
||||
import org.example.presenceapp.domain.entities.Student
|
||||
import org.example.presenceapp.domain.repo.ScheduleRepository
|
||||
|
||||
class ScheduleNetRepository(
|
||||
@ -14,4 +16,10 @@ class ScheduleNetRepository(
|
||||
val result = scheduleApiImpl.getSchedule(groupCommand.toDto())
|
||||
return result.map { it.toEntity() }
|
||||
}
|
||||
|
||||
override suspend fun getStudentsByGroupId(getStudentsByGroupIdCommand: GetStudentsByGroupIdCommand): List<Student> {
|
||||
val result = scheduleApiImpl.getStudentsByGroupId(getStudentsByGroupIdCommand.toDto())
|
||||
println("Students from API: $result")
|
||||
return result.map { it.toEntity() }
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
package org.example.presenceapp.data.repository.settings
|
||||
|
||||
import org.example.presenceapp.domain.entities.AttendanceType
|
||||
import org.example.presenceapp.domain.entities.AttendanceTypeView
|
||||
|
||||
interface SettingsRepository {
|
||||
suspend fun getDefaultAttendanceStatus(): AttendanceType
|
||||
suspend fun setDefaultAttendanceStatus(type: AttendanceType)
|
||||
suspend fun getDefaultAttendanceStatus(): AttendanceTypeView
|
||||
suspend fun setDefaultAttendanceStatus(type: AttendanceTypeView)
|
||||
}
|
@ -1,21 +1,21 @@
|
||||
package org.example.presenceapp.data.repository.settings
|
||||
|
||||
import org.example.presenceapp.domain.entities.AttendanceTypeView
|
||||
import org.example.presenceapp.data.local.storage.SettingsStorage
|
||||
import org.example.presenceapp.domain.entities.AttendanceType
|
||||
|
||||
class SettingsRepositoryImpl(
|
||||
private val settingsStorage: SettingsStorage
|
||||
) : SettingsRepository {
|
||||
|
||||
override suspend fun getDefaultAttendanceStatus(): AttendanceType {
|
||||
override suspend fun getDefaultAttendanceStatus(): AttendanceTypeView {
|
||||
val statusString = settingsStorage.get(
|
||||
key = "default_attendance_status",
|
||||
defaultValue = AttendanceType.ABSENT.name
|
||||
defaultValue = AttendanceTypeView.ABSENT.name
|
||||
)
|
||||
return enumValueOf(statusString)
|
||||
}
|
||||
|
||||
override suspend fun setDefaultAttendanceStatus(type: AttendanceType) {
|
||||
override suspend fun setDefaultAttendanceStatus(type: AttendanceTypeView) {
|
||||
settingsStorage.put(
|
||||
key = "default_attendance_status",
|
||||
value = type.name
|
||||
|
@ -13,7 +13,7 @@ import org.example.presenceapp.data.repository.ScheduleNetRepository
|
||||
import org.example.presenceapp.domain.repo.LoginRepository
|
||||
import org.example.presenceapp.domain.repo.ScheduleRepository
|
||||
import org.example.presenceapp.domain.usecases.LoginUseCase
|
||||
import org.example.project.ui.login.LoginViewModel
|
||||
import org.example.presenceapp.ui.feature.login.LoginScreenModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
val networkModule = module {
|
||||
@ -29,5 +29,5 @@ val networkModule = module {
|
||||
single { ScheduleApiImpl(get()) }
|
||||
single<ScheduleRepository> { ScheduleNetRepository (get()) }
|
||||
|
||||
factory { LoginViewModel(get(), get()) }
|
||||
factory { org.example.presenceapp.ui.feature.login.LoginScreenModel(get(), get()) }
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package org.example.presenceapp.domain.command
|
||||
|
||||
data class GroupCommand(
|
||||
val groupId: Int
|
||||
)
|
@ -0,0 +1,6 @@
|
||||
package org.example.presenceapp.domain.command.attendance
|
||||
|
||||
data class GetAttendanceTypesCommand(
|
||||
val typeId: Int,
|
||||
val typeName: String
|
||||
)
|
@ -0,0 +1,5 @@
|
||||
package org.example.presenceapp.domain.command.schedule
|
||||
|
||||
data class GetStudentsByGroupIdCommand(
|
||||
val groupId: Int
|
||||
)
|
@ -0,0 +1,5 @@
|
||||
package org.example.presenceapp.domain.command.schedule
|
||||
|
||||
data class GroupCommand(
|
||||
val groupId: Int
|
||||
)
|
@ -2,27 +2,22 @@ package org.example.presenceapp.domain.common
|
||||
|
||||
import org.example.presenceapp.domain.entities.Schedule
|
||||
|
||||
data class Student(
|
||||
val id: String,
|
||||
val name: String
|
||||
)
|
||||
|
||||
class SomeStudents {
|
||||
val students = listOf(
|
||||
Student(id = "1", name = "Васильев Кирилл"),
|
||||
Student(id = "2", name = "Игнатова Вероника"),
|
||||
Student(id = "3", name = "Латышева Екатерина"),
|
||||
Student(id = "4", name = "Ермолаев Егор"),
|
||||
Student(id = "5", name = "Фролов Владимир"),
|
||||
Student(id = "6", name = "Чеботарева Анастасия"),
|
||||
Student(id = "7", name = "Попова Виктория"),
|
||||
Student(id = "8", name = "Соловьева Лейла"),
|
||||
Student(id = "9", name = "Орлова Анжелика"),
|
||||
Student(id = "10", name = "Осипова Татьяна"),
|
||||
Student(id = "11", name = "Николаева Ева"),
|
||||
Student(id = "12", name = "Федосеева Майя")
|
||||
)
|
||||
}
|
||||
//class SomeStudents {
|
||||
// val students = listOf(
|
||||
// Student(id = 1, name = "Васильев Кирилл"),
|
||||
// Student(id = 2, name = "Игнатова Вероника"),
|
||||
// Student(id = 3, name = "Латышева Екатерина"),
|
||||
// Student(id = 4, name = "Ермолаев Егор"),
|
||||
// Student(id = 5, name = "Фролов Владимир"),
|
||||
// Student(id = 6, name = "Чеботарева Анастасия"),
|
||||
// Student(id = 7, name = "Попова Виктория"),
|
||||
// Student(id = 8, name = "Соловьева Лейла"),
|
||||
// Student(id = 9, name = "Орлова Анжелика"),
|
||||
// Student(id = 10, name = "Осипова Татьяна"),
|
||||
// Student(id = 11, name = "Николаева Ева"),
|
||||
// Student(id = 12, name = "Федосеева Майя")
|
||||
// )
|
||||
//}
|
||||
|
||||
object SampleData {
|
||||
val lessonTimes = listOf("9:00", "9:55", "10:50", "11:55", "13:00", "14:00", "14:55", "15:45")
|
||||
|
@ -3,6 +3,7 @@ package org.example.project.domain.models
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.Month
|
||||
import kotlinx.datetime.number
|
||||
import org.example.presenceapp.domain.entities.Week
|
||||
|
||||
|
||||
fun Week.formatWeek(): String {
|
||||
|
@ -15,5 +15,5 @@ data class Attendance(
|
||||
@Serializable
|
||||
data class AttendanceType(
|
||||
val id: Int,
|
||||
var name: String = ""
|
||||
var name: String
|
||||
)
|
@ -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)
|
||||
}
|
@ -5,5 +5,5 @@ import kotlinx.datetime.LocalDate
|
||||
data class DayData(
|
||||
val date: LocalDate,
|
||||
val isCurrentMonth: Boolean,
|
||||
val attendance: AttendanceType?
|
||||
val attendance: AttendanceTypeView?
|
||||
)
|
@ -1,4 +1,4 @@
|
||||
package org.example.project.domain.models
|
||||
package org.example.presenceapp.domain.entities
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.Month
|
||||
|
@ -9,9 +9,3 @@ sealed class Either<A, B> {
|
||||
class Left<A, B>(val value: A): Either<A, B>()
|
||||
class Right<A, B>(val value: B): Either<A, B>()
|
||||
}
|
||||
|
||||
enum class ServiceError {
|
||||
NOT_FOUND,
|
||||
UNAUTHORIZED,
|
||||
NOT_CREATED
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
package org.example.presenceapp.domain.entities
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
data class Schedule(
|
||||
val id: Int,
|
||||
val lessonNumber: Int,
|
||||
@ -7,9 +9,23 @@ data class Schedule(
|
||||
val subject: Subject,
|
||||
val dayOfWeek: Int,
|
||||
)
|
||||
|
||||
data class Subject(
|
||||
val id: Int,
|
||||
val name: String
|
||||
)
|
||||
|
||||
data class Student(
|
||||
val id: Int,
|
||||
val uuid: String,
|
||||
val fio: String,
|
||||
val role: String,
|
||||
val enrollDate: LocalDate,
|
||||
var expulsionDate: LocalDate? = null
|
||||
)
|
||||
|
||||
data class Group(
|
||||
val groupId: Int,
|
||||
var name: String,
|
||||
var students: List<Student> = emptyList()
|
||||
)
|
@ -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>
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
package org.example.presenceapp.domain.repo
|
||||
|
||||
import org.example.presenceapp.domain.command.GroupCommand
|
||||
import org.example.presenceapp.data.common.dto.group.StudentResponseDto
|
||||
import org.example.presenceapp.domain.command.schedule.GetStudentsByGroupIdCommand
|
||||
import org.example.presenceapp.domain.command.schedule.GroupCommand
|
||||
import org.example.presenceapp.domain.entities.Group
|
||||
import org.example.presenceapp.domain.entities.Schedule
|
||||
import org.example.presenceapp.domain.entities.Student
|
||||
|
||||
interface ScheduleRepository {
|
||||
suspend fun getSchedule(groupCommand: GroupCommand): List<Schedule>
|
||||
suspend fun getStudentsByGroupId(getStudentsByGroupIdCommand: GetStudentsByGroupIdCommand): List<Student>
|
||||
}
|
@ -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>
|
||||
}
|
@ -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>
|
||||
}
|
@ -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>
|
||||
}
|
@ -2,20 +2,46 @@ package org.example.presenceapp.domain.usecases
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.example.presenceapp.data.common.toDto
|
||||
import org.example.presenceapp.data.common.toEntity
|
||||
import org.example.presenceapp.domain.command.attendance.AddAttendanceCommand
|
||||
import org.example.presenceapp.domain.command.attendance.AddPresettingCommand
|
||||
import org.example.presenceapp.domain.command.attendance.GetAttendanceCommand
|
||||
import org.example.presenceapp.domain.command.attendance.GetAttendanceTypesCommand
|
||||
import org.example.presenceapp.domain.command.attendance.GetPresettingCommand
|
||||
import org.example.presenceapp.domain.entities.Attendance
|
||||
import org.example.presenceapp.domain.entities.AttendanceType
|
||||
import org.example.presenceapp.domain.entities.AttendanceTypeView
|
||||
import org.example.presenceapp.domain.entities.AttendanceView
|
||||
import org.example.presenceapp.domain.entities.Either
|
||||
import org.example.presenceapp.domain.entities.Presetting
|
||||
import org.example.presenceapp.domain.repo.attendance.AttendanceRepository
|
||||
import org.example.presenceapp.domain.repo.AttendanceRepository
|
||||
|
||||
class AttendanceUseCase(
|
||||
private val attendanceRepository: AttendanceRepository
|
||||
) {
|
||||
fun addAttendance(addAttendanceCommand: AddAttendanceCommand): Flow<Either<Exception, Attendance>> = flow {
|
||||
return@flow try {
|
||||
private fun Attendance.toView(
|
||||
attendanceTypeMapper: (Int) -> AttendanceTypeView,
|
||||
absenceReasonProvider: (Int, Int) -> String?
|
||||
): AttendanceView = this.toEntity(attendanceTypeMapper, absenceReasonProvider)
|
||||
|
||||
private fun AttendanceView.toApi(
|
||||
scheduleId: Int,
|
||||
absenceReason: String?
|
||||
): Attendance = this.toDto(scheduleId, absenceReason)
|
||||
|
||||
|
||||
|
||||
suspend fun saveAttendanceLocally(attendance: Map<Int, AttendanceView>) {
|
||||
attendanceRepository.saveAttendanceLocally(attendance)
|
||||
}
|
||||
|
||||
fun observeLocalAttendance(): Flow<Map<Int, AttendanceView>> {
|
||||
return attendanceRepository.observeLocalAttendance()
|
||||
}
|
||||
|
||||
fun addAttendance(addAttendanceCommand: AddAttendanceCommand): Flow<Either<Exception, List<Attendance>>> = flow {
|
||||
try {
|
||||
val result = attendanceRepository.addAttendance(addAttendanceCommand)
|
||||
emit(Either.Right(result))
|
||||
} catch (e: Exception) {
|
||||
@ -23,8 +49,33 @@ class AttendanceUseCase(
|
||||
}
|
||||
}
|
||||
|
||||
fun addAttendanceView(
|
||||
attendanceViews: List<AttendanceView>,
|
||||
scheduleId: Int,
|
||||
absenceReasonProvider: (AttendanceView) -> String?
|
||||
): Flow<Either<Exception, List<Attendance>>> = flow {
|
||||
try {
|
||||
val results = mutableListOf<Attendance>()
|
||||
attendanceViews.forEach { view ->
|
||||
val absenceReason = absenceReasonProvider(view)
|
||||
val dto = view.toApi(scheduleId, absenceReason)
|
||||
val command = AddAttendanceCommand(
|
||||
attendanceDate = dto.date,
|
||||
studentId = dto.studentId,
|
||||
attendanceTypeId = dto.attendanceTypeId,
|
||||
scheduleId = dto.scheduleId
|
||||
)
|
||||
val result = attendanceRepository.addAttendance(command)
|
||||
results.addAll(result)
|
||||
}
|
||||
emit(Either.Right(results))
|
||||
} catch (e: Exception) {
|
||||
emit(Either.Left(e))
|
||||
}
|
||||
}
|
||||
|
||||
fun getAttendance(getAttendanceCommand: GetAttendanceCommand): Flow<Either<Exception, List<Attendance>>> = flow {
|
||||
return@flow try {
|
||||
try {
|
||||
val result = attendanceRepository.getAttendance(getAttendanceCommand)
|
||||
emit(Either.Right(result))
|
||||
} catch (e: Exception) {
|
||||
@ -32,8 +83,47 @@ class AttendanceUseCase(
|
||||
}
|
||||
}
|
||||
|
||||
fun addPresetting(addPresettingCommand: AddPresettingCommand): Flow<Either<Exception, Presetting>> = flow {
|
||||
return@flow try {
|
||||
fun getAttendanceView(
|
||||
getAttendanceCommand: GetAttendanceCommand,
|
||||
attendanceTypeMapper: (Int) -> AttendanceTypeView,
|
||||
absenceReasonProvider: (Int, Int) -> String?
|
||||
): Flow<Either<Exception, List<AttendanceView>>> = flow {
|
||||
try {
|
||||
val result = attendanceRepository.getAttendance(getAttendanceCommand)
|
||||
.map { it.toView(attendanceTypeMapper, absenceReasonProvider) }
|
||||
emit(Either.Right(result))
|
||||
} catch (e: Exception) {
|
||||
emit(Either.Left(e))
|
||||
}
|
||||
}
|
||||
|
||||
fun getAttendanceTypes(getAttendanceTypesCommand: GetAttendanceTypesCommand): Flow<Either<Exception, List<AttendanceType>>> = flow {
|
||||
try {
|
||||
val result = attendanceRepository.getAttendanceTypes(getAttendanceTypesCommand)
|
||||
emit(Either.Right(result))
|
||||
} catch (e: Exception) {
|
||||
emit(Either.Left(e))
|
||||
}
|
||||
}
|
||||
|
||||
fun getAttendanceTypesView(
|
||||
getAttendanceTypesCommand: GetAttendanceTypesCommand,
|
||||
attendanceTypeMapper: (Int) -> AttendanceTypeView
|
||||
): Flow<Either<Exception, List<AttendanceTypeView>>> = flow {
|
||||
try {
|
||||
val result = attendanceRepository.getAttendanceTypes(getAttendanceTypesCommand)
|
||||
.map { attendanceType ->
|
||||
val (attendanceTypeView, _) = attendanceType.toEntity(attendanceType.id)
|
||||
attendanceTypeView
|
||||
}
|
||||
emit(Either.Right(result))
|
||||
} catch (e: Exception) {
|
||||
emit(Either.Left(e))
|
||||
}
|
||||
}
|
||||
|
||||
fun addPresetting(addPresettingCommand: AddPresettingCommand): Flow<Either<Exception, Boolean>> = flow {
|
||||
try {
|
||||
val result = attendanceRepository.addPresetting(addPresettingCommand)
|
||||
emit(Either.Right(result))
|
||||
} catch (e: Exception) {
|
||||
@ -42,7 +132,7 @@ class AttendanceUseCase(
|
||||
}
|
||||
|
||||
fun getPresetting(getPresettingCommand: GetPresettingCommand): Flow<Either<Exception, List<Presetting>>> = flow {
|
||||
return@flow try {
|
||||
try {
|
||||
val result = attendanceRepository.getPresetting(getPresettingCommand)
|
||||
emit(Either.Right(result))
|
||||
} catch (e: Exception) {
|
||||
|
@ -2,9 +2,12 @@ package org.example.presenceapp.domain.usecases
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.example.presenceapp.domain.command.GroupCommand
|
||||
import org.example.presenceapp.data.common.dto.group.StudentResponseDto
|
||||
import org.example.presenceapp.domain.command.schedule.GetStudentsByGroupIdCommand
|
||||
import org.example.presenceapp.domain.command.schedule.GroupCommand
|
||||
import org.example.presenceapp.domain.entities.Either
|
||||
import org.example.presenceapp.domain.entities.Schedule
|
||||
import org.example.presenceapp.domain.entities.Student
|
||||
import org.example.presenceapp.domain.repo.ScheduleRepository
|
||||
|
||||
class ScheduleUseCase(
|
||||
@ -18,4 +21,13 @@ class ScheduleUseCase(
|
||||
emit(Either.Left(e))
|
||||
}
|
||||
}
|
||||
|
||||
fun getStudentsByGroupId(getStudentsByGroupIdCommand: GetStudentsByGroupIdCommand): Flow<Either<Exception, List<Student>>> = flow {
|
||||
return@flow try {
|
||||
val result = scheduleRepository.getStudentsByGroupId(getStudentsByGroupIdCommand)
|
||||
emit(Either.Right(result))
|
||||
} catch (e: Exception) {
|
||||
emit(Either.Left(e))
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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}")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
)
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
}
|
@ -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
|
||||
)
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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 ?: "Ошибка"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
)
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package org.example.presenceapp.ui.feature.login.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CheckboxDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.example.presenceapp.ui.feature.commons.CommonRegularText
|
||||
import org.example.presenceapp.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
fun LoginCheckBox(
|
||||
check: Boolean,
|
||||
onCheck: (Boolean) -> Unit,
|
||||
top: Int
|
||||
){
|
||||
Row(
|
||||
modifier = Modifier.padding(top = top.dp).fillMaxWidth().height(44.dp),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = check,
|
||||
onCheckedChange = onCheck,
|
||||
modifier = Modifier.size(18.dp),
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = AppTheme.colors.black,
|
||||
checkmarkColor = AppTheme.colors.white,
|
||||
uncheckedColor = AppTheme.colors.black
|
||||
)
|
||||
)
|
||||
CommonRegularText(
|
||||
text = "Пользовательское\nсоглашение",
|
||||
color = AppTheme.colors.black,
|
||||
textAlign = TextAlign.Start,
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user