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:
2026-09-19 23:32:09 +03:00
parent 39e4f5a04f
commit 1d55e84e96
7 changed files with 205 additions and 103 deletions
@@ -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)
}
}
}