From e16454a1963306d5a6e93aad0c5496610461c390 Mon Sep 17 00:00:00 2001 From: Agzamus Date: Sun, 20 Sep 2026 15:13:27 +0300 Subject: [PATCH] fix(teacher): group day slots by real lesson start time slotIndex is a per-department index of non-empty intervals, not a time. Merging all departments of a period by slotIndex and deriving the slot time from the first inserted lesson produced shifted times (e.g. Meltsov 10:00 shown at 11:45) depending on sync order. Group by lesson start minute instead so each department's real times are kept. Add debug logging for teacher schedule sync/build and debounce teacher search in picker. --- .../data/remote/TeacherScheduleParser.kt | 17 ++++++++ .../TeacherScheduleRepositoryImpl.kt | 43 ++++++++++++++++--- .../agzam/vyatsu/ui/picker/PickerViewModel.kt | 23 +++++----- .../ui/teacher/TeacherScheduleViewModel.kt | 8 ++++ 4 files changed, 76 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/agzam/vyatsu/data/remote/TeacherScheduleParser.kt b/app/src/main/java/agzam/vyatsu/data/remote/TeacherScheduleParser.kt index d2b5f0e..efad657 100644 --- a/app/src/main/java/agzam/vyatsu/data/remote/TeacherScheduleParser.kt +++ b/app/src/main/java/agzam/vyatsu/data/remote/TeacherScheduleParser.kt @@ -5,6 +5,7 @@ import agzam.vyatsu.domain.model.Day import agzam.vyatsu.domain.model.Lesson import agzam.vyatsu.domain.model.LessonType import agzam.vyatsu.domain.model.TimeSlot +import android.util.Log import org.jsoup.Jsoup import org.jsoup.nodes.Element import java.time.LocalDate @@ -13,6 +14,7 @@ import java.time.format.DateTimeFormatter object TeacherScheduleParser { + private const val TAG = "TeacherParserDebug" private val DAY_REGEX = Regex("""^(понедельник|вторник|среда|четверг|пятница|суббота|воскресенье)\s+(\d{2}\.\d{2}\.\d{2})$""") private val INTERVAL_REGEX = Regex("""^(\d{2}:\d{2})-(\d{2}:\d{2})$""") private val GROUP_TOKEN = Regex("""(? + Log.d(TAG, " mel: room=${l.room} group=${l.group} subj=${l.subject} type=${l.type} raw=${l.raw}") + } + } + } return ParsedTeacherSchedule(teachers, days) } diff --git a/app/src/main/java/agzam/vyatsu/data/repository/TeacherScheduleRepositoryImpl.kt b/app/src/main/java/agzam/vyatsu/data/repository/TeacherScheduleRepositoryImpl.kt index 175eeaa..8f9b44c 100644 --- a/app/src/main/java/agzam/vyatsu/data/repository/TeacherScheduleRepositoryImpl.kt +++ b/app/src/main/java/agzam/vyatsu/data/repository/TeacherScheduleRepositoryImpl.kt @@ -153,9 +153,20 @@ class TeacherScheduleRepositoryImpl( LocalDate.ofEpochDay(target.weekEnd).format(RANGE_FMT) ) val url = VyatsuUrls.teacherPrepodUrl(target.departmentId, season, range) + Log.d(TAG, "syncDepartment: dept=${target.departmentId} url=$url") val html = VyatsuHttpClient.getBytes(url).toString(Charsets.UTF_8) val parsed = TeacherScheduleParser.parse(html) + Log.d(TAG, "syncDepartment: dept=${target.departmentId} parsedDays=${parsed.days.size}") + parsed.days.forEach { day -> + day.intervals.forEach { slot -> + val mel = slot.lessons.filter { it.teacher == "Мельцов В.Ю." } + if (mel.isNotEmpty()) { + Log.d(TAG, "syncDepartment: dept=${target.departmentId} date=${day.date} slot=${slot.start}-${slot.end} melLessons=${mel.size}") + } + } + } + val dayEntities = parsed.days.map { TeacherDayEntity(scheduleId = 0, date = it.date.toEpochDay()) } @@ -181,6 +192,15 @@ class TeacherScheduleRepositoryImpl( } } } + Log.d(TAG, "syncDepartment: dept=${target.departmentId} -> lessons=${lessonEntities.size}") + val melLessonEntities = lessonEntities.filter { it.teacher == "Мельцов В.Ю." } + melLessonEntities.forEach { le -> + Log.d( + TAG, + "syncDepartment: dept=${target.departmentId} MEL date=${le.date} slotIndex=${le.slotIndex} " + + "start=$le.start end=$le.end raw=${le.raw}" + ) + } scheduleDao.replaceSchedule( TeacherScheduleEntity( departmentId = target.departmentId, @@ -222,18 +242,31 @@ class TeacherScheduleRepositoryImpl( .distinctBy { it.date } .map { day -> val lessons = lessonsByDate[day.date].orEmpty() - .groupBy { it.slotIndex } + Log.d( + TAG, + "build: date=${day.date} totalLessons=${lessons.size} " + + "groupedByStart=${lessons.groupBy { it.start }.keys.sorted()}" + ) + val slots = lessons.groupBy { it.start } .toSortedMap() - .map { (_, slotLessons) -> + .map { (startMin, slotLessons) -> + val first = slotLessons.first() + val times = slotLessons.map { "${it.start}-${it.end}" }.distinct() + val teachers = slotLessons.map { it.teacher }.distinct() + Log.d( + TAG, + "build: date=${day.date} start=$startMin firstEnd=$first.end " + + "times=$times teachers=$teachers" + ) TimeSlot( - start = LocalTime.of(slotLessons.first().start / 60, slotLessons.first().start % 60), - end = LocalTime.of(slotLessons.first().end / 60, slotLessons.first().end % 60), + start = LocalTime.of(startMin / 60, startMin % 60), + end = LocalTime.of(first.end / 60, first.end % 60), lessons = slotLessons .sortedBy { it.lessonIndex } .map { it.toDomain() } ) } - Day(date = LocalDate.ofEpochDay(day.date), intervals = lessons) + Day(date = LocalDate.ofEpochDay(day.date), intervals = slots) } return TeacherSchedule(period = period, days = days) } diff --git a/app/src/main/java/agzam/vyatsu/ui/picker/PickerViewModel.kt b/app/src/main/java/agzam/vyatsu/ui/picker/PickerViewModel.kt index a3bf236..72860bb 100644 --- a/app/src/main/java/agzam/vyatsu/ui/picker/PickerViewModel.kt +++ b/app/src/main/java/agzam/vyatsu/ui/picker/PickerViewModel.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -160,16 +161,18 @@ class PickerViewModel( teachersJob?.cancel() teachersJob = viewModelScope.launch { - teacherScheduleRepository.observeTeachers(period).collect { teachers -> - val query = _uiState.value.teacherQuery - val isLoading = _uiState.value.teachersLoading - _uiState.value = _uiState.value.copy( - teachers = teachers, - filteredTeachers = filterTeachers(teachers, query), - teachersLoading = isLoading && teachers.isEmpty(), - error = null - ) - } + teacherScheduleRepository.observeTeachers(period) + .debounce(300) + .collect { teachers -> + val query = _uiState.value.teacherQuery + val isLoading = _uiState.value.teachersLoading + _uiState.value = _uiState.value.copy( + teachers = teachers, + filteredTeachers = filterTeachers(teachers, query), + teachersLoading = isLoading && teachers.isEmpty(), + error = null + ) + } } } diff --git a/app/src/main/java/agzam/vyatsu/ui/teacher/TeacherScheduleViewModel.kt b/app/src/main/java/agzam/vyatsu/ui/teacher/TeacherScheduleViewModel.kt index 59d79b8..6eeac61 100644 --- a/app/src/main/java/agzam/vyatsu/ui/teacher/TeacherScheduleViewModel.kt +++ b/app/src/main/java/agzam/vyatsu/ui/teacher/TeacherScheduleViewModel.kt @@ -123,6 +123,14 @@ class TeacherScheduleViewModel( if (schedule == null) return val filteredDays = schedule.daysForTeacher(teacherName) + android.util.Log.d("TeacherSchedDebug", "apply: teacher=$teacherName periodWeek=$schedule.period rawDays=${schedule.days.size} filteredDays=${filteredDays.size}") + filteredDays.forEach { d -> + android.util.Log.d( + "TeacherSchedDebug", + "apply: filteredDay=${d.date} slots=${d.intervals.map { "${it.start}-${it.end}:${it.lessons.size}" }}" + ) + } + val today = ScheduleDates.moscowToday() val todayDate = if (isInPeriod(today, schedule.period)) today else null