From 510d052116dfb05b1580f4c6b17361a02276c6db Mon Sep 17 00:00:00 2001 From: primakov Date: Sun, 23 Mar 2025 18:44:53 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=BB=D0=BE?= =?UTF-8?q?=D0=BA=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BF=D1=80=D0=BE=D1=88=D0=B5=D0=B4=D1=88=D0=B8?= =?UTF-8?q?=D1=85=20=D1=83=D1=80=D0=BE=D0=BA=D0=BE=D0=B2=20=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=81=D0=B5=D1=89=D0=B0=D0=B5=D0=BC=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B8.=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D1=8B=20=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82=D0=B8=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=BE=D0=B4=D1=81?= =?UTF-8?q?=D0=BA=D0=B0=D0=B7=D0=BE=D0=BA=20=D1=81=20=D0=B8=D0=BD=D1=84?= =?UTF-8?q?=D0=BE=D1=80=D0=BC=D0=B0=D1=86=D0=B8=D0=B5=D0=B9=20=D0=BE=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=81=D0=B5=D1=89=D0=B0=D0=B5=D0=BC=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D0=B8=20=D0=BF=D1=80=D0=BE=D1=88=D0=B5=D0=B4?= =?UTF-8?q?=D1=88=D0=B8=D1=85=20=D0=B7=D0=B0=D0=BD=D1=8F=D1=82=D0=B8=D1=8F?= =?UTF-8?q?=D1=85.=20=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=BE=20?= =?UTF-8?q?=D0=B2=D0=B7=D0=B0=D0=B8=D0=BC=D0=BE=D0=B4=D0=B5=D0=B9=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D0=B8=D0=B5=20=D1=81=20=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=BC=20=D1=87?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B7=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D1=81=D0=BA=D0=B0=D0=B7=D0=BE=D0=BA=20=D0=B2=20=D0=B8=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=D0=B5.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/en.json | 5 +- locales/ru.json | 5 +- .../components/statistics/ActivityStats.tsx | 37 ++++++--- .../statistics/CourseAttendanceList.tsx | 34 ++++++-- .../statistics/StudentAttendanceList.tsx | 28 +++++-- .../statistics/WeekdayActivityChart.tsx | 80 ++++++++++++++++--- .../components/statistics/useStats.ts | 32 ++++---- 7 files changed, 168 insertions(+), 53 deletions(-) diff --git a/locales/en.json b/locales/en.json index 4a9c5da..b104121 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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" } \ No newline at end of file diff --git a/locales/ru.json b/locales/ru.json index ce69ac7..d4bc5b3 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -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": "Посещаемость рассчитана только по прошедшим занятиям" } \ No newline at end of file diff --git a/src/pages/course-list/components/statistics/ActivityStats.tsx b/src/pages/course-list/components/statistics/ActivityStats.tsx index 662b5aa..43fa195 100644 --- a/src/pages/course-list/components/statistics/ActivityStats.tsx +++ b/src/pages/course-list/components/statistics/ActivityStats.tsx @@ -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 = ({ stats }) => { return ( - - {t('journal.pl.overview.activityStats')} - + + + {t('journal.pl.overview.activityStats')} + + + + + - - {t('journal.pl.overview.courseCompletion')}: - + + + {t('journal.pl.overview.courseCompletion')}: + + + + + = ({ stats }) => { - - {t('journal.pl.overview.studentAttendance')}: - + + + {t('journal.pl.overview.studentAttendance')}: + + + + + @@ -33,25 +35,41 @@ export const CourseAttendanceList: React.FC = ({ courses return ( - - {t('journal.pl.overview.topAttendanceCourses')}: + + + {t('journal.pl.overview.topAttendanceCourses')} {courses.map((course, index) => ( - + #{index + 1} - - {course.name} - - + + + {course.name} + + + {Math.round(course.attendanceRate)}% ))} + + + {t('journal.pl.overview.attendanceHelp')} + ) } \ No newline at end of file diff --git a/src/pages/course-list/components/statistics/StudentAttendanceList.tsx b/src/pages/course-list/components/statistics/StudentAttendanceList.tsx index 1c3b932..1afa7fe 100644 --- a/src/pages/course-list/components/statistics/StudentAttendanceList.tsx +++ b/src/pages/course-list/components/statistics/StudentAttendanceList.tsx @@ -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 = ({ {title} + + {t('journal.pl.overview.pastLessonsStats')} + + {students.map((student, index) => ( @@ -58,11 +63,18 @@ export const StudentAttendanceList: React.FC = ({ bg={index < 3 ? ['yellow.400', 'gray.400', 'orange.300'][index] : 'blue.300'} /> - - - {student.name} - - + + + + {student.name} + + + + + {student.attended}/{student.total} + + + = ({ ))} + + + {t('journal.pl.overview.attendanceHelp')} + ) } \ No newline at end of file diff --git a/src/pages/course-list/components/statistics/WeekdayActivityChart.tsx b/src/pages/course-list/components/statistics/WeekdayActivityChart.tsx index 3d29faa..f10bdae 100644 --- a/src/pages/course-list/components/statistics/WeekdayActivityChart.tsx +++ b/src/pages/course-list/components/statistics/WeekdayActivityChart.tsx @@ -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 ( - - {t('journal.pl.overview.mostActiveDay')}: + + + {t('journal.pl.overview.mostActiveDay')}: + + + {getDayOfWeekName(mostActiveDayIndex)} + + + + + {t('journal.pl.overview.pastLessonsStats')} - - {getDayOfWeekName(mostActiveDayIndex)} - {/* Визуализация активности по дням недели */} - + {weekdayActivity.map((count, index) => ( - + + {/* Область для числа */} + + {count > 0 && ( + + {count} + + )} + + + {/* Столбец графика */} - - {getShortDayName(index)} - + + {/* Буква дня недели */} + + + {getShortDayName(index)} + + + + {/* Процент */} + + {count > 0 && totalLessons > 0 && ( + + {Math.round((count / totalLessons) * 100)}% + + )} + ))} + + + {t('journal.pl.overview.dayOfWeekHelp')} + ) -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/pages/course-list/components/statistics/useStats.ts b/src/pages/course-list/components/statistics/useStats.ts index da24081..0712a51 100644 --- a/src/pages/course-list/components/statistics/useStats.ts +++ b/src/pages/course-list/components/statistics/useStats.ts @@ -89,6 +89,9 @@ export const useStats = ( uniqueTeachers.add(teacher.sub) }) + // Добавляем студентов в множество + const courseUniqueStudents = new Set() + // Получаем детализированные данные об уроках курса (если доступны) 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() - 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 }) // Находим самый активный день недели