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:
@@ -5,6 +5,7 @@ import agzam.vyatsu.domain.model.Day
|
|||||||
import agzam.vyatsu.domain.model.Lesson
|
import agzam.vyatsu.domain.model.Lesson
|
||||||
import agzam.vyatsu.domain.model.LessonType
|
import agzam.vyatsu.domain.model.LessonType
|
||||||
import agzam.vyatsu.domain.model.TimeSlot
|
import agzam.vyatsu.domain.model.TimeSlot
|
||||||
|
import android.util.Log
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
@@ -13,6 +14,7 @@ import java.time.format.DateTimeFormatter
|
|||||||
|
|
||||||
object TeacherScheduleParser {
|
object TeacherScheduleParser {
|
||||||
|
|
||||||
|
private const val TAG = "TeacherParserDebug"
|
||||||
private val DAY_REGEX = Regex("""^(понедельник|вторник|среда|четверг|пятница|суббота|воскресенье)\s+(\d{2}\.\d{2}\.\d{2})$""")
|
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 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}])""")
|
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()
|
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)
|
return ParsedTeacherSchedule(teachers, days)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -153,9 +153,20 @@ class TeacherScheduleRepositoryImpl(
|
|||||||
LocalDate.ofEpochDay(target.weekEnd).format(RANGE_FMT)
|
LocalDate.ofEpochDay(target.weekEnd).format(RANGE_FMT)
|
||||||
)
|
)
|
||||||
val url = VyatsuUrls.teacherPrepodUrl(target.departmentId, season, range)
|
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 html = VyatsuHttpClient.getBytes(url).toString(Charsets.UTF_8)
|
||||||
val parsed = TeacherScheduleParser.parse(html)
|
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 {
|
val dayEntities = parsed.days.map {
|
||||||
TeacherDayEntity(scheduleId = 0, date = it.date.toEpochDay())
|
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(
|
scheduleDao.replaceSchedule(
|
||||||
TeacherScheduleEntity(
|
TeacherScheduleEntity(
|
||||||
departmentId = target.departmentId,
|
departmentId = target.departmentId,
|
||||||
@@ -222,18 +242,31 @@ class TeacherScheduleRepositoryImpl(
|
|||||||
.distinctBy { it.date }
|
.distinctBy { it.date }
|
||||||
.map { day ->
|
.map { day ->
|
||||||
val lessons = lessonsByDate[day.date].orEmpty()
|
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()
|
.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(
|
TimeSlot(
|
||||||
start = LocalTime.of(slotLessons.first().start / 60, slotLessons.first().start % 60),
|
start = LocalTime.of(startMin / 60, startMin % 60),
|
||||||
end = LocalTime.of(slotLessons.first().end / 60, slotLessons.first().end % 60),
|
end = LocalTime.of(first.end / 60, first.end % 60),
|
||||||
lessons = slotLessons
|
lessons = slotLessons
|
||||||
.sortedBy { it.lessonIndex }
|
.sortedBy { it.lessonIndex }
|
||||||
.map { it.toDomain() }
|
.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)
|
return TeacherSchedule(period = period, days = days)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@@ -160,16 +161,18 @@ class PickerViewModel(
|
|||||||
|
|
||||||
teachersJob?.cancel()
|
teachersJob?.cancel()
|
||||||
teachersJob = viewModelScope.launch {
|
teachersJob = viewModelScope.launch {
|
||||||
teacherScheduleRepository.observeTeachers(period).collect { teachers ->
|
teacherScheduleRepository.observeTeachers(period)
|
||||||
val query = _uiState.value.teacherQuery
|
.debounce(300)
|
||||||
val isLoading = _uiState.value.teachersLoading
|
.collect { teachers ->
|
||||||
_uiState.value = _uiState.value.copy(
|
val query = _uiState.value.teacherQuery
|
||||||
teachers = teachers,
|
val isLoading = _uiState.value.teachersLoading
|
||||||
filteredTeachers = filterTeachers(teachers, query),
|
_uiState.value = _uiState.value.copy(
|
||||||
teachersLoading = isLoading && teachers.isEmpty(),
|
teachers = teachers,
|
||||||
error = null
|
filteredTeachers = filterTeachers(teachers, query),
|
||||||
)
|
teachersLoading = isLoading && teachers.isEmpty(),
|
||||||
}
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,14 @@ class TeacherScheduleViewModel(
|
|||||||
if (schedule == null) return
|
if (schedule == null) return
|
||||||
|
|
||||||
val filteredDays = schedule.daysForTeacher(teacherName)
|
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 today = ScheduleDates.moscowToday()
|
||||||
val todayDate = if (isInPeriod(today, schedule.period)) today else null
|
val todayDate = if (isInPeriod(today, schedule.period)) today else null
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user