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

This commit is contained in:
Primakov Alexandr Alexandrovich 2025-03-23 18:24:51 +03:00
parent b37c96f640
commit 5f952ece7a
12 changed files with 900 additions and 5 deletions

View File

@ -54,6 +54,7 @@
"journal.pl.course.progress": "Course progress",
"journal.pl.course.completedLessons": "Completed lessons",
"journal.pl.course.upcomingLessons": "Upcoming lessons",
"journal.pl.course.noCourses": "No courses available",
"journal.pl.lesson.created": "Lesson created",
"journal.pl.lesson.successMessage": "Lesson {{name}} successfully created",
@ -155,5 +156,40 @@
"journal.pl.statistics.noUpcoming": "Not scheduled",
"journal.pl.statistics.in": "in",
"journal.pl.statistics.days": "days",
"journal.pl.statistics.courseProgress": "Course Progress"
"journal.pl.statistics.courseProgress": "Course Progress",
"journal.pl.days.sunday": "Sunday",
"journal.pl.days.monday": "Monday",
"journal.pl.days.tuesday": "Tuesday",
"journal.pl.days.wednesday": "Wednesday",
"journal.pl.days.thursday": "Thursday",
"journal.pl.days.friday": "Friday",
"journal.pl.days.saturday": "Saturday",
"journal.pl.overview.title": "Courses Overview",
"journal.pl.overview.totalCourses": "Total Courses",
"journal.pl.overview.active": "active",
"journal.pl.overview.completed": "completed",
"journal.pl.overview.totalLessons": "Total Lessons",
"journal.pl.overview.upcoming": "upcoming",
"journal.pl.overview.totalStudents": "Total Students",
"journal.pl.overview.totalTeachers": "Total Teachers",
"journal.pl.overview.perCourse": "per course",
"journal.pl.overview.attendance": "attendance",
"journal.pl.overview.noAttendanceData": "no attendance data",
"journal.pl.overview.noActiveData": "no active courses",
"journal.pl.overview.courseTrends": "Course Trends",
"journal.pl.overview.newCourses": "New Courses",
"journal.pl.overview.olderCourses": "Older Courses",
"journal.pl.overview.last3Months": "in last 3 months",
"journal.pl.overview.beyondLast3Months": "created earlier than 3 months",
"journal.pl.overview.mostActiveDay": "Most Active Day",
"journal.pl.overview.activityStats": "Activity Statistics",
"journal.pl.overview.courseCompletion": "Course Completion",
"journal.pl.overview.studentAttendance": "Student Attendance",
"journal.pl.overview.averageRate": "Average Rate",
"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"
}

View File

@ -51,6 +51,7 @@
"journal.pl.course.progress": "Прогресс курса",
"journal.pl.course.completedLessons": "Завершено занятий",
"journal.pl.course.upcomingLessons": "Предстоящие занятия",
"journal.pl.course.noCourses": "Нет доступных курсов",
"journal.pl.lesson.created": "Лекция создана",
"journal.pl.lesson.successMessage": "Лекция {{name}} успешно создана",
@ -152,5 +153,40 @@
"journal.pl.statistics.noUpcoming": "Не запланировано",
"journal.pl.statistics.in": "через",
"journal.pl.statistics.days": "дн.",
"journal.pl.statistics.courseProgress": "Прогресс курса"
"journal.pl.statistics.courseProgress": "Прогресс курса",
"journal.pl.days.sunday": "Воскресенье",
"journal.pl.days.monday": "Понедельник",
"journal.pl.days.tuesday": "Вторник",
"journal.pl.days.wednesday": "Среда",
"journal.pl.days.thursday": "Четверг",
"journal.pl.days.friday": "Пятница",
"journal.pl.days.saturday": "Суббота",
"journal.pl.overview.title": "Обзор всех курсов",
"journal.pl.overview.totalCourses": "Всего курсов",
"journal.pl.overview.active": "активных",
"journal.pl.overview.completed": "завершенных",
"journal.pl.overview.totalLessons": "Всего занятий",
"journal.pl.overview.upcoming": "предстоящих",
"journal.pl.overview.totalStudents": "Всего студентов",
"journal.pl.overview.totalTeachers": "Всего преподавателей",
"journal.pl.overview.perCourse": "на курс",
"journal.pl.overview.attendance": "посещаемость",
"journal.pl.overview.noAttendanceData": "нет данных о посещаемости",
"journal.pl.overview.noActiveData": "нет активных курсов",
"journal.pl.overview.courseTrends": "Тренды курсов",
"journal.pl.overview.newCourses": "Новые курсы",
"journal.pl.overview.olderCourses": "Старые курсы",
"journal.pl.overview.last3Months": "за последние 3 месяца",
"journal.pl.overview.beyondLast3Months": "созданы ранее 3 месяцев",
"journal.pl.overview.mostActiveDay": "Самый активный день недели",
"journal.pl.overview.activityStats": "Статистика активности",
"journal.pl.overview.courseCompletion": "Завершенность курсов",
"journal.pl.overview.studentAttendance": "Посещаемость студентов",
"journal.pl.overview.averageRate": "Средний показатель",
"journal.pl.overview.lessons": "занятий",
"journal.pl.overview.topStudents": "Лучшие студенты по посещаемости",
"journal.pl.overview.topAttendanceCourses": "Курсы с лучшей посещаемостью",
"journal.pl.overview.new": "новых"
}

