Добавлены новые локализации для статистики прошедших уроков и посещаемости. Обновлены компоненты статистики для отображения подсказок с информацией о посещаемости и прошедших занятиях. Улучшено взаимодействие с пользователем через использование подсказок в интерфейсе.
This commit is contained in:
parent
d61a93e67c
commit
510d052116
@ -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"
|
||||
}
|
@ -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": "Посещаемость рассчитана только по прошедшим занятиям"
|
||||
}
|
@ -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"
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
})
|
||||
|
||||
// Находим самый активный день недели
|
||||
|
Loading…
x
Reference in New Issue
Block a user