diff --git a/api/app/controllers/courses_router.py b/api/app/controllers/courses_router.py
index 49f55ea..4a9ba5d 100644
--- a/api/app/controllers/courses_router.py
+++ b/api/app/controllers/courses_router.py
@@ -7,11 +7,13 @@ from app.database.session import get_db
from app.domain.entities.course_teachers import CourseTeacherRead, CourseTeacherCreate
from app.domain.entities.courses import CourseRead, CourseCreate, CourseUpdate, CourseCreated
from app.domain.entities.enrollments import EnrollmentRead, EnrollmentCreate
+from app.domain.entities.gradebook import GradeBookRead
from app.domain.models import User
from app.infrastructure.course_teachers_service import CourseTeachersService
from app.infrastructure.courses_service import CoursesService
from app.infrastructure.dependencies import require_auth_user, require_teacher, require_admin
from app.infrastructure.enrollments_service import EnrollmentsService
+from app.infrastructure.gradebook_service import GradeBookService
courses_router = APIRouter()
@@ -152,3 +154,13 @@ async def replace_course_students(
):
service = EnrollmentsService(db)
return await service.replace_course_students_list(students, course_id)
+
+
+@courses_router.get("/gradebook/{course_id}/", response_model=GradeBookRead)
+async def get_gradebook(
+ course_id: int,
+ db: AsyncSession = Depends(get_db),
+ user: User = Depends(require_auth_user)
+):
+ service = GradeBookService(db)
+ return await service.get_gradebook_by_course_id(course_id)
diff --git a/api/app/domain/entities/gradebook.py b/api/app/domain/entities/gradebook.py
new file mode 100644
index 0000000..8e8844c
--- /dev/null
+++ b/api/app/domain/entities/gradebook.py
@@ -0,0 +1,44 @@
+from typing import List, Optional, Dict
+from pydantic import BaseModel
+
+
+class LessonInfo(BaseModel):
+ id: int
+ title: str
+ number: int
+
+ class Config:
+ from_attributes = True
+
+
+class TaskInfo(BaseModel):
+ id: int
+ title: str
+ number: int
+ max_points: int = 100
+
+ class Config:
+ from_attributes = True
+
+
+class StudentProgress(BaseModel):
+ student_id: int
+ first_name: str
+ last_name: str
+ patronymic: str = ""
+ read_lesson_ids: List[int] = []
+ task_grades: Dict[int, Optional[int]]
+
+ class Config:
+ from_attributes = True
+
+
+class GradeBookRead(BaseModel):
+ course_id: int
+ course_title: str
+ lessons: List[LessonInfo]
+ tasks: List[TaskInfo]
+ students: List[StudentProgress]
+
+ class Config:
+ from_attributes = True
\ No newline at end of file
diff --git a/api/app/infrastructure/gradebook_service.py b/api/app/infrastructure/gradebook_service.py
new file mode 100644
index 0000000..967fa26
--- /dev/null
+++ b/api/app/infrastructure/gradebook_service.py
@@ -0,0 +1,97 @@
+from typing import Dict
+
+from sqlalchemy import select, func
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.domain.entities.gradebook import GradeBookRead, StudentProgress, LessonInfo, TaskInfo
+from app.domain.models import Course, Lesson, Task, User, Enrollment, UserCheckLessons, Solution
+
+
+class GradeBookService:
+ def __init__(self, db: AsyncSession):
+ self.db = db
+
+ async def get_gradebook_by_course_id(self, course_id: int) -> GradeBookRead:
+ # 1. Курс
+ course = await self.db.scalar(select(Course).filter_by(id=course_id))
+ if not course:
+ raise ValueError("Курс не найден")
+
+ # 2. Лекции
+ lessons = (await self.db.execute(
+ select(Lesson).filter_by(course_id=course_id).order_by(Lesson.number)
+ )).scalars().all()
+
+ # 3. Задания
+ tasks = (await self.db.execute(
+ select(Task).filter_by(course_id=course_id).order_by(Task.number)
+ )).scalars().all()
+
+ # 4. Студенты
+ students = (await self.db.execute(
+ select(User)
+ .join(Enrollment, User.id == Enrollment.student_id)
+ .filter(Enrollment.course_id == course_id)
+ .order_by(User.last_name, User.first_name)
+ )).scalars().all()
+
+ student_progress_map: dict[int, StudentProgress] = {}
+
+ if students:
+ student_ids = [s.id for s in students]
+
+ # --- Прочитанные лекции ---
+ read_result = await self.db.execute(
+ select(UserCheckLessons.user_id, UserCheckLessons.lesson_id)
+ .filter(UserCheckLessons.user_id.in_(student_ids))
+ )
+ read_map: dict[int, set[int]] = {}
+ for user_id, lesson_id in read_result:
+ read_map.setdefault(user_id, set()).add(lesson_id)
+
+ # --- МАКСИМАЛЬНЫЕ ОЦЕНКИ (ТОЛЬКО ИЗ ОЦЕНЕННЫХ РЕШЕНИЙ!) ---
+ grades_result = await self.db.execute(
+ select(
+ Solution.student_id,
+ Solution.task_id,
+ func.max(Solution.assessment)
+ )
+ .where(
+ Solution.student_id.in_(student_ids),
+ Solution.assessment.is_not(None) # ← Вот это главное!
+ )
+ .group_by(Solution.student_id, Solution.task_id)
+ )
+
+ grade_map: dict[tuple[int, int], int] = {
+ (student_id, task_id): best_grade
+ for student_id, task_id, best_grade in grades_result
+ }
+
+ # --- Собираем прогресс ---
+ for student in students:
+ student_progress_map[student.id] = StudentProgress(
+ student_id=student.id,
+ first_name=student.first_name,
+ last_name=student.last_name,
+ patronymic=student.patronymic or "",
+ read_lesson_ids=sorted(read_map.get(student.id, set())),
+ task_grades={
+ task.id: grade_map.get((student.id, task.id))
+ for task in tasks
+ }
+ )
+
+ return GradeBookRead(
+ course_id=course_id,
+ course_title=course.title,
+ lessons=[
+ LessonInfo(id=l.id, title=l.title, number=l.number)
+ for l in lessons
+ ],
+ tasks=[
+ TaskInfo(id=t.id, title=t.title, number=t.number)
+ for t in tasks
+ ],
+ students=list(student_progress_map.values())
+ )
\ No newline at end of file
diff --git a/web/src/Api/coursesApi.js b/web/src/Api/coursesApi.js
index 91d57da..3de7b6d 100644
--- a/web/src/Api/coursesApi.js
+++ b/web/src/Api/coursesApi.js
@@ -74,6 +74,13 @@ export const coursesApi = createApi({
}),
invalidatesTags: ['student'],
}),
+ getGradebookByCourse: builder.query({
+ query: (courseId) => ({
+ url: `/courses/gradebook/${courseId}/`,
+ method: "GET",
+ }),
+ providesTags: ['gradebook'],
+ }),
}),
});
@@ -87,4 +94,5 @@ export const {
useReplaceCourseTeachersMutation,
useGetCourseStudentsQuery,
useReplaceCourseStudentsMutation,
+ useGetGradebookByCourseQuery,
} = coursesApi;
\ No newline at end of file
diff --git a/web/src/Components/Pages/CourseDetailPage/Components/ViewTaskModalForm/useTaskLessonModal.js b/web/src/Components/Pages/CourseDetailPage/Components/ViewTaskModalForm/useTaskLessonModal.js
index 42ac609..1db6f67 100644
--- a/web/src/Components/Pages/CourseDetailPage/Components/ViewTaskModalForm/useTaskLessonModal.js
+++ b/web/src/Components/Pages/CourseDetailPage/Components/ViewTaskModalForm/useTaskLessonModal.js
@@ -214,7 +214,7 @@ const useViewTaskModal = () => {
}
- const response = await fetch(`${CONFIG.BASE_URL}/solutions/file/${fileId}/`, {
+ const response = await fetch(`${CONFIG.BASE_URL}/tasks/file/${fileId}/`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
diff --git a/web/src/Components/Pages/CourseDetailPage/CourseDetailPage.jsx b/web/src/Components/Pages/CourseDetailPage/CourseDetailPage.jsx
index 15de2b7..63474db 100644
--- a/web/src/Components/Pages/CourseDetailPage/CourseDetailPage.jsx
+++ b/web/src/Components/Pages/CourseDetailPage/CourseDetailPage.jsx
@@ -5,7 +5,7 @@ import {
Card,
Col,
Empty,
- FloatButton,
+ FloatButton, Grid,
Popconfirm,
Result,
Row,
@@ -19,8 +19,8 @@ import {
BookOutlined, CheckCircleFilled, ClockCircleOutlined,
DeleteOutlined,
EditOutlined,
- FormOutlined,
- PlusOutlined
+ FormOutlined, MenuFoldOutlined, MenuUnfoldOutlined,
+ PlusOutlined, TableOutlined
} from "@ant-design/icons";
import {useNavigate, useParams} from "react-router-dom";
import {ROLES} from "../../../Core/constants.js";
@@ -32,12 +32,17 @@ import UpdateLessonModalForm from "./Components/UpdateLessonModalForm/UpdateLess
import CreateTaskModalForm from "./Components/CreateTaskModalForm/CreateTaskModalForm.jsx";
import UpdateTaskModalForm from "./Components/UpdateTaskModalForm/UpdateTaskModalForm.jsx";
import ViewTaskModal from "./Components/ViewTaskModalForm/ViewTaskModal.jsx";
+import {useState} from "react";
const {Title, Text} = Typography;
+const {useBreakpoint} = Grid;
const CourseDetailPage = () => {
const navigate = useNavigate();
+ const screens = useBreakpoint();
+ const [hovered, setHovered] = useState(false);
+
const {courseId} = useParams();
const {
tasksData,
@@ -242,6 +247,39 @@ const CourseDetailPage = () => {
+
setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ >
+
+
{[CONFIG.ROOT_ROLE_NAME, ROLES.TEACHER].includes(userData.role.title) && (
{
onClick={handleCreateTask}
/>
- )}
+
+ )
+ }
)
};
diff --git a/web/src/Components/Pages/GradebookPage/GradebookPage.jsx b/web/src/Components/Pages/GradebookPage/GradebookPage.jsx
index c4dd14c..584e096 100644
--- a/web/src/Components/Pages/GradebookPage/GradebookPage.jsx
+++ b/web/src/Components/Pages/GradebookPage/GradebookPage.jsx
@@ -1,723 +1,217 @@
-import { useState, useMemo } from "react";
-import { Avatar, Progress, Tag, Tooltip, Space, Button, Table, Card, Row, Col, Statistic, Select, Typography, Input, Modal, Badge } from "antd";
-import { UserOutlined, SearchOutlined, SortAscendingOutlined, SortDescendingOutlined, FilterOutlined } from "@ant-design/icons";
+import {useParams, useNavigate} from "react-router-dom";
+import {
+ Table,
+ Card,
+ Typography,
+ Spin,
+ Button,
+ Space,
+ Tag,
+ Tooltip,
+ Empty,
+ Result,
+ Avatar, Statistic,
+} from "antd";
+import {
+ ArrowLeftOutlined,
+ CheckCircleFilled,
+ ClockCircleOutlined,
+ MinusOutlined,
+ UserOutlined,
+} from "@ant-design/icons";
+import {
+ useGetCourseByIdQuery,
+ useGetGradebookByCourseQuery,
+} from "../../../Api/coursesApi.js";
+import {useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js";
-const { Title } = Typography;
-const { Search } = Input;
+const {Title, Text} = Typography;
-const GradebookPage = ({ onLogout }) => {
- const [selectedCourse, setSelectedCourse] = useState(null);
- const [isLoading, setIsLoading] = useState(false);
- const [filters, setFilters] = useState({
- groups: [],
- searchText: "",
- progressRange: [0, 100]
- });
- const [sortConfig, setSortConfig] = useState({
- key: null,
- direction: 'ascend'
- });
- const [isSearchModalVisible, setIsSearchModalVisible] = useState(false);
+const GradebookPage = () => {
+ const navigate = useNavigate();
+ const {courseId} = useParams();
- // Заглушки для данных
- const courses = [
- { id: 1, title: "Основы программирования" },
- { id: 2, title: "Веб-разработка" },
- { id: 3, title: "Базы данных" },
- { id: 4, title: "Алгоритмы и структуры данных" },
- ];
+ // Данные текущего пользователя
+ const {data: userData, isLoading: userLoading} = useGetAuthenticatedUserDataQuery();
- const assignments = [
- { id: 1, name: "ДЗ №1", maxScore: 100 },
- { id: 2, name: "ДЗ №2", maxScore: 100 },
- { id: 3, name: "ДЗ №3", maxScore: 100 },
- { id: 4, name: "Проект", maxScore: 200 },
- ];
+ const {
+ data: courseData,
+ isLoading: courseLoading,
+ } = useGetCourseByIdQuery(courseId);
- const students = [
- {
- id: 1,
- firstName: "Иван",
- lastName: "Иванов",
- email: "ivanov@mail.com",
- group: "ПИ-201",
- grades: { 1: 85, 2: 92, 3: 78, 4: 180 },
- progress: 85
- },
- {
- id: 2,
- firstName: "Мария",
- lastName: "Петрова",
- email: "petrova@mail.com",
- group: "ПИ-201",
- grades: { 1: 95, 2: 88, 3: 91, 4: 190 },
- progress: 92
- },
- {
- id: 3,
- firstName: "Алексей",
- lastName: "Сидоров",
- email: "sidorov@mail.com",
- group: "ПИ-202",
- grades: { 1: 72, 2: null, 3: 85, 4: null },
- progress: 52
- },
- {
- id: 4,
- firstName: "Екатерина",
- lastName: "Смирнова",
- email: "smirnova@mail.com",
- group: "ПИ-202",
- grades: { 1: 88, 2: 90, 3: 87, 4: 175 },
- progress: 88
- },
- {
- id: 5,
- firstName: "Дмитрий",
- lastName: "Козлов",
- email: "kozlov@mail.com",
- group: "ПИ-203",
- grades: { 1: 65, 2: 70, 3: null, 4: null },
- progress: 45
- },
- {
- id: 6,
- firstName: "Анна",
- lastName: "Волкова",
- email: "volkova@mail.com",
- group: "ПИ-201",
- grades: { 1: 100, 2: 98, 3: 95, 4: 195 },
- progress: 97
- },
- {
- id: 7,
- firstName: "Сергей",
- lastName: "Орлов",
- email: "orlov@mail.com",
- group: "ПИ-203",
- grades: { 1: 80, 2: 85, 3: 82, 4: 170 },
- progress: 79
- },
- {
- id: 8,
- firstName: "Ольга",
- lastName: "Лебедева",
- email: "lebedeva@mail.com",
- group: "ПИ-202",
- grades: { 1: 90, 2: 92, 3: 88, 4: 185 },
- progress: 89
- },
- ];
+ const {
+ data: gradebook,
+ isLoading: gradebookLoading,
+ isError: gradebookError,
+ } = useGetGradebookByCourseQuery(courseId, {pollingInterval: 15000});
- // Получение уникальных групп для фильтра
- const uniqueGroups = useMemo(() => {
- return [...new Set(students.map(student => student.group))];
- }, [students]);
-
- // Фильтрация и сортировка данных
- const filteredAndSortedStudents = useMemo(() => {
- let filtered = students.filter(student => {
- // Фильтр по группам
- if (filters.groups.length > 0 && !filters.groups.includes(student.group)) {
- return false;
- }
-
- // Поиск по тексту (ФИО, email, группа)
- if (filters.searchText) {
- const searchLower = filters.searchText.toLowerCase();
- const fullName = `${student.firstName} ${student.lastName}`.toLowerCase();
- if (!fullName.includes(searchLower) &&
- !student.email.toLowerCase().includes(searchLower) &&
- !student.group.toLowerCase().includes(searchLower)) {
- return false;
- }
- }
-
- // Фильтр по прогрессу
- if (student.progress < filters.progressRange[0] || student.progress > filters.progressRange[1]) {
- return false;
- }
-
- return true;
- });
-
- // Сортировка
- if (sortConfig.key) {
- filtered.sort((a, b) => {
- let aValue, bValue;
-
- switch (sortConfig.key) {
- case 'student':
- aValue = `${a.firstName} ${a.lastName}`;
- bValue = `${b.firstName} ${b.lastName}`;
- break;
- case 'group':
- aValue = a.group;
- bValue = b.group;
- break;
- case 'progress':
- aValue = a.progress;
- bValue = b.progress;
- break;
- case 'average':
- const aGrades = Object.values(a.grades).filter(g => g !== null);
- const bGrades = Object.values(b.grades).filter(g => g !== null);
- aValue = aGrades.length > 0 ? aGrades.reduce((sum, grade) => sum + grade, 0) / aGrades.length : 0;
- bValue = bGrades.length > 0 ? bGrades.reduce((sum, grade) => sum + grade, 0) / bGrades.length : 0;
- break;
- default:
- // Сортировка по заданиям
- if (sortConfig.key.startsWith('assignment_')) {
- const assignmentId = parseInt(sortConfig.key.split('_')[1]);
- aValue = a.grades[assignmentId] || 0;
- bValue = b.grades[assignmentId] || 0;
- } else {
- aValue = a[sortConfig.key];
- bValue = b[sortConfig.key];
- }
- }
-
- if (aValue < bValue) {
- return sortConfig.direction === 'ascend' ? -1 : 1;
- }
- if (aValue > bValue) {
- return sortConfig.direction === 'ascend' ? 1 : -1;
- }
- return 0;
- });
- }
-
- return filtered;
- }, [students, filters, sortConfig]);
-
- // Обработчики фильтров
- const handleGroupFilter = (selectedGroups) => {
- setFilters(prev => ({ ...prev, groups: selectedGroups }));
- };
-
- const handleSearch = (value) => {
- setFilters(prev => ({ ...prev, searchText: value }));
- setIsSearchModalVisible(false);
- };
-
- const handleSort = (key) => {
- setSortConfig(prev => ({
- key,
- direction: prev.key === key && prev.direction === 'ascend' ? 'descend' : 'ascend'
- }));
- };
-
- // Колонки таблицы с фильтрацией и сортировкой
- const columns = useMemo(() => {
- const baseColumns = [
- {
- title: (
-
- Студент
- : ) :
-
- }
- onClick={() => handleSort('student')}
- />
-
- }
- onClick={() => setIsSearchModalVisible(true)}
- style={{
- color: filters.searchText ? '#1890ff' : undefined
- }}
- />
-
-
- ),
- key: "student",
- fixed: "left",
- width: 250,
- render: (_, record) => (
-
- }
- >
- {record.firstName[0]}{record.lastName[0]}
-
-
-
- {record.firstName} {record.lastName}
-
-
- {record.email}
-
-
-
- ),
- },
- {
- title: (
-
- Группа
- : ) :
-
- }
- onClick={() => handleSort('group')}
- />
-
- ),
- dataIndex: "group",
- key: "group",
- width: 120,
- render: (group) => {group},
- filters: uniqueGroups.map(group => ({ text: group, value: group })),
- filteredValue: filters.groups,
- onFilter: (value, record) => record.group === value,
- },
- ];
-
- // Колонки для заданий
- const assignmentColumns = assignments.map(assignment => ({
- title: (
-
- {assignment.name}
- : ) :
-
- }
- onClick={() => handleSort(`assignment_${assignment.id}`)}
- />
-
- ),
- key: `assignment_${assignment.id}`,
- width: 130,
- align: "center",
- render: (_, record) => {
- const grade = record.grades[assignment.id];
- if (grade === null || grade === undefined) {
- return Не сдано;
- }
-
- const percentage = (grade / assignment.maxScore) * 100;
- let color = "red";
- if (percentage >= 90) color = "green";
- else if (percentage >= 75) color = "blue";
- else if (percentage >= 60) color = "orange";
+ const isLoading = courseLoading || gradebookLoading || userLoading;
+ if (isLoading) {
return (
-
- {grade}
-
+
+
+
);
- }
- }));
-
- // Колонки итогов
- const summaryColumns = [
- {
- title: (
-
- Средний балл
- : ) :
-
- }
- onClick={() => handleSort('average')}
- />
-
- ),
- key: "average",
- width: 140,
- align: "center",
- render: (_, record) => {
- const grades = Object.values(record.grades).filter(g => g !== null);
- if (grades.length === 0) return Нет оценок;
-
- const totalMax = assignments.reduce((sum, a) => sum + a.maxScore, 0);
- const totalGrade = Object.entries(record.grades)
- .reduce((sum, [assignmentId, grade]) => {
- if (grade === null) return sum;
- return sum + grade;
- }, 0);
-
- const percentage = (totalGrade / totalMax) * 100;
-
- return (
-
- = 75 ? "green" : percentage >= 60 ? "orange" : "red"}>
- {percentage.toFixed(1)}%
-
-
- );
- }
- },
- {
- title: (
-
- Прогресс
- : ) :
-
- }
- onClick={() => handleSort('progress')}
- />
-
- ),
- key: "progress",
- width: 160,
- render: (_, record) => (
-
-
-
- )
}
+
+ if (gradebookError || !gradebook) {
+ return ;
+ }
+
+ const isStudent = userData?.role?.title === "student";
+ const currentStudentId = userData?.id;
+
+ // Если студент — фильтруем только его данные
+ const visibleStudents = isStudent
+ ? gradebook.students.filter((s) => s.student_id === currentStudentId)
+ : gradebook.students;
+
+ // Если студент, но его нет в курсе — покажем сообщение
+ if (isStudent && visibleStudents.length === 0) {
+ return (
+
+ navigate(-1)}>
+ Назад
+
+ }
+ />
+
+ );
+ }
+
+ // === Колонки ===
+ const baseColumns = [
+ {
+ title: "№",
+ width: 60,
+ fixed: "left",
+ render: (_, __, index) => index + 1,
+ },
+ {
+ title: "Студент",
+ fixed: "left",
+ width: isStudent ? 300 : 240,
+ render: (record) => {
+ const fullName = `${record.last_name} ${record.first_name} ${
+ record.patronymic ? record.patronymic[0] + "." : ""
+ }`;
+
+ if (isStudent) {
+ return (
+
+ } style={{backgroundColor: "#1890ff"}}/>
+
+ {fullName}
+
+ Ваша успеваемость
+
+
+ );
+ }
+ return {fullName};
+ },
+ },
];
- return [...baseColumns, ...assignmentColumns, ...summaryColumns];
- }, [assignments, filters.groups, sortConfig, uniqueGroups, filters.searchText]);
+ const lessonColumns = gradebook.lessons.map((lesson) => ({
+ title: (
+
+ Л{lesson.number}
+
+ ),
+ width: 70,
+ align: "center",
+ render: (record) =>
+ record.read_lesson_ids.includes(lesson.id) ? (
+
+ ) : (
+
+ ),
+ }));
- // Статистика с учетом фильтров
- const statistics = useMemo(() => {
- if (!selectedCourse) {
- return {
- totalStudents: 0,
- averageGrade: 0,
- completedAssignments: 0,
- totalAssignments: 0,
- pendingReview: 0
- };
- }
+ const taskColumns = gradebook.tasks.map((task) => ({
+ title: (
+
+ З{task.number}
+
+ ),
+ width: 90,
+ align: "center",
+ render: (record) => {
+ const grade = record.task_grades[task.id];
+ if (grade === null || grade === undefined) {
+ return ;
+ }
+ const color = grade >= 90 ? "green" : grade >= 70 ? "orange" : "red";
+ return {grade};
+ },
+ }));
- const totalStudents = filteredAndSortedStudents.length;
- const totalAssignments = assignments.length * filteredAndSortedStudents.length;
-
- let completedCount = 0;
- let totalGradeSum = 0;
- let gradedCount = 0;
+ const columns = [...baseColumns, ...lessonColumns, ...taskColumns];
- filteredAndSortedStudents.forEach(student => {
- Object.values(student.grades).forEach(grade => {
- if (grade !== null) {
- completedCount++;
- totalGradeSum += grade;
- gradedCount++;
- }
- });
- });
+ const dataSource = visibleStudents.map((student) => ({
+ key: student.student_id,
+ ...student,
+ }));
- const averageGrade = gradedCount > 0 ? (totalGradeSum / gradedCount).toFixed(1) : 0;
- const pendingReview = filteredAndSortedStudents.reduce((count, student) => {
- const nullGrades = Object.values(student.grades).filter(grade => grade === null).length;
- return count + nullGrades;
- }, 0);
+ return (
+
+
+
+
- return {
- totalStudents,
- averageGrade,
- completedAssignments: completedCount,
- totalAssignments,
- pendingReview
- };
- }, [selectedCourse, filteredAndSortedStudents, assignments]);
+
+ {isStudent ? "Моя успеваемость" : "Журнал успеваемости"} — {courseData?.title}
+
- const handleCourseChange = (courseId) => {
- setIsLoading(true);
- setSelectedCourse(courseId);
-
- // Сброс фильтров при смене курса
- setFilters({
- groups: [],
- searchText: "",
- progressRange: [0, 100]
- });
- setSortConfig({
- key: null,
- direction: 'ascend'
- });
+ {!isStudent && (
+
+
+
+
+
+ )}
+
+
- // Имитация загрузки данных
- setTimeout(() => {
- setIsLoading(false);
- }, 500);
- };
+
+
+
- // Модальное окно поиска
- const SearchModal = () => (
-
setIsSearchModalVisible(false)}
- footer={[
- ,
- ,
- ]}
- width={400}
- >
-
-
-
- Поиск по ФИО, email или группе:
-
-
+ {!isStudent && (
+
+ Легенда:
+ 90–100
+ 70–89
+ 0–69
+
+ — прочитано
+
+
+ — не прочитано
+
+
+ — не сдано / не оценено
+
+
+ )}
- {filters.searchText && (
-
-
- Активный поиск: "{filters.searchText}"
-
-
- )}
-
-
- );
-
- return (
-
- {/* Шапка с навигацией */}
-
-
- Электронный журнал
-
-
-
- {/* Выбор курса */}
-
-
- Выберите курс:
-
-
-
- {/* Статистика */}
- {selectedCourse && (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )}
-
- {/* Информация о выбранном курсе */}
- {selectedCourse && (
-
-
-
-
-
- {courses.find(c => c.id === selectedCourse)?.title}
-
-
- Активный курс
-
-
-
-
-
- {filters.groups.length > 0 && (
-
- Фильтр по группам
-
- )}
- {filters.searchText && (
-
-
-
- Поиск: "{filters.searchText}"
-
-
- )}
- {(filters.groups.length > 0 || filters.searchText) && (
-
- )}
-
-
-
-
- )}
-
- {/* Таблица студентов */}
- {selectedCourse && (
-
- ({ ...student, key: student.id }))}
- loading={isLoading}
- scroll={{ x: 1200 }}
- pagination={{
- pageSize: 10,
- showSizeChanger: true,
- showQuickJumper: true,
- showTotal: (total, range) =>
- `Показано ${range[0]}-${range[1]} из ${total} студентов`,
- pageSizeOptions: ['10', '20', '50'],
- style: { marginTop: 16, marginRight: 16 }
- }}
- size="middle"
- />
-
- )}
-
- {/* Сообщение при невыбранном курсе */}
- {!selectedCourse && (
-
-
- Выберите курс для просмотра журнала
-
-
- Пожалуйста, выберите курс из выпадающего списка выше чтобы увидеть успеваемость студентов
-
-
- )}
-
- {/* Модальное окно поиска */}
-
-
- );
+ );
};
export default GradebookPage;
\ No newline at end of file