View File

@ -0,0 +1,76 @@
import React from 'react'
import {
Box,
Heading,
SimpleGrid,
useColorModeValue,
Card
} from '@chakra-ui/react'
import { useTranslation } from 'react-i18next'
import { Course, Lesson } from '../../../__data__/model'
import {
useStats,
StatCards,
StudentAttendanceList,
CourseAttendanceList,
ActivityStats
} from './statistics'
interface CoursesOverviewProps {
courses: Course[]
isLoading: boolean
// Детализированные данные с уроками (если есть)
lessonsByCourse?: Record<string, Lesson[]>
}
export const CoursesOverview: React.FC<CoursesOverviewProps> = ({
courses = [],
isLoading = false,
lessonsByCourse = {}
}) => {
const { t } = useTranslation()
const bgColor = useColorModeValue('white', 'gray.700')
// Используем хук для расчета статистики
const stats = useStats(courses, lessonsByCourse)
// Если загрузка или нет данных, возвращаем null
if (isLoading || !courses.length) {
return null
}
return (
<Box mb={6} py={3}>
<Heading size="md" mb={4}>
{t('journal.pl.overview.title')}
</Heading>
{/* Основные показатели */}
<StatCards stats={stats} bgColor={bgColor} />
{/* Дополнительная статистика */}
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={5} mb={5}>
{/* Статистика посещаемости и топ-студенты */}
<Card bg={bgColor} p={4} borderRadius="lg" boxShadow="sm">
<StudentAttendanceList
students={stats.topStudents}
title={t('journal.pl.overview.topStudents')}
/>
{stats.topCoursesByAttendance.length > 0 && (
<>
<Box h={3} />
<CourseAttendanceList courses={stats.topCoursesByAttendance} />
</>
)}
</Card>
{/* Статистика деятельности и активности */}
<Card bg={bgColor} p={4} borderRadius="lg" boxShadow="sm">
<ActivityStats stats={stats} />
</Card>
</SimpleGrid>
</Box>
)
}

View File

@ -1,2 +1,3 @@
export * from './CreateCourseForm'
export * from './YearGroup'
export * from './CreateCourseForm'
export * from './CoursesOverview'

View File

