From e50fb4fd8215bd225c03b812a04ea65dbee220b2 Mon Sep 17 00:00:00 2001
From: primakov <primakovpro@gmail.com>
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<string>())
   
+  // Добавляем состояние для отслеживания пульсации
+  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 (
     <>
       <BreadcrumbsWrapper>
@@ -211,10 +241,49 @@ const LessonDetail = () => {
             boxShadow="md"
           ><Box pb={3}>
           {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 && (
+            <Box 
+              as="span" 
+              px={2} 
+              py={1} 
+              ml={2}
+              borderRadius="md"
+              fontWeight="bold"
+              bg={getAttendanceColor(
+                accessCode?.body?.lesson?.students?.length || 0, 
+                AllStudents?.data?.body?.length || 1
+              ).bg}
+              color={getAttendanceColor(
+                accessCode?.body?.lesson?.students?.length || 0, 
+                AllStudents?.data?.body?.length || 1
+              ).color}
+              _dark={{ 
+                bg: getAttendanceColor(
+                  accessCode?.body?.lesson?.students?.length || 0, 
+                  AllStudents?.data?.body?.length || 1
+                ).dark.bg, 
+                color: getAttendanceColor(
+                  accessCode?.body?.lesson?.students?.length || 0, 
+                  AllStudents?.data?.body?.length || 1
+                ).dark.color 
+              }}
+              position="relative"
+              animation={isPulsing ? "pulse 1.5s ease-in-out" : "none"}
+              sx={{
+                '@keyframes pulse': {
+                  '0%': { transform: 'scale(1)' },
+                  '50%': { transform: 'scale(1.15)', boxShadow: '0 0 10px rgba(66, 153, 225, 0.7)' },
+                  '100%': { transform: 'scale(1)' }
+                }
+              }}
+            >
+              {accessCode?.body?.lesson?.students?.length} / {AllStudents?.data?.body?.length}
+            </Box>
+          )}
+          {!AllStudents.isSuccess && (
+            <span> {accessCode?.body?.lesson?.students?.length}</span>
+          )}{' '}
           {t('journal.pl.common.people')}
         </Box>
             <a href={userUrl}>
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 = () => {
             ))}
           </Box>
         ) : (
-          <TableContainer whiteSpace="wrap" pb={13}>
-            <Table variant="striped" colorScheme="cyan">
-              <Thead>
-                <Tr>
-                  {isTeacher(user) && (
-                    <Th align="center" width={1}>
-                      {t('journal.pl.lesson.link')}
-                    </Th>
-                  )}
-                  <Th textAlign="center" width={1}>
-                    {groupByDate ? t('journal.pl.lesson.time') : t('journal.pl.common.date')}
-                  </Th>
-                  <Th width="100%">{t('journal.pl.common.name')}</Th>
-                  {isTeacher(user) && <Th>{t('journal.pl.lesson.action')}</Th>}
-                  <Th isNumeric>{t('journal.pl.common.marked')}</Th>
-                </Tr>
-              </Thead>
-              <Tbody>
-                {lessonCalc?.map(({ data: lessons, date }) => (
-                  <LessonItems
-                    courseId={courseId}
-                    date={date}
-                    isTeacher={isTeacher(user)}
-                    lessons={lessons}
-                    setlessonToDelete={setlessonToDelete}
-                    setEditLesson={handleEditLesson}
-                    key={date}
-                  />
-                ))}
-              </Tbody>
-            </Table>
-          </TableContainer>
+          <Box pb={13}>
+            {lessonCalc?.map(({ data: lessons, date }) => (
+              <Box key={date} mb={6}>
+                {date && (
+                  <Box 
+                    p={3} 
+                    mb={4} 
+                    bg="cyan.50" 
+                    borderRadius="md"
+                    _dark={{ bg: "cyan.900" }}
+                    boxShadow="sm"
+                  >
+                    <Text fontWeight="bold" fontSize="lg">
+                      {formatDate(date, 'DD MMMM YYYY')}
+                    </Text>
+                  </Box>
+                )}
+                <Box>
+                  {lessons.map((lesson, index) => (
+                    <Box 
+                      key={lesson.id} 
+                      borderRadius="lg" 
+                      boxShadow="md"
+                      bg="white"
+                      _dark={{ bg: "gray.700" }}
+                      transition="all 0.3s"
+                      _hover={{ 
+                        transform: "translateX(5px)", 
+                        boxShadow: "lg" 
+                      }}
+                      overflow="hidden"
+                      position="relative"
+                      mb={4}
+                      animation={`slideIn 0.6s ease-out ${index * 0.15}s both`}
+                      sx={{
+                        '@keyframes slideIn': {
+                          '0%': {
+                            opacity: 0,
+                            transform: 'translateX(-30px)'
+                          },
+                          '100%': {
+                            opacity: 1,
+                            transform: 'translateX(0)'
+                          }
+                        }
+                      }}
+                    >
+                      <Flex direction={{ base: "column", sm: "row" }}>
+                        {/* QR код и ссылка - левая часть карточки */}
+                        {isTeacher(user) && (
+                          <Link
+                            to={`${getNavigationValue('journal.main')}/lesson/${courseId}/${lesson.id}`}
+                          >
+                            <Box 
+                              p={4}
+                              bg="cyan.500" 
+                              _dark={{ bg: "cyan.600" }} 
+                              color="white"
+                              display="flex"
+                              alignItems="center"
+                              justifyContent="center"
+                              transition="all 0.2s"
+                              _hover={{ bg: "cyan.600", _dark: { bg: "cyan.700" } }}
+                              height="100%"
+                              minW="150px"
+                            >
+                              <Box 
+                                mr={0}
+                                bg="white" 
+                                borderRadius="md" 
+                                p={2}
+                                display="flex"
+                              >
+                                <img width={32} src={qrCode} alt="QR код" />
+                              </Box>
+                            </Box>
+                          </Link>
+                        )}
+                        
+                        {/* Содержимое карточки */}
+                        <Box p={5} w="100%" display="flex" flexDirection="column" justifyContent="space-between">
+                          <Flex mb={3} justify="space-between" align="center">
+                            {/* Название урока */}
+                            <Text fontWeight="bold" fontSize="xl" lineHeight="1.4" flex="1">
+                              {lesson.name}
+                            </Text>
+                            
+                            <Text fontSize="sm" color="gray.500" _dark={{ color: "gray.300" }} ml={3} whiteSpace="nowrap">
+                              {formatDate(lesson.date, groupByDate ? 'HH:mm' : 'HH:mm DD.MM.YY')}
+                            </Text>
+                          </Flex>
+                          
+                          {/* Нижняя часть с метками и действиями */}
+                          <Flex justifyContent="space-between" alignItems="center" mt={1}>
+                            <Flex align="center">
+                              <Text fontSize="sm" mr={2}>
+                                {t('journal.pl.common.marked')}:
+                              </Text>
+                              <Text 
+                                px={2} 
+                                py={1} 
+                                bg={getAttendanceColor(lesson.students.length).bg}
+                                color={getAttendanceColor(lesson.students.length).color}
+                                _dark={{ 
+                                  bg: getAttendanceColor(lesson.students.length).dark.bg, 
+                                  color: getAttendanceColor(lesson.students.length).dark.color 
+                                }}
+                                borderRadius="md"
+                                fontWeight="bold"
+                                fontSize="sm"
+                              >
+                                {lesson.students.length}
+                              </Text>
+                            </Flex>
+                            
+                            {isTeacher(user) && (
+                              <Menu>
+                                <MenuButton 
+                                  as={Button} 
+                                  size="sm" 
+                                  colorScheme="cyan" 
+                                  variant="ghost"
+                                  rightIcon={<EditIcon />}
+                                >
+                                  {t('journal.pl.edit')}
+                                </MenuButton>
+                                <MenuList>
+                                  <MenuItem
+                                    onClick={() => handleEditLesson(lesson)}
+                                    icon={<EditIcon />}
+                                  >
+                                    {t('journal.pl.edit')}
+                                  </MenuItem>
+                                  <MenuItem 
+                                    onClick={() => setlessonToDelete(lesson)}
+                                    color="red.500"
+                                  >
+                                    {t('journal.pl.delete')}
+                                  </MenuItem>
+                                </MenuList>
+                              </Menu>
+                            )}
+                          </Flex>
+                        </Box>
+                      </Flex>
+                    </Box>
+                  ))}
+                </Box>
+              </Box>
+            ))}
+          </Box>
         )}
       </Container>
     </>