feat(domain): add teacher models and repository interfaces
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
package agzam.vyatsu.domain.model
|
||||||
|
|
||||||
|
data class BulkSyncProgress(
|
||||||
|
val total: Int,
|
||||||
|
val done: Int,
|
||||||
|
val failed: Int
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package agzam.vyatsu.domain.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Кафедра ВятГУ.
|
||||||
|
* departmentId — идентификатор кафедры из data-kaf_period_id без последней цифры (как в URL).
|
||||||
|
*/
|
||||||
|
data class Department(
|
||||||
|
val departmentId: String,
|
||||||
|
val name: String,
|
||||||
|
val faculty: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package agzam.vyatsu.domain.model
|
||||||
|
|
||||||
|
data class ParsedDepartments(
|
||||||
|
val departments: List<Department>,
|
||||||
|
val dateRanges: Map<String, Map<Season, List<DateRange>>>
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package agzam.vyatsu.domain.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Преподаватель ВятГУ.
|
||||||
|
* Один и тот же преподаватель может вести занятия в нескольких кафедрах,
|
||||||
|
* поэтому модель не привязана к departmentId — расписание агрегируется
|
||||||
|
* по всем кафедрам периода.
|
||||||
|
*/
|
||||||
|
data class Teacher(
|
||||||
|
val name: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package agzam.vyatsu.domain.model
|
||||||
|
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Расписание преподавателя за период, агрегированное по всем кафедрам.
|
||||||
|
*/
|
||||||
|
data class TeacherSchedule(
|
||||||
|
val period: SchedulePeriod,
|
||||||
|
val days: List<Day>
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Возвращает дни периода только с занятиями указанного преподавателя.
|
||||||
|
* Слоты без занятий отбрасываются, порядок [Day] и [TimeSlot] сохраняется.
|
||||||
|
*
|
||||||
|
* Дедупликация — ТОЛЬКО по полному совпадению (дата + слот + аудитория +
|
||||||
|
* группа + предмет + тип + raw): два занятия коллапсируют в одно лишь когда
|
||||||
|
* они идентичны целиком (одно физическое занятие, прочитанное из файлов двух
|
||||||
|
* кафедр). Всё, что отличается хотя бы одним полем, остаётся и показывается.
|
||||||
|
*/
|
||||||
|
fun daysForTeacher(teacherName: String): List<Day> =
|
||||||
|
days.mapNotNull { day ->
|
||||||
|
val seen = HashSet<FullMatchKey>()
|
||||||
|
val intervals = day.intervals.mapNotNull { slot ->
|
||||||
|
val lessons = slot.lessons
|
||||||
|
.filter { it.teacher == teacherName }
|
||||||
|
.filter {
|
||||||
|
seen.add(
|
||||||
|
FullMatchKey(
|
||||||
|
date = day.date,
|
||||||
|
start = slot.start,
|
||||||
|
end = slot.end,
|
||||||
|
room = it.room,
|
||||||
|
group = it.group,
|
||||||
|
subject = it.subject,
|
||||||
|
type = it.type,
|
||||||
|
raw = it.raw
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (lessons.isEmpty()) null else slot.copy(lessons = lessons)
|
||||||
|
}
|
||||||
|
if (intervals.isEmpty()) null else day.copy(intervals = intervals)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class FullMatchKey(
|
||||||
|
val date: LocalDate,
|
||||||
|
val start: LocalTime,
|
||||||
|
val end: LocalTime,
|
||||||
|
val room: String?,
|
||||||
|
val group: String?,
|
||||||
|
val subject: String?,
|
||||||
|
val type: LessonType,
|
||||||
|
val raw: String
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,6 +9,14 @@ interface PreferencesRepository {
|
|||||||
suspend fun selectedDayDate(): String?
|
suspend fun selectedDayDate(): String?
|
||||||
suspend fun setSelectedDayDate(date: String)
|
suspend fun setSelectedDayDate(date: String)
|
||||||
|
|
||||||
|
suspend fun selectedTeacherId(): String? = null
|
||||||
|
suspend fun setSelectedTeacherId(id: String) = Unit
|
||||||
|
suspend fun selectedTeacherWeekStart(): String? = null
|
||||||
|
suspend fun setSelectedTeacherWeekStart(weekStart: String) = Unit
|
||||||
|
suspend fun selectedTeacherDayDate(): String? = null
|
||||||
|
suspend fun setSelectedTeacherDayDate(date: String) = Unit
|
||||||
|
suspend fun clearSelectedTeacher() = Unit
|
||||||
|
|
||||||
suspend fun autoUpdateEnabled(): Boolean
|
suspend fun autoUpdateEnabled(): Boolean
|
||||||
suspend fun setAutoUpdateEnabled(enabled: Boolean)
|
suspend fun setAutoUpdateEnabled(enabled: Boolean)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package agzam.vyatsu.domain.repository
|
||||||
|
|
||||||
|
import agzam.vyatsu.domain.model.Department
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface TeacherRepository {
|
||||||
|
fun observeDepartments(): Flow<List<Department>>
|
||||||
|
suspend fun getDepartments(): List<Department>
|
||||||
|
suspend fun getFaculties(): List<String>
|
||||||
|
suspend fun refreshIfStale(): Boolean
|
||||||
|
suspend fun forceRefresh(): Boolean
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package agzam.vyatsu.domain.repository
|
||||||
|
|
||||||
|
import agzam.vyatsu.domain.model.BulkSyncProgress
|
||||||
|
import agzam.vyatsu.domain.model.SchedulePeriod
|
||||||
|
import agzam.vyatsu.domain.model.Teacher
|
||||||
|
import agzam.vyatsu.domain.model.TeacherSchedule
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
interface TeacherScheduleRepository {
|
||||||
|
fun observeTeachers(period: SchedulePeriod): Flow<List<Teacher>>
|
||||||
|
fun observeSchedule(period: SchedulePeriod): Flow<TeacherSchedule?>
|
||||||
|
suspend fun syncPeriod(date: LocalDate): Flow<BulkSyncProgress>
|
||||||
|
suspend fun hasData(period: SchedulePeriod): Boolean
|
||||||
|
suspend fun clearAll()
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package agzam.vyatsu
|
||||||
|
|
||||||
|
import agzam.vyatsu.domain.model.Day
|
||||||
|
import agzam.vyatsu.domain.model.Lesson
|
||||||
|
import agzam.vyatsu.domain.model.LessonType
|
||||||
|
import agzam.vyatsu.domain.model.SchedulePeriod
|
||||||
|
import agzam.vyatsu.domain.model.Season
|
||||||
|
import agzam.vyatsu.domain.model.TeacherSchedule
|
||||||
|
import agzam.vyatsu.domain.model.TimeSlot
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
|
class TeacherScheduleDomainTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun daysForTeacherFiltersAndPreservesOrder() {
|
||||||
|
val date1 = LocalDate.of(2026, 9, 14)
|
||||||
|
val date2 = LocalDate.of(2026, 9, 15)
|
||||||
|
val slot1 = TimeSlot(
|
||||||
|
LocalTime.of(8, 20),
|
||||||
|
LocalTime.of(9, 50),
|
||||||
|
listOf(lesson(room = "4-302", teacher = "A"), lesson(room = "5-100", teacher = "B"))
|
||||||
|
)
|
||||||
|
val slot2 = TimeSlot(
|
||||||
|
LocalTime.of(10, 0),
|
||||||
|
LocalTime.of(11, 30),
|
||||||
|
listOf(lesson(room = "5-100", teacher = "B"))
|
||||||
|
)
|
||||||
|
val day2Slot = TimeSlot(
|
||||||
|
LocalTime.of(8, 20),
|
||||||
|
LocalTime.of(9, 50),
|
||||||
|
listOf(lesson(room = "5-100", teacher = "B"))
|
||||||
|
)
|
||||||
|
val schedule = TeacherSchedule(
|
||||||
|
period = SchedulePeriod(date1, date2, Season.AUTUMN),
|
||||||
|
days = listOf(Day(date1, listOf(slot1, slot2)), Day(date2, listOf(day2Slot)))
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = schedule.daysForTeacher("A")
|
||||||
|
|
||||||
|
assertEquals(1, result.size)
|
||||||
|
assertEquals(date1, result[0].date)
|
||||||
|
assertEquals(1, result[0].intervals.size)
|
||||||
|
assertEquals(LocalTime.of(8, 20), result[0].intervals[0].start)
|
||||||
|
assertEquals(1, result[0].intervals[0].lessons.size)
|
||||||
|
assertEquals("A", result[0].intervals[0].lessons[0].teacher)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun daysForTeacherDropsDayWithoutTeacher() {
|
||||||
|
val date1 = LocalDate.of(2026, 9, 14)
|
||||||
|
val date2 = LocalDate.of(2026, 9, 15)
|
||||||
|
val schedule = TeacherSchedule(
|
||||||
|
period = SchedulePeriod(date1, date2, Season.AUTUMN),
|
||||||
|
days = listOf(
|
||||||
|
Day(date1, listOf(slotOf(lesson(room = "4-302", teacher = "A")))),
|
||||||
|
Day(date2, listOf(slotOf(lesson(room = "5-100", teacher = "B"))))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = schedule.daysForTeacher("A")
|
||||||
|
|
||||||
|
assertEquals(1, result.size)
|
||||||
|
assertEquals(date1, result[0].date)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dedupCollapsesOnlyIdenticalLessons() {
|
||||||
|
val date = LocalDate.of(2026, 9, 14)
|
||||||
|
val raw = "4-302 Программирование Лекция ИВТб-3304-01-00"
|
||||||
|
val identical = lesson(
|
||||||
|
room = "4-302",
|
||||||
|
group = "ИВТб-3304-01-00",
|
||||||
|
teacher = "A",
|
||||||
|
raw = raw,
|
||||||
|
subject = "Программирование"
|
||||||
|
)
|
||||||
|
val differsInGroup = identical.copy(group = "ИВТб-3304-02-00")
|
||||||
|
val differsInRoom = identical.copy(room = "5-102")
|
||||||
|
|
||||||
|
val schedule = TeacherSchedule(
|
||||||
|
period = SchedulePeriod(date, date, Season.AUTUMN),
|
||||||
|
days = listOf(
|
||||||
|
Day(
|
||||||
|
date,
|
||||||
|
listOf(
|
||||||
|
TimeSlot(
|
||||||
|
LocalTime.of(8, 20),
|
||||||
|
LocalTime.of(9, 50),
|
||||||
|
listOf(identical, identical, differsInGroup, differsInRoom)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = schedule.daysForTeacher("A")
|
||||||
|
|
||||||
|
val lessons = result[0].intervals[0].lessons
|
||||||
|
assertEquals(3, lessons.size)
|
||||||
|
assertEquals(1, lessons.count { it == identical })
|
||||||
|
assertEquals(1, lessons.count { it.room == "4-302" && it.group == "ИВТб-3304-01-00" })
|
||||||
|
assertTrue(lessons.any { it.group == "ИВТб-3304-02-00" })
|
||||||
|
assertTrue(lessons.any { it.room == "5-102" })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun slotOf(lesson: Lesson) =
|
||||||
|
TimeSlot(LocalTime.of(8, 20), LocalTime.of(9, 50), listOf(lesson))
|
||||||
|
|
||||||
|
private fun lesson(
|
||||||
|
room: String,
|
||||||
|
group: String = "ИВТб-3304-01-00",
|
||||||
|
teacher: String,
|
||||||
|
raw: String = "$room Программирование Лекция $group",
|
||||||
|
subject: String = "Программирование"
|
||||||
|
) = Lesson(
|
||||||
|
raw = raw,
|
||||||
|
room = room,
|
||||||
|
group = group,
|
||||||
|
subject = subject,
|
||||||
|
type = LessonType.LECTURE,
|
||||||
|
teacher = teacher
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user