@ -0,0 +1,93 @@
import React from 'react'
import {
Box,
Text,
Progress,
Flex,
Divider
} from '@chakra-ui/react'
import { useTranslation } from 'react-i18next'
import { CourseStats } from './useStats'
import { WeekdayActivityChart } from './WeekdayActivityChart'
interface ActivityStatsProps {
stats: CourseStats
}
export const ActivityStats: React.FC<ActivityStatsProps> = ({ stats }) => {
const { t } = useTranslation()
// Определяем цвет для прогресса в зависимости от значения
const getProgressColor = (value: number) => {
if (value > 80) return 'green'
if (value > 50) return 'blue'
if (value > 30) return 'yellow'
return 'red'
}
// Вычисляем процент завершенности курсов
const completionPercentage =
stats.totalLessons > 0
? (stats.completedLessons / stats.totalLessons) * 100
: 0
return (
<Box>
<Text fontWeight="medium" fontSize="md" mb={3}>
{t('journal.pl.overview.activityStats')}
</Text>
<Box mb={3}>
<Text fontSize="sm" fontWeight="medium" mb={1}>
{t('journal.pl.overview.courseCompletion')}:
</Text>
<Progress
value={completionPercentage}
size="md"
borderRadius="md"
colorScheme={getProgressColor(completionPercentage)}
mb={1}
hasStripe
/>
<Flex justify="space-between" fontSize="sm">
<Text>
{stats.completedLessons} / {stats.totalLessons} {t('journal.pl.overview.lessons')}
</Text>
<Text fontWeight="medium">
{Math.round(completionPercentage)}%
</Text>
</Flex>
</Box>
<Box mb={3}>
<Text fontSize="sm" fontWeight="medium" mb={1}>
{t('journal.pl.overview.studentAttendance')}:
</Text>
<Progress
value={stats.averageAttendance}
size="md"
borderRadius="md"
colorScheme={getProgressColor(stats.averageAttendance)}
mb={1}
hasStripe
/>
<Flex justify="space-between" fontSize="sm">
<Text>
{t('journal.pl.overview.averageRate')}
</Text>
<Text fontWeight="medium">
{Math.round(stats.averageAttendance)}%
</Text>
</Flex>
</Box>
<Divider my={3} />
<WeekdayActivityChart
weekdayActivity={stats.weekdayActivity}
mostActiveDayIndex={stats.mostActiveDayIndex}
/>
</Box>
)
}

View File

@ -0,0 +1,57 @@
import React from 'react'
import {
VStack,
HStack,
Box,
Text,
Badge
} from '@chakra-ui/react'
import { useTranslation } from 'react-i18next'
interface CourseAttendanceProps {
courses: Array<{id: string, name: string, attendanceRate: number}>
}
export const CourseAttendanceList: React.FC<CourseAttendanceProps> = ({ courses }) => {
const { t } = useTranslation()
// Определяем цвет для прогресса в зависимости от значения
const getProgressColor = (value: number) => {
if (value > 80) return 'green'
if (value > 50) return 'blue'
if (value > 30) return 'yellow'
return 'red'
}
if (!courses?.length) {
return (
<Text color="gray.500" fontSize="sm" textAlign="center">
{t('journal.pl.overview.noAttendanceData')}
</Text>
)
}
return (
<Box>
<Text fontWeight="medium" fontSize="sm" mb={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]}>
#{index + 1}
</Badge>
<Text fontSize="sm" fontWeight="medium" isTruncated flex="1">
{course.name}
</Text>
<Badge colorScheme={getProgressColor(course.attendanceRate)}>
{Math.round(course.attendanceRate)}%
</Badge>
</HStack>
))}
</VStack>
</Box>
)
}

View File

