package agzam.vyatsu.ui.schedule import agzam.vyatsu.domain.model.Day import agzam.vyatsu.domain.model.SchedulePeriod import agzam.vyatsu.domain.model.TimeSlot import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import java.time.DayOfWeek import java.time.LocalDate import java.time.format.DateTimeFormatter @OptIn(ExperimentalMaterial3Api::class) @Composable fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) { val state by viewModel.uiState.collectAsState() Column(Modifier.fillMaxSize()) { TopAppBar( title = { Text("Расписание") }, actions = { TextButton(onClick = { onSwitchGroup() }) { Text("Сменить группу") } IconButton(onClick = { viewModel.refresh() }) { Icon(Icons.Default.Refresh, contentDescription = "Обновить") } }, windowInsets = WindowInsets.systemBars.only(androidx.compose.foundation.layout.WindowInsetsSides.Top) ) Column(Modifier.fillMaxSize().padding(horizontal = 16.dp, vertical = 0.dp)) { when { state.isSyncing && state.schedule == null -> { Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center ) { CircularProgressIndicator() } } state.schedule == null -> { Column( Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { Text( "Не удалось загрузить расписание", style = MaterialTheme.typography.bodyLarge ) TextButton(onClick = { viewModel.refresh() }) { Text("Повторить") } } } else -> { val schedule = state.schedule!! val weekDays = state.weekStart ?.let { ws -> schedule.periods.firstOrNull { it.weekStart == ws } } ?.let { schedule.daysOf(it) } .orEmpty() val day = schedule.days.firstOrNull { it.date == state.dayDate } if (day == null) { Text("Занятий нет", style = MaterialTheme.typography.bodyLarge) } else { val isTodayDay = day.date == state.todayDate val dayLabel = if (isTodayDay) "Сегодня " else "" Text( "$dayLabel ${dayName(day.date.dayOfWeek)} · ${day.date.format(FULL_DATE)}", style = MaterialTheme.typography.titleMedium ) Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { TextButton(onClick = { viewModel.clearSelectedPeriod() }) { Text("Очистить эту неделю") } TextButton(onClick = { viewModel.clearAll() }) { Text("Очистить всё") } } HorizontalDivider(Modifier.padding(vertical = 8.dp)) WeekChips( periods = schedule.periods, selectedWeekStart = state.weekStart, todayWeekStart = state.todayDate?.let { schedule.periodOf(it)?.weekStart }, onSelect = viewModel::selectWeek ) HorizontalDivider(Modifier.padding(vertical = 8.dp)) DayChips( days = weekDays, selectedDate = state.dayDate, todayDate = state.todayDate, onSelect = viewModel::selectDay ) HorizontalDivider(Modifier.padding(vertical = 8.dp)) if (day.intervals.all { it.lessons.isEmpty() }) { Text("Занятий нет", style = MaterialTheme.typography.bodyLarge) } else { LazyColumn( modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(8.dp) ) { itemsIndexed(day.intervals) { slotIndex, slot -> LessonSlot(slotIndex = slotIndex, slot = slot) } } } } } } } } } @Composable private fun WeekChips( periods: List, selectedWeekStart: LocalDate?, todayWeekStart: LocalDate?, onSelect: (LocalDate) -> Unit ) { LazyRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) { items(periods, key = { it.weekStart.toEpochDay() }) { period -> val selected = selectedWeekStart == period.weekStart val isTodayWeek = todayWeekStart == period.weekStart FilterChip( selected = selected, onClick = { onSelect(period.weekStart) }, label = { Text( weekLabel(period), style = MaterialTheme.typography.labelLarge ) }, modifier = Modifier.padding(vertical = 2.dp), border = if (isTodayWeek && !selected) { FilterChipDefaults.filterChipBorder( borderColor = MaterialTheme.colorScheme.primary, borderWidth = 1.dp, enabled = true, selected = false ) } else { FilterChipDefaults.filterChipBorder(enabled = true, selected = selected) }, shape = RoundedCornerShape(8.dp) ) } } } @Composable private fun DayChips( days: List, selectedDate: LocalDate?, todayDate: LocalDate?, onSelect: (LocalDate) -> Unit ) { LazyRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) { items(days, key = { it.date.toEpochDay() }) { day -> val selected = selectedDate == day.date val isTodayChip = todayDate == day.date FilterChip( selected = selected, onClick = { onSelect(day.date) }, label = { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( dayName(day.date.dayOfWeek), style = MaterialTheme.typography.labelMedium ) Text( day.date.format(CHIP_DATE), style = MaterialTheme.typography.labelSmall, color = if (selected) { MaterialTheme.colorScheme.onSecondaryContainer } else { MaterialTheme.colorScheme.onSurfaceVariant } ) } }, modifier = Modifier.padding(vertical = 2.dp), border = if (isTodayChip && !selected) { FilterChipDefaults.filterChipBorder( borderColor = MaterialTheme.colorScheme.primary, borderWidth = 1.dp, enabled = true, selected = false ) } else { FilterChipDefaults.filterChipBorder(enabled = true, selected = selected) }, shape = RoundedCornerShape(8.dp) ) } } } private fun weekLabel(period: SchedulePeriod): String { val first = period.weekStart return "с ${first.format(CHIP_DATE)}" } @Composable private fun LessonSlot(slotIndex: Int, slot: TimeSlot) { Column(Modifier.fillMaxWidth()) { Text( "${slotIndex + 1} пара · ${slot.start.format(TIME)}–${slot.end.format(TIME)}", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary ) if (slot.lessons.isEmpty()) { Text("—", style = MaterialTheme.typography.bodyMedium) } else { slot.lessons.forEach { lesson -> Column(Modifier.fillMaxWidth().padding(vertical = 2.dp)) { Text( lesson.subject?.takeIf { it.isNotBlank() } ?: lesson.raw, style = MaterialTheme.typography.bodyLarge ) Text( listOfNotNull( lesson.type.label, lesson.subgroup, lesson.teacher, lesson.room ) .filter { it.isNotBlank() } .joinToString(" · "), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) } } } } } private fun dayName(dayOfWeek: DayOfWeek): String = when (dayOfWeek) { DayOfWeek.MONDAY -> "Пн" DayOfWeek.TUESDAY -> "Вт" DayOfWeek.WEDNESDAY -> "Ср" DayOfWeek.THURSDAY -> "Чт" DayOfWeek.FRIDAY -> "Пт" DayOfWeek.SATURDAY -> "Сб" DayOfWeek.SUNDAY -> "Вс" } private val FULL_DATE: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") private val CHIP_DATE: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM") private val TIME: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm")