Добавлены новые локализации для статистики прошедших уроков и посещаемости. Обновлены компоненты статистики для отображения подсказок с информацией о посещаемости и прошедших занятиях. Улучшено взаимодействие с пользователем через использование подсказок в интерфейсе.

This commit is contained in:
Primakov Alexandr Alexandrovich 2025-03-23 18:44:53 +03:00
parent d61a93e67c
commit 510d052116
7 changed files with 168 additions and 53 deletions

View File

@ -191,5 +191,8 @@
"journal.pl.overview.lessons": "lessons",
"journal.pl.overview.topStudents": "Top Students by Attendance",
"journal.pl.overview.topAttendanceCourses": "Courses with Best Attendance",
"journal.pl.overview.new": "new"
"journal.pl.overview.new": "new",
"journal.pl.overview.pastLessonsStats": "Statistics of past lessons",
"journal.pl.overview.dayOfWeekHelp": "Only statistics for completed lessons are shown",
"journal.pl.overview.attendanceHelp": "Attendance is calculated based on past lessons only"
}

View File

@ -188,5 +188,8 @@
"journal.pl.overview.lessons": "занятий",
"journal.pl.overview.topStudents": "Лучшие студенты по посещаемости",
"journal.pl.overview.topAttendanceCourses": "Курсы с лучшей посещаемостью",
"journal.pl.overview.new": "новых"
"journal.pl.overview.new": "новых",
"journal.pl.overview.pastLessonsStats": "Статистика проведённых занятий",
"journal.pl.overview.dayOfWeekHelp": "Показана статистика только состоявшихся занятий",
"journal.pl.overview.attendanceHelp": "Посещаемость рассчитана только по прошедшим занятиям"
}

View File

@ -4,9 +4,11 @@ import {
Text,
Progress,
Flex,
Divider
Divider,
Tooltip
} from '@chakra-ui/react'
import { useTranslation } from 'react-i18next'
import { InfoOutlineIcon } from '@chakra-ui/icons'
import { CourseStats } from './useStats'
import { WeekdayActivityChart } from './WeekdayActivityChart'
@ -34,14 +36,24 @@ export const ActivityStats: React.FC<ActivityStatsProps> = ({ stats }) => {
return (
<Box>
<Text fontWeight="medium" fontSize="md" mb={3}>
{t('journal.pl.overview.activityStats')}
</Text>
<Flex align="center" mb={3}>
<Text fontWeight="medium" fontSize="md" mr={2}>
{t('journal.pl.overview.activityStats')}
</Text>
<Tooltip label={t('journal.pl.overview.pastLessonsStats')}>
<InfoOutlineIcon color="gray.400" boxSize={3} />
</Tooltip>
</Flex>
<Box mb={3}>
<Text fontSize="sm" fontWeight="medium" mb={1}>
{t('journal.pl.overview.courseCompletion')}:
</Text>
<Flex align="center" mb={1}>
<Text fontSize="sm" fontWeight="medium" mr={1}>
{t('journal.pl.overview.courseCompletion')}:
</Text>
<Tooltip label={`${stats.completedLessons} / ${stats.totalLessons}`}>
<InfoOutlineIcon color="gray.400" boxSize={3} />
</Tooltip>
</Flex>
<Progress
value={completionPercentage}
size="md"
@ -61,9 +73,14 @@ export const ActivityStats: React.FC<ActivityStatsProps> = ({ stats }) => {
</Box>
<Box mb={3}>
<Text fontSize="sm" fontWeight="medium" mb={1}>
{t('journal.pl.overview.studentAttendance')}:
</Text>
<Flex align="center" mb={1}>
<Text fontSize="sm" fontWeight="medium" mr={1}>
{t('journal.pl.overview.studentAttendance')}:
</Text>
<Tooltip label={t('journal.pl.overview.attendanceHelp')}>
<InfoOutlineIcon color="gray.400" boxSize={3} />
</Tooltip>
</Flex>
<Progress
value={stats.averageAttendance}
size="md"

View File

@ -4,9 +4,11 @@ import {
HStack,
Box,
Text,
Badge
Badge,
Tooltip
} from '@chakra-ui/react'
import { useTranslation } from 'react-i18next'
import { CheckCircleIcon } from '@chakra-ui/icons'
interface CourseAttendanceProps {
courses: Array<{id: string, name: string, attendanceRate: number}>
@ -33,25 +35,41 @@ export const CourseAttendanceList: React.FC<CourseAttendanceProps> = ({ courses
return (
<Box>
<Text fontWeight="medium" fontSize="sm" mb={2}>
{t('journal.pl.overview.topAttendanceCourses')}:
<Text fontWeight="medium" fontSize="sm" mb={2} display="flex" alignItems="center">
<CheckCircleIcon color="green.400" mr={2} />
{t('journal.pl.overview.topAttendanceCourses')}
</Text>
<VStack align="stretch" spacing={2}>
{courses.map((course, index) => (
<HStack key={course.id} spacing={2}>
<Badge colorScheme={['green', 'blue', 'yellow'][index]}>
<Badge
colorScheme={['green', 'blue', 'yellow'][index]}
borderRadius="full"
minW="22px"
textAlign="center"
>
#{index + 1}
</Badge>
<Text fontSize="sm" fontWeight="medium" isTruncated flex="1">
{course.name}
</Text>
<Badge colorScheme={getProgressColor(course.attendanceRate)}>
<Tooltip label={course.name}>
<Text fontSize="sm" fontWeight="medium" isTruncated flex="1">
{course.name}
</Text>
</Tooltip>
<Badge
colorScheme={getProgressColor(course.attendanceRate)}
variant="solid"
px={2}
>
{Math.round(course.attendanceRate)}%
</Badge>
</HStack>
))}
</VStack>
<Text fontSize="xs" color="gray.500" mt={2} fontStyle="italic">
{t('journal.pl.overview.attendanceHelp')}
</Text>
</Box>
)
}

View File

@ -7,7 +7,8 @@ import {
Progress,
Badge,
Avatar,
Tooltip
Tooltip,
Flex
} from '@chakra-ui/react'
import { useTranslation } from 'react-i18next'
import { StarIcon } from '@chakra-ui/icons'
@ -48,6 +49,10 @@ export const StudentAttendanceList: React.FC<StudentAttendanceListProps> = ({
{title}
</Text>
<Text fontSize="xs" color="gray.500" mb={2}>
{t('journal.pl.overview.pastLessonsStats')}
</Text>
<VStack align="stretch" spacing={3}>
{students.map((student, index) => (
<HStack key={student.id} spacing={3}>
@ -58,11 +63,18 @@ export const StudentAttendanceList: React.FC<StudentAttendanceListProps> = ({
bg={index < 3 ? ['yellow.400', 'gray.400', 'orange.300'][index] : 'blue.300'}
/>
<Box flex="1">
<Tooltip label={student.name}>
<Text fontSize="sm" fontWeight="medium" isTruncated maxW="150px">
{student.name}
</Text>
</Tooltip>
<Flex justify="space-between">
<Tooltip label={student.name}>
<Text fontSize="sm" fontWeight="medium" isTruncated maxW="150px">
{student.name}
</Text>
</Tooltip>
<Tooltip label={`${student.attended} из ${student.total} занятий`}>
<Text fontSize="xs" color="gray.500">
{student.attended}/{student.total}
</Text>
</Tooltip>
</Flex>
<Progress
value={student.percent}
size="xs"
@ -77,6 +89,10 @@ export const StudentAttendanceList: React.FC<StudentAttendanceListProps> = ({
</HStack>
))}
</VStack>
<Text fontSize="xs" color="gray.500" mt={2} fontStyle="italic">
{t('journal.pl.overview.attendanceHelp')}
</Text>
</Box>
)
}

View File

@ -5,7 +5,8 @@ import {
Text,
Badge,
Tooltip,
VStack
VStack,
Flex
} from '@chakra-ui/react'
import { useTranslation } from 'react-i18next'
@ -54,39 +55,92 @@ ${count} ${t('journal.pl.overview.lessons').toLowerCase()}`
)
}
// Находим максимальное и суммарное значение для расчета процентов
const maxValue = Math.max(...weekdayActivity)
const totalLessons = weekdayActivity.reduce((sum, count) => sum + count, 0)
return (
<VStack align="start" width="100%">
<Text fontSize="sm" fontWeight="medium">
{t('journal.pl.overview.mostActiveDay')}:
<Flex justify="space-between" width="100%" align="center">
<Text fontSize="sm" fontWeight="medium">
{t('journal.pl.overview.mostActiveDay')}:
</Text>
<Badge colorScheme="blue" fontSize="md" px={2} py={1}>
{getDayOfWeekName(mostActiveDayIndex)}
</Badge>
</Flex>
<Text fontSize="xs" color="gray.500" mb={2}>
{t('journal.pl.overview.pastLessonsStats')}
</Text>
<Badge colorScheme="blue" fontSize="md" px={2} py={1}>
{getDayOfWeekName(mostActiveDayIndex)}
</Badge>
{/* Визуализация активности по дням недели */}
<Box w="100%" mt={2}>
<HStack spacing={1} w="100%" justify="space-between">
<HStack spacing={1} w="100%" justify="space-between" alignItems="flex-end">
{weekdayActivity.map((count, index) => (
<Tooltip
key={index}
label={getDayTooltip(index, count)}
>
<Box>
<Box width="24px" display="flex" flexDirection="column" alignItems="center">
{/* Область для числа */}
<Box height="15px" mb={1}>
{count > 0 && (
<Text
fontSize="10px"
fontWeight="bold"
color={index === mostActiveDayIndex ? 'blue.500' : 'gray.500'}
lineHeight="15px"
textAlign="center"
>
{count}
</Text>
)}
</Box>
{/* Столбец графика */}
<Box
h={`${Math.max((count / Math.max(...weekdayActivity, 1)) * 50, 3)}px`}
h={`${Math.max((count / Math.max(maxValue, 1)) * 50, 3)}px`}
w="12px"
bg={index === mostActiveDayIndex ? 'blue.400' : 'gray.300'}
minH="3px"
borderRadius="sm"
/>
<Text fontSize="xs" textAlign="center" mt={1}>
{getShortDayName(index)}
</Text>
{/* Буква дня недели */}
<Box height="15px" mt={1}>
<Text
fontSize="xs"
fontWeight={index === mostActiveDayIndex ? "bold" : "normal"}
lineHeight="15px"
textAlign="center"
>
{getShortDayName(index)}
</Text>
</Box>
{/* Процент */}
<Box height="15px">
{count > 0 && totalLessons > 0 && (
<Text
fontSize="9px"
color="gray.500"
lineHeight="15px"
textAlign="center"
>
{Math.round((count / totalLessons) * 100)}%
</Text>
)}
</Box>
</Box>
</Tooltip>
))}
</HStack>
</Box>
<Text fontSize="xs" color="gray.500" mt={2} fontStyle="italic">
{t('journal.pl.overview.dayOfWeekHelp')}
</Text>
</VStack>
)
}
}

View File

@ -89,6 +89,9 @@ export const useStats = (
uniqueTeachers.add(teacher.sub)
})
// Добавляем студентов в множество
const courseUniqueStudents = new Set<string>()
// Получаем детализированные данные об уроках курса (если доступны)
const courseLessons = lessonsByCourse[course._id] || []
@ -109,11 +112,19 @@ export const useStats = (
activeCourses.push(course)
}
// Собираем всех уникальных студентов курса для более точной статистики
courseLessons.forEach(lesson => {
lesson.students?.forEach(student => {
courseUniqueStudents.add(student.sub)
uniqueStudents.add(student.sub)
})
})
// Для статистики посещаемости по курсу
let courseAttendances = 0
let coursePossibleAttendances = 0
// Считаем посещаемость по прошедшим занятиям
// Считаем посещаемость ТОЛЬКО по прошедшим занятиям
completed.forEach(lesson => {
// Добавляем статистику по дням недели
// В dayjs 0 = воскресенье, 1 = понедельник, ... 6 = суббота
@ -134,8 +145,6 @@ export const useStats = (
// Собираем статистику по каждому студенту
lesson.students?.forEach(student => {
uniqueStudents.add(student.sub)
// Добавляем или обновляем данные студента в глобальной карте
const studentId = student.sub
const currentGlobal = globalStudentsMap.get(studentId) || {
@ -155,15 +164,9 @@ export const useStats = (
})
})
// Потенциальные посещения для этого курса
// (кол-во прошедших занятий * кол-во уникальных студентов)
const courseUniqueStudents = new Set<string>()
courseLessons.forEach(lesson => {
lesson.students?.forEach(student => {
courseUniqueStudents.add(student.sub)
})
})
// Потенциальные посещения для этого курса рассчитываем только по прошедшим занятиям
// и только для студентов, которые есть хотя бы на одном занятии
// Кол-во прошедших занятий * кол-во уникальных студентов на курсе
coursePossibleAttendances = completed.length * (courseUniqueStudents.size || 1)
totalPossibleAttendances += coursePossibleAttendances
@ -190,9 +193,10 @@ export const useStats = (
// Обрабатываем глобальную статистику посещаемости студентов
// Устанавливаем общее число занятий для каждого студента
globalStudentsMap.forEach(student => {
// Можем установить только примерную метрику, т.к. студенты могут быть на разных курсах
// Устанавливаем максимально возможное кол-во занятий как общее число прошедших занятий
// (это завышенная оценка, т.к. студент может быть не на всех курсах)
student.total = completedLessonsCount
student.percent = student.attended / student.total * 100
student.percent = completedLessonsCount > 0 ? (student.attended / student.total) * 100 : 0
})
// Находим самый активный день недели