fix(teacher): register migration, containment period queries, concurrency guards
- Register AppDatabase.MIGRATION_3_4 so v3 databases upgrade without data loss - Query teacher schedule periods by containment (weekStart<=x AND weekEnd>=x) instead of exact weekStart, matching 2-week catalog windows - Clip buildTeacherSchedule days to the requested week range - Add weeksFor() to enumerate weeks of a catalog window for the UI selector - Serialize syncPeriod body with a Mutex to protect the unique index - Rethrow CancellationException before generic catch; version loadRevision checks so stale observers/loads are cancelled - Sync job tracking in PickerViewModel; keep loading true while empty - Drop dead selectTeacher helper
This commit is contained in:
@@ -40,14 +40,16 @@ interface TeacherScheduleDao {
|
||||
|
||||
@Query(
|
||||
"SELECT teacher_days.* FROM teacher_days INNER JOIN teacher_schedules ON teacher_schedules.id = teacher_days.scheduleId " +
|
||||
"WHERE teacher_schedules.season = :season AND teacher_schedules.weekStart = :weekStart ORDER BY teacher_days.date"
|
||||
"WHERE teacher_schedules.season = :season AND teacher_schedules.weekStart <= :weekStart " +
|
||||
"AND teacher_schedules.weekEnd >= :weekStart ORDER BY teacher_days.date"
|
||||
)
|
||||
fun observeDaysForPeriod(season: String, weekStart: Long): Flow<List<TeacherDayEntity>>
|
||||
|
||||
@Query(
|
||||
"SELECT teacher_lessons.* FROM teacher_lessons INNER JOIN teacher_days ON teacher_days.id = teacher_lessons.dayId " +
|
||||
"INNER JOIN teacher_schedules ON teacher_schedules.id = teacher_days.scheduleId " +
|
||||
"WHERE teacher_schedules.season = :season AND teacher_schedules.weekStart = :weekStart " +
|
||||
"WHERE teacher_schedules.season = :season AND teacher_schedules.weekStart <= :weekStart " +
|
||||
"AND teacher_schedules.weekEnd >= :weekStart " +
|
||||
"ORDER BY teacher_lessons.date, teacher_lessons.slotIndex, teacher_lessons.lessonIndex"
|
||||
)
|
||||
fun observeLessonsForPeriod(season: String, weekStart: Long): Flow<List<TeacherLessonEntity>>
|
||||
@@ -55,12 +57,14 @@ interface TeacherScheduleDao {
|
||||
@Query(
|
||||
"SELECT DISTINCT teacher FROM teacher_lessons INNER JOIN teacher_days ON teacher_days.id = teacher_lessons.dayId " +
|
||||
"INNER JOIN teacher_schedules ON teacher_schedules.id = teacher_days.scheduleId " +
|
||||
"WHERE teacher_schedules.season = :season AND teacher_schedules.weekStart = :weekStart ORDER BY teacher"
|
||||
"WHERE teacher_schedules.season = :season AND teacher_schedules.weekStart <= :weekStart " +
|
||||
"AND teacher_schedules.weekEnd >= :weekStart ORDER BY teacher"
|
||||
)
|
||||
fun observeDistinctTeachers(season: String, weekStart: Long): Flow<List<String>>
|
||||
|
||||
@Query(
|
||||
"SELECT EXISTS(SELECT 1 FROM teacher_schedules WHERE season = :season AND weekStart = :weekStart)"
|
||||
"SELECT EXISTS(SELECT 1 FROM teacher_schedules WHERE season = :season " +
|
||||
"AND weekStart <= :weekStart AND weekEnd >= :weekStart)"
|
||||
)
|
||||
suspend fun hasSchedulesForPeriod(season: String, weekStart: Long): Boolean
|
||||
}
|
||||
|
||||
@@ -26,17 +26,24 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.temporal.TemporalAdjusters
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
class TeacherScheduleRepositoryImpl(
|
||||
private val scheduleDao: TeacherScheduleDao,
|
||||
private val teacherDao: TeacherDao
|
||||
) : TeacherScheduleRepository {
|
||||
|
||||
private val syncMutex = Mutex()
|
||||
|
||||
override fun observeSchedule(period: SchedulePeriod): Flow<TeacherSchedule?> =
|
||||
combine(
|
||||
scheduleDao.observeDaysForPeriod(period.season.name, period.weekStart.toEpochDay()),
|
||||
@@ -50,54 +57,60 @@ class TeacherScheduleRepositoryImpl(
|
||||
.map { names -> names.map { name -> Teacher(name = name) } }
|
||||
|
||||
override suspend fun syncPeriod(date: LocalDate): Flow<BulkSyncProgress> = flow {
|
||||
val targets = withContext(Dispatchers.IO) {
|
||||
teacherDao.getPeriodsForDate(date.toEpochDay())
|
||||
}
|
||||
if (targets.isEmpty()) {
|
||||
emit(BulkSyncProgress(0, 0, 0))
|
||||
return@flow
|
||||
}
|
||||
|
||||
targets.map { it.season to it.weekStart }
|
||||
.distinct()
|
||||
.forEach { (season, weekStart) ->
|
||||
withContext(Dispatchers.IO) {
|
||||
scheduleDao.deletePeriod(season.name, weekStart)
|
||||
}
|
||||
// Unique index (departmentId, season, weekStart) breaks on interleaved
|
||||
// delete+insert from concurrent syncPeriod() callers.
|
||||
syncMutex.withLock {
|
||||
val targets = withContext(Dispatchers.IO) {
|
||||
teacherDao.getPeriodsForDate(date.toEpochDay())
|
||||
}
|
||||
if (targets.isEmpty()) {
|
||||
emit(BulkSyncProgress(0, 0, 0))
|
||||
return@withLock
|
||||
}
|
||||
|
||||
val total = targets.size
|
||||
val semaphore = Semaphore(PARALLELISM)
|
||||
val jobs = coroutineScope {
|
||||
targets.map { target ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
try {
|
||||
withContext(Dispatchers.IO) { syncDepartment(target) }
|
||||
true
|
||||
} catch (e: Throwable) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"sync: failed for department ${target.departmentId} " +
|
||||
"season ${target.season} week ${target.weekStart} " +
|
||||
"(${e::class.simpleName}: ${e.message})",
|
||||
e
|
||||
)
|
||||
false
|
||||
targets.map { it.season to it.weekStart }
|
||||
.distinct()
|
||||
.forEach { (season, weekStart) ->
|
||||
withContext(Dispatchers.IO) {
|
||||
scheduleDao.deletePeriod(season.name, weekStart)
|
||||
}
|
||||
}
|
||||
|
||||
val total = targets.size
|
||||
val semaphore = Semaphore(PARALLELISM)
|
||||
val jobs = coroutineScope {
|
||||
targets.map { target ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
try {
|
||||
withContext(Dispatchers.IO) { syncDepartment(target) }
|
||||
true
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"sync: failed for department ${target.departmentId} " +
|
||||
"season ${target.season} week ${target.weekStart} " +
|
||||
"(${e::class.simpleName}: ${e.message})",
|
||||
e
|
||||
)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var done = 0
|
||||
var failed = 0
|
||||
for (job in jobs) {
|
||||
if (job.await()) done++ else failed++
|
||||
emit(BulkSyncProgress(total, done + failed, failed))
|
||||
}
|
||||
var done = 0
|
||||
var failed = 0
|
||||
for (job in jobs) {
|
||||
if (job.await()) done++ else failed++
|
||||
emit(BulkSyncProgress(total, done + failed, failed))
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) { trimToRecent(keepDistinctPeriods = 2) }
|
||||
withContext(Dispatchers.IO) { trimToRecent(keepDistinctPeriods = 2) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun hasData(period: SchedulePeriod): Boolean =
|
||||
@@ -105,6 +118,30 @@ class TeacherScheduleRepositoryImpl(
|
||||
scheduleDao.hasSchedulesForPeriod(period.season.name, period.weekStart.toEpochDay())
|
||||
}
|
||||
|
||||
override suspend fun weeksFor(date: LocalDate): List<SchedulePeriod> =
|
||||
withContext(Dispatchers.IO) {
|
||||
teacherDao.getPeriodsForDate(date.toEpochDay())
|
||||
.distinctBy { it.season to (it.weekStart to it.weekEnd) }
|
||||
.flatMap { window ->
|
||||
val windowStart = LocalDate.ofEpochDay(window.weekStart)
|
||||
val windowEnd = LocalDate.ofEpochDay(window.weekEnd)
|
||||
generateSequence(
|
||||
windowStart.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
|
||||
) { it.plusWeeks(1) }
|
||||
.takeWhile { it <= windowEnd }
|
||||
.map { monday ->
|
||||
val weekEnd = monday.plusDays(6)
|
||||
SchedulePeriod(
|
||||
weekStart = monday,
|
||||
weekEnd = if (weekEnd.isAfter(windowEnd)) windowEnd else weekEnd,
|
||||
season = window.season
|
||||
)
|
||||
}
|
||||
}
|
||||
.distinctBy { it.season to it.weekStart }
|
||||
.sortedBy { it.weekStart }
|
||||
}
|
||||
|
||||
override suspend fun clearAll() = withContext(Dispatchers.IO) {
|
||||
scheduleDao.deleteAll()
|
||||
}
|
||||
@@ -182,9 +219,13 @@ class TeacherScheduleRepositoryImpl(
|
||||
dayEntities: List<TeacherDayEntity>,
|
||||
lessonEntities: List<TeacherLessonEntity>
|
||||
): TeacherSchedule? {
|
||||
if (dayEntities.isEmpty()) return null
|
||||
val weekDays = dayEntities
|
||||
.filter {
|
||||
it.date >= period.weekStart.toEpochDay() && it.date <= period.weekEnd.toEpochDay()
|
||||
}
|
||||
if (weekDays.isEmpty()) return null
|
||||
val lessonsByDate = lessonEntities.groupBy { it.date }
|
||||
val days = dayEntities
|
||||
val days = weekDays
|
||||
.sortedBy { it.date }
|
||||
.distinctBy { it.date }
|
||||
.map { day ->
|
||||
|
||||
@@ -23,6 +23,7 @@ class AppContainer(context: Context) {
|
||||
AppDatabase::class.java,
|
||||
"vyatsu_schedule.db"
|
||||
)
|
||||
.addMigrations(AppDatabase.MIGRATION_3_4)
|
||||
.fallbackToDestructiveMigration(dropAllTables = true)
|
||||
.build()
|
||||
|
||||
|
||||
@@ -12,5 +12,6 @@ interface TeacherScheduleRepository {
|
||||
fun observeSchedule(period: SchedulePeriod): Flow<TeacherSchedule?>
|
||||
suspend fun syncPeriod(date: LocalDate): Flow<BulkSyncProgress>
|
||||
suspend fun hasData(period: SchedulePeriod): Boolean
|
||||
suspend fun weeksFor(date: LocalDate): List<SchedulePeriod>
|
||||
suspend fun clearAll()
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import java.time.DayOfWeek
|
||||
import java.time.temporal.TemporalAdjusters
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -45,6 +46,7 @@ class PickerViewModel(
|
||||
val uiState: StateFlow<PickerUiState> = _uiState.asStateFlow()
|
||||
|
||||
private var teachersJob: kotlinx.coroutines.Job? = null
|
||||
private var syncJob: kotlinx.coroutines.Job? = null
|
||||
|
||||
init {
|
||||
groupsLoad()
|
||||
@@ -139,11 +141,20 @@ class PickerViewModel(
|
||||
|
||||
_uiState.value = _uiState.value.copy(syncProgress = null, teachersLoading = true)
|
||||
|
||||
viewModelScope.launch {
|
||||
runCatching {
|
||||
syncJob?.cancel()
|
||||
syncJob = viewModelScope.launch {
|
||||
try {
|
||||
teacherScheduleRepository.syncPeriod(today).collect { progress ->
|
||||
_uiState.value = _uiState.value.copy(syncProgress = progress)
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
error = e.message ?: "Не удалось загрузить расписание преподавателей"
|
||||
)
|
||||
} finally {
|
||||
_uiState.value = _uiState.value.copy(teachersLoading = false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,10 +162,11 @@ class PickerViewModel(
|
||||
teachersJob = viewModelScope.launch {
|
||||
teacherScheduleRepository.observeTeachers(period).collect { teachers ->
|
||||
val query = _uiState.value.teacherQuery
|
||||
val isLoading = _uiState.value.teachersLoading
|
||||
_uiState.value = _uiState.value.copy(
|
||||
teachersLoading = false,
|
||||
teachers = teachers,
|
||||
filteredTeachers = filterTeachers(teachers, query),
|
||||
teachersLoading = isLoading && teachers.isEmpty(),
|
||||
error = null
|
||||
)
|
||||
}
|
||||
@@ -178,11 +190,4 @@ class PickerViewModel(
|
||||
fun retryTeachers() {
|
||||
syncAndObserveTeachers()
|
||||
}
|
||||
|
||||
fun selectTeacher(teacher: Teacher) {
|
||||
viewModelScope.launch {
|
||||
preferencesRepository.clearSelectedGroup()
|
||||
preferencesRepository.setSelectedTeacherId(teacher.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,13 +132,17 @@ fun TeacherScheduleScreen(viewModel: TeacherScheduleViewModel, onSwitchTeacher:
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
WeekSelector(
|
||||
periods = listOf(schedule.period),
|
||||
periods = state.weeks.ifEmpty { listOf(schedule.period) },
|
||||
selectedWeekStart = state.weekStart,
|
||||
todayWeekStart = state.todayDate?.let { todayDate ->
|
||||
schedule.period.weekStart.takeIf {
|
||||
!todayDate.isBefore(it) && !todayDate.isAfter(it.plusDays(6))
|
||||
todayWeekStart = state.todayDate
|
||||
?.let { todayDate ->
|
||||
state.weeks.firstOrNull { period ->
|
||||
!todayDate.isBefore(period.weekStart) && !todayDate.isAfter(period.weekEnd)
|
||||
}?.weekStart
|
||||
}
|
||||
},
|
||||
?: schedule.period.weekStart.takeIf {
|
||||
state.todayDate?.let { today -> !today.isBefore(it) && !today.isAfter(it.plusDays(6)) } == true
|
||||
},
|
||||
onSelect = viewModel::selectWeek
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.temporal.TemporalAdjusters
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -27,6 +28,8 @@ class TeacherScheduleViewModel(
|
||||
private var currentTeacherName: String? = null
|
||||
private var currentPeriod: SchedulePeriod? = null
|
||||
private var scheduleJob: Job? = null
|
||||
private var weeksJob: Job? = null
|
||||
private var loadRevision = 0
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
@@ -40,52 +43,76 @@ class TeacherScheduleViewModel(
|
||||
}
|
||||
|
||||
fun selectTeacher(name: String) {
|
||||
val revision = ++loadRevision
|
||||
currentTeacherName = name
|
||||
_uiState.value = _uiState.value.copy(noTeacher = false, teacherName = name, isLoading = true, loadError = false)
|
||||
viewModelScope.launch {
|
||||
preferencesRepository.setSelectedTeacherId(name)
|
||||
try {
|
||||
preferencesRepository.setSelectedTeacherId(name)
|
||||
|
||||
val savedWeekStart = runCatching {
|
||||
preferencesRepository.selectedTeacherWeekStart()?.let(LocalDate::parse)
|
||||
}.getOrNull()
|
||||
val savedDayDate = runCatching {
|
||||
preferencesRepository.selectedTeacherDayDate()?.let(LocalDate::parse)
|
||||
}.getOrNull()
|
||||
val savedWeekStart = runCatching {
|
||||
preferencesRepository.selectedTeacherWeekStart()?.let(LocalDate::parse)
|
||||
}.getOrNull()
|
||||
val savedDayDate = runCatching {
|
||||
preferencesRepository.selectedTeacherDayDate()?.let(LocalDate::parse)
|
||||
}.getOrNull()
|
||||
|
||||
val today = ScheduleDates.moscowToday()
|
||||
val weekStart = savedWeekStart
|
||||
?: today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
|
||||
_uiState.value = _uiState.value.copy(
|
||||
noTeacher = false,
|
||||
teacherName = name,
|
||||
weekStart = weekStart,
|
||||
dayDate = savedDayDate
|
||||
)
|
||||
val today = ScheduleDates.moscowToday()
|
||||
val weekStart = savedWeekStart
|
||||
?: today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
|
||||
if (revision != loadRevision) return@launch
|
||||
_uiState.value = _uiState.value.copy(
|
||||
noTeacher = false,
|
||||
teacherName = name,
|
||||
weekStart = weekStart,
|
||||
dayDate = savedDayDate
|
||||
)
|
||||
|
||||
val period = SchedulePeriod(
|
||||
weekStart = weekStart,
|
||||
weekEnd = weekStart.plusDays(6),
|
||||
season = ScheduleDates.currentSeason(today)
|
||||
)
|
||||
loadSchedule(period)
|
||||
val period = SchedulePeriod(
|
||||
weekStart = weekStart,
|
||||
weekEnd = weekStart.plusDays(6),
|
||||
season = ScheduleDates.currentSeason(today)
|
||||
)
|
||||
loadSchedule(period, revision)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadSchedule(period: SchedulePeriod) {
|
||||
private suspend fun loadSchedule(period: SchedulePeriod, revision: Int) {
|
||||
if (revision != loadRevision) return
|
||||
scheduleJob?.cancel()
|
||||
scheduleJob = null
|
||||
currentPeriod = period
|
||||
|
||||
if (!scheduleRepository.hasData(period)) {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true, loadError = false)
|
||||
runCatching { scheduleRepository.syncPeriod(period.weekStart).collect { } }
|
||||
try {
|
||||
scheduleRepository.syncPeriod(period.weekStart).collect { }
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
if (revision != loadRevision) return
|
||||
if (!scheduleRepository.hasData(period)) {
|
||||
_uiState.value = _uiState.value.copy(isLoading = false, loadError = true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
weeksJob?.cancel()
|
||||
weeksJob = viewModelScope.launch {
|
||||
val weeks = scheduleRepository.weeksFor(period.weekStart)
|
||||
if (revision != loadRevision) return@launch
|
||||
_uiState.value = _uiState.value.copy(weeks = weeks)
|
||||
}
|
||||
|
||||
scheduleJob = viewModelScope.launch {
|
||||
scheduleRepository.observeSchedule(period).collect { schedule ->
|
||||
if (revision != loadRevision) {
|
||||
throw CancellationException("stale schedule observation")
|
||||
}
|
||||
currentTeacherName?.let { applySchedule(schedule, it) }
|
||||
}
|
||||
}
|
||||
@@ -122,21 +149,27 @@ class TeacherScheduleViewModel(
|
||||
val state = _uiState.value
|
||||
val name = state.teacherName ?: return
|
||||
if (state.weekStart == weekStart) return
|
||||
val revision = ++loadRevision
|
||||
viewModelScope.launch {
|
||||
preferencesRepository.setSelectedTeacherWeekStart(weekStart.toString())
|
||||
_uiState.value = _uiState.value.copy(
|
||||
weekStart = weekStart,
|
||||
dayDate = null,
|
||||
schedule = null,
|
||||
isLoading = true,
|
||||
loadError = false
|
||||
)
|
||||
val period = SchedulePeriod(
|
||||
weekStart = weekStart,
|
||||
weekEnd = weekStart.plusDays(6),
|
||||
season = ScheduleDates.currentSeason(weekStart)
|
||||
)
|
||||
loadSchedule(period)
|
||||
try {
|
||||
preferencesRepository.setSelectedTeacherWeekStart(weekStart.toString())
|
||||
if (revision != loadRevision) return@launch
|
||||
_uiState.value = _uiState.value.copy(
|
||||
weekStart = weekStart,
|
||||
dayDate = null,
|
||||
schedule = null,
|
||||
isLoading = true,
|
||||
loadError = false
|
||||
)
|
||||
val period = SchedulePeriod(
|
||||
weekStart = weekStart,
|
||||
weekEnd = weekStart.plusDays(6),
|
||||
season = ScheduleDates.currentSeason(weekStart)
|
||||
)
|
||||
loadSchedule(period, revision)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,14 +186,26 @@ class TeacherScheduleViewModel(
|
||||
|
||||
fun refresh() {
|
||||
val period = currentPeriod ?: return
|
||||
val revision = ++loadRevision
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true, loadError = false)
|
||||
runCatching { scheduleRepository.syncPeriod(ScheduleDates.moscowToday()).collect { } }
|
||||
loadSchedule(period)
|
||||
try {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true, loadError = false)
|
||||
try {
|
||||
scheduleRepository.syncPeriod(ScheduleDates.moscowToday()).collect { }
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
if (revision != loadRevision) return@launch
|
||||
loadSchedule(period, revision)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun switchTeacher() {
|
||||
loadRevision++
|
||||
viewModelScope.launch {
|
||||
currentTeacherName = null
|
||||
currentPeriod = null
|
||||
@@ -183,5 +228,6 @@ data class TeacherScheduleUiState(
|
||||
val teacherName: String? = null,
|
||||
val weekStart: LocalDate? = null,
|
||||
val dayDate: LocalDate? = null,
|
||||
val todayDate: LocalDate? = null
|
||||
val todayDate: LocalDate? = null,
|
||||
val weeks: List<SchedulePeriod> = emptyList()
|
||||
)
|
||||
Reference in New Issue
Block a user