import React, { useEffect, useMemo, useRef, useState } from 'react' import dayjs, { formatDate } from '../../utils/dayjs-config' import { generatePath, Link, useParams } from 'react-router-dom' import { getNavigationValue, getFeatures } from '@brojs/cli' import { Container, Box, Button, useToast, Toast, TableContainer, Table, Thead, Tr, Th, Tbody, Td, Text, AlertDialog, AlertDialogBody, AlertDialogContent, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, useBreakpointValue, Flex, Menu, MenuButton, MenuList, MenuItem, useColorMode, Portal, } from '@chakra-ui/react' import { AddIcon, EditIcon } from '@chakra-ui/icons' import { useTranslation } from 'react-i18next' import { useAppSelector } from '../../__data__/store' import { api } from '../../__data__/api/api' import { isTeacher } from '../../utils/user' import { Lesson } from '../../__data__/model' import { XlSpinner, useSetBreadcrumbs } from '../../components' import { qrCode } from '../../assets' import { LessonForm } from './components/lessons-form' import { Bar } from './components/bar' import { LessonItems } from './components/lesson-items' import { CourseStatistics } from './components/statistics' const features = getFeatures('journal') const barFeature = features?.['lesson.bar'] const groupByDate = features?.['group.by.date'] const courseStatistics = features?.['course.statistics'] const LessonList = () => { const { courseId } = useParams() const user = useAppSelector((s) => s.user) const { data, isLoading, error, isSuccess } = api.useLessonListQuery(courseId) const { data: courseData } = api.useGetCourseByIdQuery(courseId) const [generateLessonsMutation, { data: generateLessons, isLoading: isLoadingGenerateLessons, error: errorGenerateLessons, isSuccess: isSuccessGenerateLessons }, ] = api.useGenerateLessonsMutation() const { colorMode } = useColorMode() const [createLesson, crLQuery] = api.useCreateLessonMutation() const [deleteLesson, deletingRqst] = api.useDeleteLessonMutation() const [updateLesson, updateLessonRqst] = api.useUpdateLessonMutation() const [showForm, setShowForm] = useState(false) const [lessonToDelete, setlessonToDelete] = useState(null) const cancelRef = React.useRef() const toast = useToast() const toastRef = useRef(null) const createdLessonRef = useRef(null) const [editLesson, setEditLesson] = useState(null) const [suggestedLessonToCreate, setSuggestedLessonToCreate] = useState(null) const { t } = useTranslation() // Устанавливаем хлебные крошки для страницы списка уроков useSetBreadcrumbs([ { title: t('journal.pl.breadcrumbs.home'), path: '/' }, { title: courseData?.name || t('journal.pl.breadcrumbs.course'), isCurrentPage: true } ]) const sorted = useMemo( () => [...(data?.body || [])]?.sort((a, b) => (a.date > b.date ? 1 : -1)), [data, data?.body], ) // Найдем максимальное количество студентов среди всех уроков const maxStudents = useMemo(() => { if (!sorted || sorted.length === 0) return 1 const max = Math.max(...sorted.map(lesson => lesson.students?.length || 0)) return max > 0 ? max : 1 // Избегаем деления на ноль }, [sorted]) // Функция для определения цвета на основе посещаемости const getAttendanceColor = (attendance: number) => { const percentage = (attendance / maxStudents) * 100 if (percentage > 80) return { bg: 'green.100', color: 'green.800', dark: { bg: 'green.800', color: 'green.100' } } if (percentage > 60) return { bg: 'teal.100', color: 'teal.800', dark: { bg: 'teal.800', color: 'teal.100' } } if (percentage > 40) return { bg: 'yellow.100', color: 'yellow.800', dark: { bg: 'yellow.800', color: 'yellow.100' } } if (percentage > 20) return { bg: 'orange.100', color: 'orange.800', dark: { bg: 'orange.800', color: 'orange.100' } } return { bg: 'red.100', color: 'red.800', dark: { bg: 'red.800', color: 'red.100' } } } const lessonCalc = useMemo(() => { if (!isSuccess) { return [] } if (!groupByDate) { return [{ date: '', data: sorted }] } const lessonsData = [] for (let i = 0; i < sorted.length; i++) { const element = sorted[i] const find = lessonsData.find( (item) => dayjs(element.date).diff(dayjs(item.date), 'day') === 0, ) if (find) { find.data.push(element) } else { lessonsData.push({ date: element.date, data: [element], }) } } return lessonsData.sort((a, b) => (a.date < b.date ? 1 : -1)) }, [groupByDate, isSuccess, sorted]) useEffect(() => { if (isSuccessGenerateLessons) { console.log(generateLessons) // Проверяем корректность ответа API if (typeof generateLessons?.body === 'string') { toast({ title: t('journal.pl.lesson.aiGenerationError'), description: t('journal.pl.lesson.tryAgainLater'), status: 'error', duration: 5000, isClosable: true, }); } } }, [isSuccessGenerateLessons, generateLessons]) useEffect(() => { if (errorGenerateLessons) { toast({ title: t('journal.pl.lesson.aiGenerationError'), description: t('journal.pl.lesson.tryAgainLater'), status: 'error', duration: 5000, isClosable: true, }); } }, [errorGenerateLessons]) const onSubmit = (lessonData) => { toastRef.current = toast({ title: t('journal.pl.common.sending'), status: 'loading', duration: 9000, }) createdLessonRef.current = lessonData if (editLesson) updateLesson(lessonData) else createLesson({ courseId, ...lessonData }) } useEffect(() => { if (deletingRqst.isError) { toast({ title: (deletingRqst.error as any)?.error, status: 'error', duration: 3000, }) } if (deletingRqst.isSuccess) { const lesson = { ...lessonToDelete } toast({ status: 'warning', duration: 9000, render: ({ id, ...toastProps }) => ( {t('journal.pl.lesson.deletedMessage', { name: lesson.name })} } /> ), }) setlessonToDelete(null) } }, [deletingRqst.isLoading, deletingRqst.isSuccess, deletingRqst.isError]) useEffect(() => { if (crLQuery.isSuccess) { const toastProps = { title: t('journal.pl.lesson.created'), description: t('journal.pl.lesson.successMessage', { name: createdLessonRef.current?.name }), status: 'success' as const, duration: 9000, isClosable: true, } if (toastRef.current) toast.update(toastRef.current, toastProps) else toast(toastProps) setShowForm(false) } }, [crLQuery.isSuccess]) useEffect(() => { if (updateLessonRqst.isSuccess) { const toastProps = { title: t('journal.pl.lesson.updated'), description: t('journal.pl.lesson.updateMessage', { name: createdLessonRef.current?.name }), status: 'success' as const, duration: 9000, isClosable: true, } if (toastRef.current) toast.update(toastRef.current, toastProps) else toast(toastProps) setShowForm(false) } }, [updateLessonRqst.isSuccess]) // Обработчик выбора предложения ИИ в форме const handleSelectAiSuggestion = (suggestion) => { setSuggestedLessonToCreate(suggestion) } // Очищаем выбранную сгенерированную лекцию при закрытии формы const handleCancelForm = () => { setShowForm(false) setEditLesson(null) setSuggestedLessonToCreate(null) // Сбрасываем флаги генерации, чтобы при повторном открытии формы // генерация запускалась снова при необходимости // (особенно если была ошибка в предыдущей генерации) } // Обработчик открытия формы создания новой лекции const handleOpenForm = () => { setShowForm(true) // Запускаем генерацию лекций только при открытии формы создания новой лекции // и если генерация ещё не была запущена или предыдущая попытка завершилась с ошибкой const shouldGenerateAgain = !generateLessons || typeof generateLessons?.body === 'string' || errorGenerateLessons; if (isTeacher(user) && !editLesson && (!isLoadingGenerateLessons && shouldGenerateAgain)) { generateLessonsMutation(courseId) } } // Обработчик редактирования существующей лекции const handleEditLesson = (lesson) => { setEditLesson(lesson) setShowForm(true) // Не запускаем генерацию при редактировании } // Обработчик повторной генерации предложений ИИ const handleRetryAiGeneration = () => { if (isTeacher(user) && !isLoadingGenerateLessons) { generateLessonsMutation(courseId) } } // Добавляем определение размера экрана const isMobile = useBreakpointValue({ base: true, md: false }) if (isLoading) { return } return ( <> setlessonToDelete(null)} > {t('journal.pl.lesson.deleteConfirm', { date: formatDate(lessonToDelete?.date, 'DD.MM.YY') })} {t('journal.pl.lesson.deleteWarning')} {isTeacher(user) && ( {showForm ? ( ({ date: lesson.date, name: lesson.name }))} /> ) : ( )} )} {/* Статистика курса */} {!showForm && courseStatistics && ( )} {barFeature && sorted?.length > 1 && ( ({ lessonIndex: `#${index + 1}`, count: lesson.students.length, }))} /> )} {isMobile ? ( {lessonCalc?.map(({ data: lessons, date }) => ( ))} ) : ( {lessonCalc?.map(({ data: lessons, date }) => ( {date && ( {formatDate(date, 'DD MMMM YYYY')} )} {lessons.map((lesson, index) => ( {/* QR код и ссылка - левая часть карточки */} {isTeacher(user) && ( QR код )} {/* Содержимое карточки */} {/* Название урока */} {lesson.name} {formatDate(lesson.date, groupByDate ? 'HH:mm' : 'HH:mm DD.MM.YY')} {/* Нижняя часть с метками и действиями */} {t('journal.pl.common.marked')}: {lesson.students.length} {isTeacher(user) && ( } > {t('journal.pl.edit')} handleEditLesson(lesson)} icon={} > {t('journal.pl.edit')} setlessonToDelete(lesson)} color="red.500" > {t('journal.pl.delete')} )} ))} ))} )} ) } export default LessonList