From e50fb4fd8215bd225c03b812a04ea65dbee220b2 Mon Sep 17 00:00:00 2001 From: primakov Date: Sun, 23 Mar 2025 22:19:43 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=82=D1=81=D0=BB=D0=B5?= =?UTF-8?q?=D0=B6=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BF=D1=83=D0=BB?= =?UTF-8?q?=D1=8C=D1=81=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B2=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D0=B5=20LessonDetail?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B8=20=D0=BA=D0=BE=D0=BB=D0=B8=D1=87=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D0=B0=20=D1=81=D1=82=D1=83=D0=B4=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=BE=D0=B2.=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D0=BF=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=86=D0=B2=D0=B5=D1=82?= =?UTF-8?q?=D0=B0=20=D0=BD=D0=B0=20=D0=BE=D1=81=D0=BD=D0=BE=D0=B2=D0=B5=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=81=D0=B5=D1=89=D0=B0=D0=B5=D0=BC=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8.=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=20LessonList=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=82=D0=BE=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D1=82=D0=B8=D1=81=D1=82=D0=B8=D0=BA=D0=B8=20=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D0=B5=D1=89=D0=B0=D0=B5=D0=BC=D0=BE=D1=81=D1=82=D0=B8=20=D1=81?= =?UTF-8?q?=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=D0=BC=20=D0=BD=D0=BE=D0=B2=D1=8B=D1=85=20?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BB=D0=B5=D0=B9=20=D0=B8=20=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=BC=D0=B0=D1=86=D0=B8=D0=B9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/lesson-details.tsx | 79 +++++++++- src/pages/lesson-list/lesson-list.tsx | 210 ++++++++++++++++++++++---- 2 files changed, 251 insertions(+), 38 deletions(-) diff --git a/src/pages/lesson-details.tsx b/src/pages/lesson-details.tsx index 25196c2..3bfd693 100644 --- a/src/pages/lesson-details.tsx +++ b/src/pages/lesson-details.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useMemo } from 'react' +import React, { useEffect, useRef, useMemo, useState } from 'react' import { useParams, Link } from 'react-router-dom' import QRCode from 'qrcode' import { sha256 } from 'js-sha256' @@ -49,6 +49,11 @@ const LessonDetail = () => { // Создаем ref для отслеживания ранее присутствовавших студентов const prevPresentStudentsRef = useRef(new Set()) + // Добавляем состояние для отслеживания пульсации + const [isPulsing, setIsPulsing] = useState(false) + // Отслеживаем предыдущее количество студентов + const prevStudentCountRef = useRef(0) + const { isFetching, data: accessCode, @@ -75,6 +80,20 @@ const LessonDetail = () => { if (accessCode?.body) { const currentPresent = new Set(accessCode.body.lesson.students.map(s => s.sub)) + // Проверяем, изменилось ли количество студентов + const currentCount = accessCode.body.lesson.students.length; + if (prevStudentCountRef.current !== currentCount && prevStudentCountRef.current > 0) { + // Запускаем эффект пульсации + setIsPulsing(true); + // Сбрасываем эффект через 1.5 секунды + setTimeout(() => { + setIsPulsing(false); + }, 1500); + } + + // Обновляем предыдущее количество + prevStudentCountRef.current = currentCount; + // Очищаем флаги предыдущего состояния после задержки const timeoutId = setTimeout(() => { prevPresentStudentsRef.current = currentPresent @@ -169,6 +188,17 @@ const LessonDetail = () => { return allStudents.sort((a, b) => (a.present ? -1 : 1)) }, [accessCode?.body, AllStudents.data, prevPresentStudentsRef.current]) + // Функция для определения цвета на основе посещаемости + const getAttendanceColor = (attendance: number, total: number) => { + const percentage = total > 0 ? (attendance / total) * 100 : 0 + + 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' } } + } + return ( <> @@ -211,10 +241,49 @@ const LessonDetail = () => { boxShadow="md" > {formatDate(accessCode?.body?.lesson?.date, t('journal.pl.lesson.dateFormat'))}{' '} - {t('journal.pl.common.marked')} - {accessCode?.body?.lesson?.students?.length}{' '} - {AllStudents.isSuccess - ? `/ ${AllStudents?.data?.body?.length}` - : ''}{' '} + {t('journal.pl.common.marked')} - + {AllStudents.isSuccess && ( + + {accessCode?.body?.lesson?.students?.length} / {AllStudents?.data?.body?.length} + + )} + {!AllStudents.isSuccess && ( + {accessCode?.body?.lesson?.students?.length} + )}{' '} {t('journal.pl.common.people')} diff --git a/src/pages/lesson-list/lesson-list.tsx b/src/pages/lesson-list/lesson-list.tsx index 6b69c85..94e02ea 100644 --- a/src/pages/lesson-list/lesson-list.tsx +++ b/src/pages/lesson-list/lesson-list.tsx @@ -26,8 +26,14 @@ import { AlertDialogHeader, AlertDialogOverlay, useBreakpointValue, + Flex, + Menu, + MenuButton, + MenuList, + MenuItem, + useColorMode, } from '@chakra-ui/react' -import { AddIcon } from '@chakra-ui/icons' +import { AddIcon, EditIcon } from '@chakra-ui/icons' import { useTranslation } from 'react-i18next' import { useAppSelector } from '../../__data__/store' @@ -35,6 +41,7 @@ import { api } from '../../__data__/api/api' import { isTeacher } from '../../utils/user' import { Lesson } from '../../__data__/model' import { XlSpinner } from '../../components/xl-spinner' +import { qrCode } from '../../assets' import { LessonForm } from './components/lessons-form' import { Bar } from './components/bar' @@ -58,6 +65,7 @@ const LessonList = () => { error: errorGenerateLessons, isSuccess: isSuccessGenerateLessons }, ] = api.useGenerateLessonsMutation() + const { colorMode } = useColorMode() const [createLesson, crLQuery] = api.useCreateLessonMutation() const [deleteLesson, deletingRqst] = api.useDeleteLessonMutation() @@ -76,6 +84,23 @@ const LessonList = () => { [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 [] @@ -379,38 +404,157 @@ const LessonList = () => { ))} ) : ( - - - - - {isTeacher(user) && ( - - )} - - - {isTeacher(user) && } - - - - - {lessonCalc?.map(({ data: lessons, date }) => ( - - ))} - -
- {t('journal.pl.lesson.link')} - - {groupByDate ? t('journal.pl.lesson.time') : t('journal.pl.common.date')} - {t('journal.pl.common.name')}{t('journal.pl.lesson.action')}{t('journal.pl.common.marked')}
-
+ + {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')} + + + + )} + + + + + ))} + + + ))} + )}