Добавлено новое зависимость "react-icons" версии 5.5.0. Обновлен компонент AttendanceTable: добавлены эмоджи для отображения посещаемости студентов, возможность скрытия/показа таблицы, а также улучшена логика расчета статистики посещаемости.
This commit is contained in:
		
							parent
							
								
									49a26edabf
								
							
						
					
					
						commit
						d5b5838e51
					
				
							
								
								
									
										10
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -28,6 +28,7 @@ | ||||
|                 "react-dom": "^18.3.1", | ||||
|                 "react-helmet": "^6.1.0", | ||||
|                 "react-hook-form": "^7.51.2", | ||||
|                 "react-icons": "^5.5.0", | ||||
|                 "react-redux": "^9.1.0", | ||||
|                 "react-router-dom": "^6.22.1", | ||||
|                 "redux": "^5.0.1", | ||||
| @ -8894,6 +8895,15 @@ | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/react-icons": { | ||||
|             "version": "5.5.0", | ||||
|             "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", | ||||
|             "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", | ||||
|             "license": "MIT", | ||||
|             "peerDependencies": { | ||||
|                 "react": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/react-is": { | ||||
|             "version": "16.13.1", | ||||
|             "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", | ||||
|  | ||||
| @ -44,6 +44,7 @@ | ||||
|         "react-dom": "^18.3.1", | ||||
|         "react-helmet": "^6.1.0", | ||||
|         "react-hook-form": "^7.51.2", | ||||
|         "react-icons": "^5.5.0", | ||||
|         "react-redux": "^9.1.0", | ||||
|         "react-router-dom": "^6.22.1", | ||||
|         "redux": "^5.0.1", | ||||
|  | ||||
| @ -5,12 +5,10 @@ import { | ||||
|   Heading, | ||||
|   Container, | ||||
|   useColorMode, | ||||
|   IconButton, | ||||
|   Flex, | ||||
|   Spacer, | ||||
|   Badge | ||||
| } from '@chakra-ui/react' | ||||
| import { MoonIcon, SunIcon } from '@chakra-ui/icons' | ||||
| 
 | ||||
| import { PageLoader } from '../../components/page-loader/page-loader' | ||||
| import { useAttendanceData, useAttendanceStats } from './hooks' | ||||
| @ -18,7 +16,7 @@ import { AttendanceTable, StatsCard } from './components' | ||||
| 
 | ||||
| export const Attendance = () => { | ||||
|   const { courseId } = useParams() | ||||
|   const { colorMode, toggleColorMode } = useColorMode() | ||||
|   const { colorMode } = useColorMode() | ||||
|   const data = useAttendanceData(courseId) | ||||
|   const stats = useAttendanceStats(data) | ||||
| 
 | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import React from 'react' | ||||
| import React, { useState } from 'react' | ||||
| import { | ||||
|   Table, | ||||
|   Thead, | ||||
| @ -8,24 +8,41 @@ import { | ||||
|   Td, | ||||
|   Box, | ||||
|   useColorMode, | ||||
|   useTheme, | ||||
|   Button, | ||||
|   useToast, | ||||
|   Flex | ||||
|   Flex, | ||||
|   Collapse, | ||||
|   HStack, | ||||
|   Text, | ||||
|   Icon, | ||||
|   Tooltip, | ||||
|   Avatar, | ||||
|   AvatarBadge | ||||
| } from '@chakra-ui/react' | ||||
| import { CopyIcon } from '@chakra-ui/icons' | ||||
| import { CopyIcon, ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons' | ||||
| import { FaSmile, FaMeh, FaFrown, FaSadTear } from 'react-icons/fa' | ||||
| import dayjs from 'dayjs' | ||||
| import { sha256 } from 'js-sha256' | ||||
| import { ShortText } from './ShortText' | ||||
| import { AttendanceData } from '../hooks' | ||||
| 
 | ||||
| // Функция для получения URL аватарки через Gravatar
 | ||||
| function getGravatarURL(email) { | ||||
|   if (!email) return undefined | ||||
|   const address = String(email).trim().toLowerCase() | ||||
|   const hash = sha256(address) | ||||
| 
 | ||||
|   return `https://www.gravatar.com/avatar/${hash}?d=robohash` | ||||
| } | ||||
| 
 | ||||
| interface AttendanceTableProps { | ||||
|   data: AttendanceData | ||||
| } | ||||
| 
 | ||||
| export const AttendanceTable: React.FC<AttendanceTableProps> = ({ data }) => { | ||||
|   const { colorMode } = useColorMode() | ||||
|   const theme = useTheme() | ||||
|   const toast = useToast() | ||||
|   const [showTable, setShowTable] = useState(false) | ||||
| 
 | ||||
|   const getPresentColor = () => { | ||||
|     return colorMode === 'dark' ? 'green.600' : 'green.100' | ||||
| @ -35,6 +52,37 @@ export const AttendanceTable: React.FC<AttendanceTableProps> = ({ data }) => { | ||||
|     return colorMode === 'dark' ? 'red.800' : 'red.100' | ||||
|   } | ||||
| 
 | ||||
|   // Получаем эмоджи на основе посещаемости
 | ||||
|   const getAttendanceEmoji = (attendedCount: number, totalLessons: number) => { | ||||
|     const attendanceRate = totalLessons > 0 ? attendedCount / totalLessons : 0 | ||||
|      | ||||
|     if (attendanceRate >= 0.9) { | ||||
|       return {  | ||||
|         icon: FaSmile,  | ||||
|         color: 'green.500',  | ||||
|         label: 'Отличная посещаемость'  | ||||
|       } | ||||
|     } else if (attendanceRate >= 0.75) { | ||||
|       return {  | ||||
|         icon: FaMeh,  | ||||
|         color: 'blue.400',  | ||||
|         label: 'Хорошая посещаемость'  | ||||
|       } | ||||
|     } else if (attendanceRate >= 0.5) { | ||||
|       return {  | ||||
|         icon: FaFrown,  | ||||
|         color: 'orange.400',  | ||||
|         label: 'Низкая посещаемость'  | ||||
|       } | ||||
|     } else { | ||||
|       return {  | ||||
|         icon: FaSadTear,  | ||||
|         color: 'red.500',  | ||||
|         label: 'Критически низкая посещаемость'  | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Функция для копирования данных таблицы без сокращений
 | ||||
|   const copyTableData = () => { | ||||
|     if (!data.attendance?.length) return | ||||
| @ -110,28 +158,111 @@ export const AttendanceTable: React.FC<AttendanceTableProps> = ({ data }) => { | ||||
|       }) | ||||
|   } | ||||
| 
 | ||||
|   // Расчет статистики посещаемости для каждого студента
 | ||||
|   const getStudentAttendance = () => { | ||||
|     const totalLessons = data.attendance.length | ||||
|     return data.students.map(student => { | ||||
|       let attendedCount = 0 | ||||
|       data.attendance.forEach(lesson => { | ||||
|         if (lesson.students.findIndex(s => s.sub === student.sub) !== -1) { | ||||
|           attendedCount++ | ||||
|         } | ||||
|       }) | ||||
|        | ||||
|       return { | ||||
|         student, | ||||
|         name: student.name || student.value || 'Имя не определено', | ||||
|         email: student.email, | ||||
|         picture: student.picture || getGravatarURL(student.email), | ||||
|         attendedCount, | ||||
|         totalLessons, | ||||
|         attendance: totalLessons > 0 ? (attendedCount / totalLessons) * 100 : 0 | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   if (!data.attendance?.length || !data.students?.length) { | ||||
|     return <Box>Нет данных для отображения</Box> | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <Box  | ||||
|       overflowX="auto"  | ||||
|       boxShadow="md"  | ||||
|       borderRadius="lg" | ||||
|       bg={colorMode === 'dark' ? 'gray.700' : 'white'} | ||||
|     > | ||||
|       <Flex justifyContent="flex-end" p={2}> | ||||
|       <Flex justifyContent="space-between" p={3} alignItems="center"> | ||||
|         <Button  | ||||
|           leftIcon={<CopyIcon />}  | ||||
|           size="sm"  | ||||
|           colorScheme="blue"  | ||||
|           onClick={copyTableData} | ||||
|           mb={2} | ||||
|           mr={2} | ||||
|         > | ||||
|           Копировать таблицу | ||||
|         </Button> | ||||
|          | ||||
|         <Button | ||||
|           rightIcon={showTable ? <ChevronUpIcon /> : <ChevronDownIcon />} | ||||
|           size="sm" | ||||
|           variant="outline" | ||||
|           onClick={() => setShowTable(!showTable)} | ||||
|         > | ||||
|           {showTable ? 'Скрыть таблицу' : 'Показать таблицу'} | ||||
|         </Button> | ||||
|       </Flex> | ||||
|        | ||||
|       {/* Краткая статистика по каждому студенту с эмоджи */} | ||||
|       <Box p={4} borderTop="1px" borderColor={colorMode === 'dark' ? 'gray.600' : 'gray.200'}> | ||||
|         <Flex flexWrap="wrap" gap={3}> | ||||
|           {getStudentAttendance().map(({ student, name, attendedCount, totalLessons, attendance, picture }) => { | ||||
|             const emoji = getAttendanceEmoji(attendedCount, totalLessons) | ||||
|              | ||||
|             return ( | ||||
|               <Tooltip  | ||||
|                 key={student.sub} | ||||
|                 label={`${emoji.label}: ${attendedCount} из ${totalLessons} занятий (${attendance.toFixed(0)}%)`}  | ||||
|                 hasArrow | ||||
|               > | ||||
|                 <Box  | ||||
|                   p={3}  | ||||
|                   borderRadius="md" | ||||
|                   bg={colorMode === 'dark' ? 'gray.800' : 'gray.50'} | ||||
|                   boxShadow="sm" | ||||
|                   minWidth="180px" | ||||
|                 > | ||||
|                   <HStack spacing={3}> | ||||
|                     <Avatar  | ||||
|                       size="md"  | ||||
|                       src={picture}  | ||||
|                       name={name} | ||||
|                     > | ||||
|                       <AvatarBadge boxSize='2em' bg={emoji.color}> | ||||
|                         <Icon as={emoji.icon} color="white" boxSize={6} /> | ||||
|                       </AvatarBadge> | ||||
|                     </Avatar> | ||||
|                     <Box> | ||||
|                       <Text fontSize="sm" fontWeight="medium" isTruncated maxW="110px">{name}</Text> | ||||
|                       <Text fontSize="xs" mt={1} color={colorMode === 'dark' ? 'gray.400' : 'gray.600'}> | ||||
|                         {attendedCount} из {totalLessons} ({attendance.toFixed(0)}%) | ||||
|                       </Text> | ||||
|                     </Box> | ||||
|                   </HStack> | ||||
|                 </Box> | ||||
|               </Tooltip> | ||||
|             ) | ||||
|           })} | ||||
|         </Flex> | ||||
|       </Box> | ||||
|        | ||||
|       {/* Полная таблица с возможностью скрытия/показа */} | ||||
|       <Collapse in={showTable} animateOpacity> | ||||
|         <Box  | ||||
|           overflowX="auto"  | ||||
|           p={3} | ||||
|           borderTop="1px"  | ||||
|           borderColor={colorMode === 'dark' ? 'gray.600' : 'gray.200'} | ||||
|         > | ||||
|           <Table variant="simple" size="sm"> | ||||
|             <Thead> | ||||
|               <Tr> | ||||
| @ -142,7 +273,14 @@ export const AttendanceTable: React.FC<AttendanceTableProps> = ({ data }) => { | ||||
|                 <Th>Название занятия</Th> | ||||
|                 {data.students.map((student) => ( | ||||
|                   <Th key={student.sub}> | ||||
|                 {student.name || student.value || 'Имя не определено'} | ||||
|                     <HStack> | ||||
|                       <Avatar  | ||||
|                         size="xs"  | ||||
|                         src={student.picture || getGravatarURL(student.email)}  | ||||
|                         name={student.name || student.value || 'Имя не определено'}  | ||||
|                       /> | ||||
|                       <Text>{student.name || student.value || 'Имя не определено'}</Text> | ||||
|                     </HStack> | ||||
|                   </Th> | ||||
|                 ))} | ||||
|               </Tr> | ||||
| @ -159,7 +297,11 @@ export const AttendanceTable: React.FC<AttendanceTableProps> = ({ data }) => { | ||||
|                         textAlign="center" | ||||
|                         bg={wasThere ? getPresentColor() : getAbsentColor()} | ||||
|                       > | ||||
|                     {wasThere ? '+' : '-'} | ||||
|                         {wasThere ? ( | ||||
|                           <Icon as={FaSmile} color="green.500" /> | ||||
|                         ) : ( | ||||
|                           <Icon as={FaFrown} color="red.500" /> | ||||
|                         )} | ||||
|                       </Td> | ||||
|                     ) | ||||
|                   })} | ||||
| @ -175,7 +317,11 @@ export const AttendanceTable: React.FC<AttendanceTableProps> = ({ data }) => { | ||||
|                         textAlign="center" | ||||
|                         bg={wasThere ? getPresentColor() : getAbsentColor()} | ||||
|                       > | ||||
|                     {wasThere ? '+' : '-'} | ||||
|                         {wasThere ? ( | ||||
|                           <Icon as={FaSmile} color="green.500" /> | ||||
|                         ) : ( | ||||
|                           <Icon as={FaFrown} color="red.500" /> | ||||
|                         )} | ||||
|                       </Td> | ||||
|                     ) | ||||
|                   })} | ||||
| @ -184,5 +330,7 @@ export const AttendanceTable: React.FC<AttendanceTableProps> = ({ data }) => { | ||||
|             </Tbody> | ||||
|           </Table> | ||||
|         </Box> | ||||
|       </Collapse> | ||||
|     </Box> | ||||
|   ) | ||||
| }  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user