@ -0,0 +1,135 @@
import React from 'react'
import {
SimpleGrid,
Stat,
StatLabel,
StatNumber,
StatHelpText,
Flex,
HStack,
Text,
Icon,
Badge
} from '@chakra-ui/react'
import { useTranslation } from 'react-i18next'
import {
FaGraduationCap,
FaChalkboardTeacher,
FaCalendarAlt,
FaUsers
} from 'react-icons/fa'
import { CourseStats } from './useStats'
interface StatCardsProps {
stats: CourseStats
bgColor: string
}
export const StatCards: React.FC<StatCardsProps> = ({ stats, bgColor }) => {
const { t } = useTranslation()
return (
<SimpleGrid columns={{ base: 1, sm: 2, md: 4 }} spacing={5} mb={5}>
{/* Статистика по курсам */}
<Stat
bg={bgColor}
p={3}
borderRadius="lg"
boxShadow="sm"
borderLeft="4px solid"
borderLeftColor="blue.400"
>
<Flex align="center" mb={2}>
<Icon as={FaGraduationCap} color="blue.400" mr={2} />
<StatLabel>{t('journal.pl.overview.totalCourses')}</StatLabel>
</Flex>
<StatNumber fontSize="2xl">{stats.totalCourses}</StatNumber>
<StatHelpText mb={0}>
<HStack spacing={3} flexWrap="wrap">
<Badge colorScheme="green">
{stats.activeCourses} {t('journal.pl.overview.active')}
</Badge>
{stats.recentCoursesCount > 0 && (
<Badge colorScheme="purple">
+{stats.recentCoursesCount} {t('journal.pl.overview.new')}
</Badge>
)}
</HStack>
</StatHelpText>
</Stat>
{/* Статистика по урокам */}
<Stat
bg={bgColor}
p={3}
borderRadius="lg"
boxShadow="sm"
borderLeft="4px solid"
borderLeftColor="green.400"
>
<Flex align="center" mb={2}>
<Icon as={FaCalendarAlt} color="green.400" mr={2} />
<StatLabel>{t('journal.pl.overview.totalLessons')}</StatLabel>
</Flex>
<StatNumber fontSize="2xl">{stats.totalLessons}</StatNumber>
<StatHelpText mb={0}>
<HStack spacing={3} flexWrap="wrap">
<Badge colorScheme="blue">
{stats.completedLessons} {t('journal.pl.overview.completed')}
</Badge>
<Badge colorScheme="orange">
{stats.upcomingLessons} {t('journal.pl.overview.upcoming')}
</Badge>
</HStack>
</StatHelpText>
</Stat>
{/* Статистика по студентам */}
<Stat
bg={bgColor}
p={3}
borderRadius="lg"
boxShadow="sm"
borderLeft="4px solid"
borderLeftColor="purple.400"
>
<Flex align="center" mb={2}>
<Icon as={FaUsers} color="purple.400" mr={2} />
<StatLabel>{t('journal.pl.overview.totalStudents')}</StatLabel>
</Flex>
<StatNumber fontSize="2xl">{stats.totalStudents.size}</StatNumber>
<StatHelpText mb={0}>
<Text>
{stats.averageAttendance > 0 ?
`~${Math.round(stats.averageAttendance)}% ${t('journal.pl.overview.attendance')}` :
t('journal.pl.overview.noAttendanceData')}
</Text>
</StatHelpText>
</Stat>
{/* Статистика по преподавателям */}
<Stat
bg={bgColor}
p={3}
borderRadius="lg"
boxShadow="sm"
borderLeft="4px solid"
borderLeftColor="orange.400"
>
<Flex align="center" mb={2}>
<Icon as={FaChalkboardTeacher} color="orange.400" mr={2} />
<StatLabel>{t('journal.pl.overview.totalTeachers')}</StatLabel>
</Flex>
<StatNumber fontSize="2xl">{stats.totalTeachers.size}</StatNumber>
<StatHelpText mb={0}>
<Text>
{stats.activeCourses > 0 ?
`~${(stats.totalTeachers.size / Math.max(1, stats.activeCourses)).toFixed(1)} ${t('journal.pl.overview.perCourse')}` :
t('journal.pl.overview.noActiveData')}
</Text>
</StatHelpText>
</Stat>
</SimpleGrid>
)
}

View File

@ -0,0 +1,82 @@
import React from 'react'
import {
VStack,
HStack,
Box,
Text,
Progress,
Badge,
Avatar,
Tooltip
} from '@chakra-ui/react'
import { useTranslation } from 'react-i18next'
import { StarIcon } from '@chakra-ui/icons'
import { StudentAttendance } from './useStats'
interface StudentAttendanceListProps {
students: StudentAttendance[]
title: string
}
export const StudentAttendanceList: React.FC<StudentAttendanceListProps> = ({
students,
title
}) => {
const { t } = useTranslation()
// Определяем цвет для прогресса в зависимости от значения
const getProgressColor = (value: number) => {
if (value > 80) return 'green'
if (value > 50) return 'blue'
if (value > 30) return 'yellow'
return 'red'
}
if (!students?.length) {
return (
<Text color="gray.500" fontSize="sm" textAlign="center">
{t('journal.pl.overview.noAttendanceData')}
</Text>
)
}
return (
<Box>
<Text fontWeight="medium" fontSize="sm" mb={2} display="flex" alignItems="center">
<StarIcon color="yellow.400" mr={2} />
{title}
</Text>
<VStack align="stretch" spacing={3}>
{students.map((student, index) => (
<HStack key={student.id} spacing={3}>
<Avatar
size="sm"
name={student.name}
src={student.avatarUrl}
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>
<Progress
value={student.percent}
size="xs"
colorScheme={getProgressColor(student.percent)}
borderRadius="full"
mt={1}
/>
</Box>
<Badge colorScheme={getProgressColor(student.percent)}>
{Math.round(student.percent)}%
</Badge>
</HStack>
))}
</VStack>
</Box>
)
}

