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.
This commit is contained in:
2026-09-20 15:13:27 +03:00
parent ef81659521
commit e16454a196
4 changed files with 76 additions and 15 deletions
@@ -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("""(?<![\p{L}\p{N}])[А-ЯЁа-яё]{1,4}-\d{2,4}-\d{2}-\d{2}(?![\p{L}\p{N}])""")
@@ -60,6 +62,21 @@ object TeacherScheduleParser {
}
}
flushDay()
Log.d(TAG, "parse: teachers=$teachers days=${days.size}")
for (day in days) {
Log.d(TAG, " parse ${day.date}: intervals=${day.intervals.size}")
for (slot in day.intervals) {
val mel = slot.lessons.filter { it.teacher == "Мельцов В.Ю." }
val teachersInSlot = slot.lessons.map { it.teacher }.distinct()
Log.d(
TAG,
" slot ${slot.start}-${slot.end} total=${slot.lessons.size} meltsov=${mel.size} teachers=$teachersInSlot"
)
mel.forEach { l ->
Log.d(TAG, " mel: room=${l.room} group=${l.group} subj=${l.subject} type=${l.type} raw=${l.raw}")
}
}
}
return ParsedTeacherSchedule(teachers, days)
}
@@ -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)
}
@@ -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,7 +161,9 @@ class PickerViewModel(
teachersJob?.cancel()
teachersJob = viewModelScope.launch {
teacherScheduleRepository.observeTeachers(period).collect { teachers ->
teacherScheduleRepository.observeTeachers(period)
.debounce(300)
.collect { teachers ->
val query = _uiState.value.teacherQuery
val isLoading = _uiState.value.teachersLoading
_uiState.value = _uiState.value.copy(
@@ -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