Добавлен компонент CourseStatistics для отображения статистики курса, включая общее количество уроков, посещаемость, количество студентов и информацию о следующем занятии. Обновлены локализации для поддержки новых статистических данных.
This commit is contained in:
parent
bc33de2721
commit
b37c96f640
@ -143,5 +143,17 @@
|
||||
"journal.pl.lesson.form.date": "Date",
|
||||
"journal.pl.lesson.form.dateTime": "Specify date and time of the lesson",
|
||||
"journal.pl.lesson.form.datePlaceholder": "Specify lesson date",
|
||||
"journal.pl.lesson.form.namePlaceholder": "Lesson name"
|
||||
"journal.pl.lesson.form.namePlaceholder": "Lesson name",
|
||||
|
||||
"journal.pl.statistics.title": "Course Statistics",
|
||||
"journal.pl.statistics.totalLessons": "Total Lessons",
|
||||
"journal.pl.statistics.completed": "completed",
|
||||
"journal.pl.statistics.attendanceRate": "Attendance Rate",
|
||||
"journal.pl.statistics.totalStudents": "Total Students",
|
||||
"journal.pl.statistics.perLesson": "per lesson",
|
||||
"journal.pl.statistics.nextLesson": "Next Lesson",
|
||||
"journal.pl.statistics.noUpcoming": "Not scheduled",
|
||||
"journal.pl.statistics.in": "in",
|
||||
"journal.pl.statistics.days": "days",
|
||||
"journal.pl.statistics.courseProgress": "Course Progress"
|
||||
}
|
@ -140,5 +140,17 @@
|
||||
"journal.pl.lesson.form.date": "Дата",
|
||||
"journal.pl.lesson.form.dateTime": "Укажите дату и время лекции",
|
||||
"journal.pl.lesson.form.datePlaceholder": "Укажите дату лекции",
|
||||
"journal.pl.lesson.form.namePlaceholder": "Название лекции"
|
||||
"journal.pl.lesson.form.namePlaceholder": "Название лекции",
|
||||
|
||||
"journal.pl.statistics.title": "Статистика курса",
|
||||
"journal.pl.statistics.totalLessons": "Всего занятий",
|
||||
"journal.pl.statistics.completed": "завершено",
|
||||
"journal.pl.statistics.attendanceRate": "Посещаемость",
|
||||
"journal.pl.statistics.totalStudents": "Всего студентов",
|
||||
"journal.pl.statistics.perLesson": "на занятие",
|
||||
"journal.pl.statistics.nextLesson": "Следующее занятие",
|
||||
"journal.pl.statistics.noUpcoming": "Не запланировано",
|
||||
"journal.pl.statistics.in": "через",
|
||||
"journal.pl.statistics.days": "дн.",
|
||||
"journal.pl.statistics.courseProgress": "Прогресс курса"
|
||||
}
|
308
src/pages/lesson-list/components/statistics.tsx
Normal file
308
src/pages/lesson-list/components/statistics.tsx
Normal file
@ -0,0 +1,308 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
Box,
|
||||
Heading,
|
||||
Text,
|
||||
SimpleGrid,
|
||||
Stat,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
StatHelpText,
|
||||
StatArrow,
|
||||
StatGroup,
|
||||
Flex,
|
||||
Icon,
|
||||
Progress,
|
||||
Divider,
|
||||
Badge,
|
||||
VStack,
|
||||
HStack,
|
||||
useColorModeValue
|
||||
} from '@chakra-ui/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
FaChalkboardTeacher,
|
||||
FaUserGraduate,
|
||||
FaClock,
|
||||
FaCalendarCheck,
|
||||
FaCalendarAlt,
|
||||
FaPercentage
|
||||
} from 'react-icons/fa'
|
||||
import { CalendarIcon, StarIcon, TimeIcon } from '@chakra-ui/icons'
|
||||
|
||||
import { Lesson } from '../../../__data__/model'
|
||||
|
||||
interface CourseStatisticsProps {
|
||||
lessons: Lesson[]
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export const CourseStatistics: React.FC<CourseStatisticsProps> = ({ lessons = [], isLoading }) => {
|
||||
const { t } = useTranslation()
|
||||
const statBgColor = useColorModeValue('white', 'gray.700')
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600')
|
||||
|
||||
// Вычисляем статистику курса
|
||||
const stats = useMemo(() => {
|
||||
if (!lessons.length) {
|
||||
return {
|
||||
totalLessons: 0,
|
||||
completedLessons: 0,
|
||||
upcomingLessons: 0,
|
||||
attendanceRate: 0,
|
||||
averageStudentsPerLesson: 0,
|
||||
nextLessonDate: null,
|
||||
mostAttendedLesson: null,
|
||||
attendanceTrend: 0,
|
||||
totalStudents: 0,
|
||||
daysUntilNextLesson: 0,
|
||||
percentageCompleted: 0
|
||||
}
|
||||
}
|
||||
|
||||
const now = dayjs()
|
||||
const completed = lessons.filter(lesson => dayjs(lesson.date).isBefore(now))
|
||||
const upcoming = lessons.filter(lesson => dayjs(lesson.date).isAfter(now))
|
||||
|
||||
// Сортируем предстоящие занятия по дате (ближайшие вперед)
|
||||
const sortedUpcoming = [...upcoming].sort((a, b) =>
|
||||
dayjs(a.date).valueOf() - dayjs(b.date).valueOf()
|
||||
)
|
||||
|
||||
// Находим ближайшее занятие
|
||||
const nextLesson = sortedUpcoming.length > 0 ? sortedUpcoming[0] : null
|
||||
|
||||
// Вычисляем среднее количество студентов на занятии
|
||||
const totalStudentsCount = completed.reduce(
|
||||
(sum, lesson) => sum + (lesson.students?.length || 0),
|
||||
0
|
||||
)
|
||||
|
||||
const averageStudents = completed.length
|
||||
? totalStudentsCount / completed.length
|
||||
: 0
|
||||
|
||||
// Находим занятие с наибольшей посещаемостью
|
||||
let mostAttended = null
|
||||
let maxAttendance = 0
|
||||
|
||||
completed.forEach(lesson => {
|
||||
const attendance = lesson.students?.length || 0
|
||||
if (attendance > maxAttendance) {
|
||||
maxAttendance = attendance
|
||||
mostAttended = lesson
|
||||
}
|
||||
})
|
||||
|
||||
// Вычисляем тренд посещаемости (положительный или отрицательный)
|
||||
let attendanceTrend = 0
|
||||
|
||||
if (completed.length >= 2) {
|
||||
// Берем последние 5 занятий или меньше, если их меньше 5
|
||||
const recentLessons = [...completed]
|
||||
.sort((a, b) => dayjs(b.date).valueOf() - dayjs(a.date).valueOf())
|
||||
.slice(0, 5)
|
||||
|
||||
if (recentLessons.length >= 2) {
|
||||
const lastLesson = recentLessons[0]
|
||||
const previousLessons = recentLessons.slice(1)
|
||||
|
||||
const lastAttendance = lastLesson.students?.length || 0
|
||||
const avgPreviousAttendance = previousLessons.reduce(
|
||||
(sum, lesson) => sum + (lesson.students?.length || 0),
|
||||
0
|
||||
) / previousLessons.length
|
||||
|
||||
// Вычисляем процентное изменение
|
||||
attendanceTrend = avgPreviousAttendance
|
||||
? ((lastAttendance - avgPreviousAttendance) / avgPreviousAttendance) * 100
|
||||
: 0
|
||||
}
|
||||
}
|
||||
|
||||
// Вычисляем количество дней до следующего занятия
|
||||
const daysUntilNext = nextLesson
|
||||
? dayjs(nextLesson.date).diff(now, 'day')
|
||||
: 0
|
||||
|
||||
// Собираем все уникальные ID студентов
|
||||
const uniqueStudents = new Set()
|
||||
lessons.forEach(lesson => {
|
||||
lesson.students?.forEach(student => {
|
||||
uniqueStudents.add(student.sub)
|
||||
})
|
||||
})
|
||||
|
||||
// Вычисляем процент завершенного курса
|
||||
const percentComplete = lessons.length
|
||||
? (completed.length / lessons.length) * 100
|
||||
: 0
|
||||
|
||||
return {
|
||||
totalLessons: lessons.length,
|
||||
completedLessons: completed.length,
|
||||
upcomingLessons: upcoming.length,
|
||||
attendanceRate: completed.length ? (totalStudentsCount / (completed.length * uniqueStudents.size || 1)) * 100 : 0,
|
||||
averageStudentsPerLesson: Math.round(averageStudents * 10) / 10,
|
||||
nextLessonDate: nextLesson?.date || null,
|
||||
mostAttendedLesson: mostAttended,
|
||||
attendanceTrend,
|
||||
totalStudents: uniqueStudents.size,
|
||||
daysUntilNextLesson: daysUntilNext,
|
||||
percentageCompleted: percentComplete
|
||||
}
|
||||
}, [lessons])
|
||||
|
||||
// Определяем цвет для показателей статистики
|
||||
const getProgressColor = (value) => {
|
||||
if (value > 80) return 'green'
|
||||
if (value > 50) return 'blue'
|
||||
if (value > 30) return 'yellow'
|
||||
return 'red'
|
||||
}
|
||||
|
||||
if (isLoading || !lessons.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Box mb={6} py={3}>
|
||||
<Heading size="md" mb={4}>
|
||||
{t('journal.pl.statistics.title')}
|
||||
</Heading>
|
||||
|
||||
<SimpleGrid columns={{ base: 1, sm: 2, md: 4 }} spacing={5} mb={5}>
|
||||
{/* Статистика по занятиям */}
|
||||
<Stat
|
||||
bg={statBgColor}
|
||||
p={3}
|
||||
borderRadius="lg"
|
||||
boxShadow="sm"
|
||||
borderLeft="4px solid"
|
||||
borderLeftColor="blue.400"
|
||||
>
|
||||
<Flex align="center" mb={2}>
|
||||
<Icon as={FaCalendarAlt} color="blue.400" mr={2} />
|
||||
<StatLabel>{t('journal.pl.statistics.totalLessons')}</StatLabel>
|
||||
</Flex>
|
||||
<StatNumber fontSize="2xl">{stats.totalLessons}</StatNumber>
|
||||
<StatHelpText mb={0}>
|
||||
<HStack>
|
||||
<Icon as={FaCalendarCheck} color="green.400" boxSize="0.9em" />
|
||||
<Text>{stats.completedLessons} {t('journal.pl.statistics.completed')}</Text>
|
||||
</HStack>
|
||||
</StatHelpText>
|
||||
</Stat>
|
||||
|
||||
{/* Статистика по посещаемости */}
|
||||
<Stat
|
||||
bg={statBgColor}
|
||||
p={3}
|
||||
borderRadius="lg"
|
||||
boxShadow="sm"
|
||||
borderLeft="4px solid"
|
||||
borderLeftColor="green.400"
|
||||
>
|
||||
<Flex align="center" mb={2}>
|
||||
<Icon as={FaPercentage} color="green.400" mr={2} />
|
||||
<StatLabel>{t('journal.pl.statistics.attendanceRate')}</StatLabel>
|
||||
</Flex>
|
||||
<StatNumber fontSize="2xl">
|
||||
{Math.round(stats.attendanceRate)}%
|
||||
</StatNumber>
|
||||
<StatHelpText>
|
||||
{stats.attendanceTrend !== 0 && (
|
||||
<Flex align="center">
|
||||
<StatArrow
|
||||
type={stats.attendanceTrend > 0 ? 'increase' : 'decrease'}
|
||||
/>
|
||||
<Text>
|
||||
{Math.abs(Math.round(stats.attendanceTrend))}%
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</StatHelpText>
|
||||
</Stat>
|
||||
|
||||
{/* Статистика по студентам */}
|
||||
<Stat
|
||||
bg={statBgColor}
|
||||
p={3}
|
||||
borderRadius="lg"
|
||||
boxShadow="sm"
|
||||
borderLeft="4px solid"
|
||||
borderLeftColor="purple.400"
|
||||
>
|
||||
<Flex align="center" mb={2}>
|
||||
<Icon as={FaUserGraduate} color="purple.400" mr={2} />
|
||||
<StatLabel>{t('journal.pl.statistics.totalStudents')}</StatLabel>
|
||||
</Flex>
|
||||
<StatNumber fontSize="2xl">{stats.totalStudents}</StatNumber>
|
||||
<StatHelpText mb={0}>
|
||||
<Text>
|
||||
~ {stats.averageStudentsPerLesson} {t('journal.pl.statistics.perLesson')}
|
||||
</Text>
|
||||
</StatHelpText>
|
||||
</Stat>
|
||||
|
||||
{/* Следующее занятие */}
|
||||
<Stat
|
||||
bg={statBgColor}
|
||||
p={3}
|
||||
borderRadius="lg"
|
||||
boxShadow="sm"
|
||||
borderLeft="4px solid"
|
||||
borderLeftColor="orange.400"
|
||||
>
|
||||
<Flex align="center" mb={2}>
|
||||
<Icon as={FaClock} color="orange.400" mr={2} />
|
||||
<StatLabel>{t('journal.pl.statistics.nextLesson')}</StatLabel>
|
||||
</Flex>
|
||||
<StatNumber fontSize="xl">
|
||||
{stats.nextLessonDate
|
||||
? dayjs(stats.nextLessonDate).format('DD.MM.YYYY')
|
||||
: t('journal.pl.statistics.noUpcoming')
|
||||
}
|
||||
</StatNumber>
|
||||
<StatHelpText mb={0}>
|
||||
{stats.nextLessonDate && (
|
||||
<Text>
|
||||
{t('journal.pl.statistics.in')} {stats.daysUntilNextLesson} {t('journal.pl.statistics.days')}
|
||||
</Text>
|
||||
)}
|
||||
</StatHelpText>
|
||||
</Stat>
|
||||
</SimpleGrid>
|
||||
|
||||
<Box
|
||||
bg={statBgColor}
|
||||
p={4}
|
||||
borderRadius="lg"
|
||||
boxShadow="sm"
|
||||
borderTop="1px solid"
|
||||
borderColor={borderColor}
|
||||
>
|
||||
<Text fontWeight="bold" mb={2}>
|
||||
{t('journal.pl.statistics.courseProgress')}
|
||||
</Text>
|
||||
<Progress
|
||||
value={stats.percentageCompleted}
|
||||
size="lg"
|
||||
borderRadius="md"
|
||||
colorScheme={getProgressColor(stats.percentageCompleted)}
|
||||
mb={1}
|
||||
hasStripe
|
||||
/>
|
||||
<Flex justify="space-between" fontSize="sm">
|
||||
<Text>
|
||||
{t('journal.pl.statistics.completed')}: {stats.completedLessons} / {stats.totalLessons}
|
||||
</Text>
|
||||
<Text fontWeight="medium">
|
||||
{Math.round(stats.percentageCompleted)}%
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
@ -40,6 +40,7 @@ import { LessonForm } from './components/lessons-form'
|
||||
import { Bar } from './components/bar'
|
||||
import { LessonItems } from './components/lesson-items'
|
||||
import { BreadcrumbsWrapper } from './style'
|
||||
import { CourseStatistics } from './components/statistics'
|
||||
|
||||
const features = getFeatures('journal')
|
||||
|
||||
@ -346,6 +347,12 @@ const LessonList = () => {
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Статистика курса */}
|
||||
{!showForm && (
|
||||
<CourseStatistics lessons={sorted} isLoading={isLoading} />
|
||||
)}
|
||||
|
||||
{barFeature && sorted?.length > 1 && (
|
||||
<Box height="300">
|
||||
<Bar
|
||||
|
Loading…
x
Reference in New Issue
Block a user