View File

@ -0,0 +1,92 @@
import React from 'react'
import {
Box,
HStack,
Text,
Badge,
Tooltip,
VStack
} from '@chakra-ui/react'
import { useTranslation } from 'react-i18next'
interface WeekdayActivityChartProps {
weekdayActivity: number[]
mostActiveDayIndex: number
}
export const WeekdayActivityChart: React.FC<WeekdayActivityChartProps> = ({
weekdayActivity,
mostActiveDayIndex
}) => {
const { t } = useTranslation()
// Переводим день недели в читаемый формат
const getDayOfWeekName = (dayIndex: number) => {
const days = [
'journal.pl.days.sunday',
'journal.pl.days.monday',
'journal.pl.days.tuesday',
'journal.pl.days.wednesday',
'journal.pl.days.thursday',
'journal.pl.days.friday',
'journal.pl.days.saturday'
]
return t(days[dayIndex])
}
// Получаем короткое название дня недели (первая буква)
const getShortDayName = (dayIndex: number) => {
return t(`journal.pl.days.${['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'][dayIndex]}`).charAt(0)
}
// Формируем подсказку для дня недели
const getDayTooltip = (dayIndex: number, count: number) => {
return `${t(`journal.pl.days.${['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'][dayIndex]}`)}:
${count} ${t('journal.pl.overview.lessons').toLowerCase()}`
}
// Если нет данных по активности, показываем сообщение
if (!weekdayActivity.some(count => count > 0)) {
return (
<Box textAlign="center" color="gray.500" fontSize="sm">
{t('journal.pl.overview.noAttendanceData')}
</Box>
)
}
return (
<VStack align="start" width="100%">
<Text fontSize="sm" fontWeight="medium">
{t('journal.pl.overview.mostActiveDay')}:
</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">
{weekdayActivity.map((count, index) => (
<Tooltip
key={index}
label={getDayTooltip(index, count)}
>
<Box>
<Box
h={`${Math.max((count / Math.max(...weekdayActivity, 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>
</Tooltip>
))}
</HStack>
</Box>
</VStack>
)
}

View File

@ -0,0 +1,6 @@
export * from './useStats'
export * from './StatCards'
export * from './StudentAttendanceList'
export * from './CourseAttendanceList'
export * from './ActivityStats'
export * from './WeekdayActivityChart'

View File

@ -0,0 +1,235 @@
import { useMemo } from 'react'
import dayjs from 'dayjs'
import { Course, Lesson } from '../../../../__data__/model'
export interface StudentAttendance {
id: string
name: string
attended: number
total: number
percent: number
avatarUrl?: string
email?: string
}
export interface CourseStats {
totalCourses: number
activeCourses: number
totalLessons: number
completedLessons: number
upcomingLessons: number
averageAttendance: number
totalStudents: Set<string>
totalTeachers: Set<string>
recentCoursesCount: number
oldCoursesCount: number
weekdayActivity: number[]
mostActiveDayIndex: number
topStudents: StudentAttendance[]
topCoursesByAttendance: Array<{id: string, name: string, attendanceRate: number}>
}
export const useStats = (
courses: Course[],
lessonsByCourse: Record<string, Lesson[]> = {}
): CourseStats => {
return useMemo(() => {
if (!courses?.length) {
return {
totalCourses: 0,
activeCourses: 0,
totalLessons: 0,
completedLessons: 0,
upcomingLessons: 0,
averageAttendance: 0,
totalStudents: new Set<string>(),
totalTeachers: new Set<string>(),
recentCoursesCount: 0,
oldCoursesCount: 0,
weekdayActivity: Array(7).fill(0),
mostActiveDayIndex: 0,
topStudents: [] as StudentAttendance[],
topCoursesByAttendance: [] as {id: string, name: string, attendanceRate: number}[]
}
}
const now = dayjs()
const threeMonthsAgo = now.subtract(3, 'month')
const weekdayActivity = Array(7).fill(0)
// Множества для уникальных студентов и учителей
const uniqueStudents = new Set<string>()
const uniqueTeachers = new Set<string>()
// Количество курсов, созданных за последние 3 месяца
const recentCourses = courses.filter(course =>
dayjs(course.created).isAfter(threeMonthsAgo)
)
// Количество активных курсов
const activeCourses = []
let totalLessonsCount = 0
let completedLessonsCount = 0
let upcomingLessonsCount = 0
let totalAttendances = 0
let totalPossibleAttendances = 0
// Для отслеживания посещаемости студентов по всем курсам
const globalStudentsMap = new Map<string, StudentAttendance>()
// Статистика посещаемости по курсам
const courseAttendanceStats: {id: string, name: string, attendanceRate: number}[] = []
// Для каждого курса считаем статистику на основе данных об уроках
courses.forEach(course => {
// Добавляем учителей в множество
course.teachers.forEach(teacher => {
uniqueTeachers.add(teacher.sub)
})
// Получаем детализированные данные об уроках курса (если доступны)
const courseLessons = lessonsByCourse[course._id] || []
// Если у нас есть детализированные данные по урокам
if (courseLessons.length > 0) {
// Добавляем количество уроков к общему счетчику
totalLessonsCount += courseLessons.length
// Считаем завершенные и предстоящие уроки
const completed = courseLessons.filter(lesson => dayjs(lesson.date).isBefore(now))
const upcoming = courseLessons.filter(lesson => dayjs(lesson.date).isAfter(now))
completedLessonsCount += completed.length
upcomingLessonsCount += upcoming.length
// Если у курса есть будущие занятия, считаем его активным
if (upcoming.length > 0) {
activeCourses.push(course)
}
// Для статистики посещаемости по курсу
let courseAttendances = 0
let coursePossibleAttendances = 0
// Считаем посещаемость по прошедшим занятиям
completed.forEach(lesson => {
// Добавляем статистику по дням недели
// В dayjs 0 = воскресенье, 1 = понедельник, ... 6 = суббота
// Нужно проверить формат даты урока, что это валидная дата
if (lesson.date && dayjs(lesson.date).isValid()) {
const lessonDay = dayjs(lesson.date).day()
weekdayActivity[lessonDay]++
}
// Добавляем студентов в глобальное множество
const lessonStudentsCount = lesson.students?.length || 0
// Добавляем в статистику посещаемости
courseAttendances += lessonStudentsCount
// Обновляем счетчики общей посещаемости
totalAttendances += lessonStudentsCount
// Собираем статистику по каждому студенту
lesson.students?.forEach(student => {
uniqueStudents.add(student.sub)
// Добавляем или обновляем данные студента в глобальной карте
const studentId = student.sub
const currentGlobal = globalStudentsMap.get(studentId) || {
id: studentId,
name: (student.family_name && student.given_name
? `${student.family_name} ${student.given_name}`
: student.name || student.email || student.preferred_username || student.family_name || student.given_name),
attended: 0,
total: 0,
percent: 0,
avatarUrl: student.picture,
email: student.email
}
currentGlobal.attended += 1
globalStudentsMap.set(studentId, currentGlobal)
})
})
// Потенциальные посещения для этого курса
// (кол-во прошедших занятий * кол-во уникальных студентов)
const courseUniqueStudents = new Set<string>()
courseLessons.forEach(lesson => {
lesson.students?.forEach(student => {
courseUniqueStudents.add(student.sub)
})
})
coursePossibleAttendances = completed.length * (courseUniqueStudents.size || 1)
totalPossibleAttendances += coursePossibleAttendances
// Добавляем статистику курса, если есть прошедшие занятия
if (completed.length > 0 && coursePossibleAttendances > 0) {
courseAttendanceStats.push({
id: course._id,
name: course.name,
attendanceRate: (courseAttendances / coursePossibleAttendances) * 100
})
}
} else {
// Если у нас нет детализированных данных, считаем на основе общих данных курса
totalLessonsCount += course.lessons.length
// Предполагаем, что курс активен
activeCourses.push(course)
}
})
// Отладочная информация по активности по дням недели
console.log('Weekday activity:', weekdayActivity)
// Обрабатываем глобальную статистику посещаемости студентов
// Устанавливаем общее число занятий для каждого студента
globalStudentsMap.forEach(student => {
// Можем установить только примерную метрику, т.к. студенты могут быть на разных курсах
student.total = completedLessonsCount
student.percent = student.attended / student.total * 100
})
// Находим самый активный день недели
const maxValue = Math.max(...weekdayActivity)
// Если максимальное значение = 0, то устанавливаем понедельник как самый активный день по умолчанию
const mostActiveDayIndex = maxValue > 0 ? weekdayActivity.indexOf(maxValue) : 1
// Вычисляем среднюю посещаемость
const averageAttendance = totalPossibleAttendances > 0
? (totalAttendances / totalPossibleAttendances) * 100
: 0
// Топ студенты по посещаемости (по всем курсам)
const topStudents = Array.from(globalStudentsMap.values())
.sort((a, b) => (b.percent - a.percent) || (b.attended - a.attended))
.slice(0, 5)
// Сортируем курсы по посещаемости
const topCoursesByAttendance = courseAttendanceStats
.sort((a, b) => b.attendanceRate - a.attendanceRate)
.slice(0, 3)
return {
totalCourses: courses.length,
activeCourses: activeCourses.length,
totalLessons: totalLessonsCount,
completedLessons: completedLessonsCount,
upcomingLessons: upcomingLessonsCount,
averageAttendance,
totalStudents: uniqueStudents,
totalTeachers: uniqueTeachers,
recentCoursesCount: recentCourses.length,
oldCoursesCount: courses.length - recentCourses.length,
weekdayActivity,
mostActiveDayIndex,
topStudents,
topCoursesByAttendance
}
}, [courses, lessonsByCourse])
}

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React, { useState, useMemo, useEffect } from 'react'
import {
Box,
Button,
@ -15,7 +15,8 @@ import { api } from '../../__data__/api/api'
import { isTeacher } from '../../utils/user'
import { PageLoader } from '../../components/page-loader/page-loader'
import { useGroupedCourses } from './hooks'
import { CreateCourseForm, YearGroup } from './components'
import { CreateCourseForm, YearGroup, CoursesOverview } from './components'
import { Lesson } from '../../__data__/model'
/**
* Основной компонент списка курсов
@ -27,11 +28,48 @@ export const CoursesList = () => {
const { t } = useTranslation()
const { colorMode } = useColorMode()
// Создаем API запросы для получения уроков
const [getLessons] = api.useLazyLessonListQuery()
const buttonSize = useBreakpointValue({ base: 'md', md: 'lg' })
const containerPadding = useBreakpointValue({ base: '2', md: '4' })
// Используем хук для группировки курсов по годам
const groupedCourses = useGroupedCourses(data?.body)
// Создаем объект с детализированными данными для всех курсов
const [lessonsByCourse, setLessonsByCourse] = useState<Record<string, Lesson[]>>({})
// Используем useMemo для проверки наличия данных
const courses = useMemo(() => data?.body || [], [data])
// Загружаем данные для каждого курса параллельно
useEffect(() => {
if (courses.length > 0 && !showForm) {
// Создаем запросы для получения данных о занятиях каждого курса
const fetchLessonsForCourses = async () => {
const lessonsData: Record<string, Lesson[]> = {}
// Получаем данные курсов параллельно (по 3 курса за раз, чтобы не перегружать сервер)
for (let i = 0; i < courses.length; i += 3) {
const batch = courses.slice(i, i + 3)
const batchPromises = batch.map(async course => {
// Используем существующий API метод с Lazy Query
const response = await getLessons(course.id)
if (response.data?.body) {
lessonsData[course._id] = response.data.body
}
})
await Promise.all(batchPromises)
}
setLessonsByCourse(lessonsData)
}
fetchLessonsForCourses()
}
}, [courses, showForm, getLessons])
if (isLoading) {
return <PageLoader />
@ -61,6 +99,14 @@ export const CoursesList = () => {
</Box>
)}
{!showForm && (
<CoursesOverview
courses={courses}
isLoading={isLoading}
lessonsByCourse={lessonsByCourse}
/>
)}
{Object.keys(groupedCourses).length > 0 ? (
Object.entries(groupedCourses)
.sort(([yearA], [yearB]) => Number(yearB) - Number(yearA)) // Сортируем годы по убыванию