Регистрация + журнал успеваемости
This commit is contained in:
parent
44ceb93729
commit
3d3d6504c2
17
web/package-lock.json
generated
17
web/package-lock.json
generated
@ -153,6 +153,7 @@
|
|||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@ -2224,6 +2225,7 @@
|
|||||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@ -2271,6 +2273,7 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@ -2439,6 +2442,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.25",
|
"baseline-browser-mapping": "^2.8.25",
|
||||||
"caniuse-lite": "^1.0.30001754",
|
"caniuse-lite": "^1.0.30001754",
|
||||||
@ -2594,7 +2598,8 @@
|
|||||||
"version": "1.11.19",
|
"version": "1.11.19",
|
||||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
|
||||||
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
|
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
@ -2699,6 +2704,7 @@
|
|||||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@ -3410,6 +3416,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -3536,6 +3543,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -3545,6 +3553,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@ -3563,6 +3572,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/use-sync-external-store": "^0.0.6",
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
"use-sync-external-store": "^1.4.0"
|
"use-sync-external-store": "^1.4.0"
|
||||||
@ -3633,7 +3643,8 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/redux-thunk": {
|
"node_modules/redux-thunk": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
@ -3905,6 +3916,7 @@
|
|||||||
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
|
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@ -4026,6 +4038,7 @@
|
|||||||
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
|
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import {Routes, Route, Navigate} from "react-router-dom";
|
|||||||
import PrivateRoute from "./PrivateRoute.jsx";
|
import PrivateRoute from "./PrivateRoute.jsx";
|
||||||
import AdminRoute from "./AdminRoute.jsx";
|
import AdminRoute from "./AdminRoute.jsx";
|
||||||
import LoginPage from "../Components/Pages/LoginPage/LoginPage.jsx";
|
import LoginPage from "../Components/Pages/LoginPage/LoginPage.jsx";
|
||||||
|
import RegisterPage from "../Components/Pages/RegisterPage/RegisterPage.jsx"; // Новая страница регистрации
|
||||||
import CoursesPage from "../Components/Pages/Courses/CoursesPage.jsx";
|
import CoursesPage from "../Components/Pages/Courses/CoursesPage.jsx";
|
||||||
import MainLayout from "../Components/Layouts/MainLayout.jsx";
|
import MainLayout from "../Components/Layouts/MainLayout.jsx";
|
||||||
import ProfilePage from "../Components/Pages/ProfilePage/ProfilePage.jsx";
|
import ProfilePage from "../Components/Pages/ProfilePage/ProfilePage.jsx";
|
||||||
@ -11,6 +12,7 @@ import AdminPage from "../Components/Pages/AdminPage/AdminPage.jsx";
|
|||||||
const AppRouter = () => (
|
const AppRouter = () => (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<LoginPage/>}/>
|
<Route path="/login" element={<LoginPage/>}/>
|
||||||
|
<Route path="/register" element={<RegisterPage/>}/>
|
||||||
|
|
||||||
<Route element={<PrivateRoute/>}>
|
<Route element={<PrivateRoute/>}>
|
||||||
<Route element={<MainLayout/>}>
|
<Route element={<MainLayout/>}>
|
||||||
|
|||||||
723
web/src/Components/Pages/GradebookPage/GradebookPage.jsx
Normal file
723
web/src/Components/Pages/GradebookPage/GradebookPage.jsx
Normal file
@ -0,0 +1,723 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
const { Title } = Typography;
|
||||||
|
const { Search } = Input;
|
||||||
|
|
||||||
|
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 courses = [
|
||||||
|
{ id: 1, title: "Основы программирования" },
|
||||||
|
{ id: 2, title: "Веб-разработка" },
|
||||||
|
{ id: 3, title: "Базы данных" },
|
||||||
|
{ id: 4, title: "Алгоритмы и структуры данных" },
|
||||||
|
];
|
||||||
|
|
||||||
|
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 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 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: (
|
||||||
|
<Space>
|
||||||
|
Студент
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={sortConfig.key === 'student' ?
|
||||||
|
(sortConfig.direction === 'ascend' ? <SortAscendingOutlined /> : <SortDescendingOutlined />) :
|
||||||
|
<SortAscendingOutlined />
|
||||||
|
}
|
||||||
|
onClick={() => handleSort('student')}
|
||||||
|
/>
|
||||||
|
<Tooltip title="Поиск студентов">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<SearchOutlined />}
|
||||||
|
onClick={() => setIsSearchModalVisible(true)}
|
||||||
|
style={{
|
||||||
|
color: filters.searchText ? '#1890ff' : undefined
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
key: "student",
|
||||||
|
fixed: "left",
|
||||||
|
width: 250,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space>
|
||||||
|
<Avatar
|
||||||
|
style={{ backgroundColor: "#1890ff" }}
|
||||||
|
icon={<UserOutlined />}
|
||||||
|
>
|
||||||
|
{record.firstName[0]}{record.lastName[0]}
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 500 }}>
|
||||||
|
{record.firstName} {record.lastName}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 12, color: "#8c8c8c" }}>
|
||||||
|
{record.email}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: (
|
||||||
|
<Space>
|
||||||
|
Группа
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={sortConfig.key === 'group' ?
|
||||||
|
(sortConfig.direction === 'ascend' ? <SortAscendingOutlined /> : <SortDescendingOutlined />) :
|
||||||
|
<SortAscendingOutlined />
|
||||||
|
}
|
||||||
|
onClick={() => handleSort('group')}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
dataIndex: "group",
|
||||||
|
key: "group",
|
||||||
|
width: 120,
|
||||||
|
render: (group) => <Tag color="blue">{group}</Tag>,
|
||||||
|
filters: uniqueGroups.map(group => ({ text: group, value: group })),
|
||||||
|
filteredValue: filters.groups,
|
||||||
|
onFilter: (value, record) => record.group === value,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Колонки для заданий
|
||||||
|
const assignmentColumns = assignments.map(assignment => ({
|
||||||
|
title: (
|
||||||
|
<Space>
|
||||||
|
{assignment.name}
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={sortConfig.key === `assignment_${assignment.id}` ?
|
||||||
|
(sortConfig.direction === 'ascend' ? <SortAscendingOutlined /> : <SortDescendingOutlined />) :
|
||||||
|
<SortAscendingOutlined />
|
||||||
|
}
|
||||||
|
onClick={() => handleSort(`assignment_${assignment.id}`)}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
key: `assignment_${assignment.id}`,
|
||||||
|
width: 130,
|
||||||
|
align: "center",
|
||||||
|
render: (_, record) => {
|
||||||
|
const grade = record.grades[assignment.id];
|
||||||
|
if (grade === null || grade === undefined) {
|
||||||
|
return <Tag color="default">Не сдано</Tag>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={`${grade} из ${assignment.maxScore} (${percentage.toFixed(1)}%)`}>
|
||||||
|
<Tag color={color}>{grade}</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Колонки итогов
|
||||||
|
const summaryColumns = [
|
||||||
|
{
|
||||||
|
title: (
|
||||||
|
<Space>
|
||||||
|
Средний балл
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={sortConfig.key === 'average' ?
|
||||||
|
(sortConfig.direction === 'ascend' ? <SortAscendingOutlined /> : <SortDescendingOutlined />) :
|
||||||
|
<SortAscendingOutlined />
|
||||||
|
}
|
||||||
|
onClick={() => handleSort('average')}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
key: "average",
|
||||||
|
width: 140,
|
||||||
|
align: "center",
|
||||||
|
render: (_, record) => {
|
||||||
|
const grades = Object.values(record.grades).filter(g => g !== null);
|
||||||
|
if (grades.length === 0) return <Tag>Нет оценок</Tag>;
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Tooltip title={`${totalGrade} из ${totalMax} баллов`}>
|
||||||
|
<Tag color={percentage >= 75 ? "green" : percentage >= 60 ? "orange" : "red"}>
|
||||||
|
{percentage.toFixed(1)}%
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: (
|
||||||
|
<Space>
|
||||||
|
Прогресс
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={sortConfig.key === 'progress' ?
|
||||||
|
(sortConfig.direction === 'ascend' ? <SortAscendingOutlined /> : <SortDescendingOutlined />) :
|
||||||
|
<SortAscendingOutlined />
|
||||||
|
}
|
||||||
|
onClick={() => handleSort('progress')}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
key: "progress",
|
||||||
|
width: 160,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Tooltip title={`${record.progress}% выполнено`}>
|
||||||
|
<Progress
|
||||||
|
percent={record.progress}
|
||||||
|
size="small"
|
||||||
|
status={record.progress < 50 ? "exception" : record.progress < 75 ? "normal" : "success"}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return [...baseColumns, ...assignmentColumns, ...summaryColumns];
|
||||||
|
}, [assignments, filters.groups, sortConfig, uniqueGroups, filters.searchText]);
|
||||||
|
|
||||||
|
// Статистика с учетом фильтров
|
||||||
|
const statistics = useMemo(() => {
|
||||||
|
if (!selectedCourse) {
|
||||||
|
return {
|
||||||
|
totalStudents: 0,
|
||||||
|
averageGrade: 0,
|
||||||
|
completedAssignments: 0,
|
||||||
|
totalAssignments: 0,
|
||||||
|
pendingReview: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalStudents = filteredAndSortedStudents.length;
|
||||||
|
const totalAssignments = assignments.length * filteredAndSortedStudents.length;
|
||||||
|
|
||||||
|
let completedCount = 0;
|
||||||
|
let totalGradeSum = 0;
|
||||||
|
let gradedCount = 0;
|
||||||
|
|
||||||
|
filteredAndSortedStudents.forEach(student => {
|
||||||
|
Object.values(student.grades).forEach(grade => {
|
||||||
|
if (grade !== null) {
|
||||||
|
completedCount++;
|
||||||
|
totalGradeSum += grade;
|
||||||
|
gradedCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 {
|
||||||
|
totalStudents,
|
||||||
|
averageGrade,
|
||||||
|
completedAssignments: completedCount,
|
||||||
|
totalAssignments,
|
||||||
|
pendingReview
|
||||||
|
};
|
||||||
|
}, [selectedCourse, filteredAndSortedStudents, assignments]);
|
||||||
|
|
||||||
|
const handleCourseChange = (courseId) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setSelectedCourse(courseId);
|
||||||
|
|
||||||
|
// Сброс фильтров при смене курса
|
||||||
|
setFilters({
|
||||||
|
groups: [],
|
||||||
|
searchText: "",
|
||||||
|
progressRange: [0, 100]
|
||||||
|
});
|
||||||
|
setSortConfig({
|
||||||
|
key: null,
|
||||||
|
direction: 'ascend'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Имитация загрузки данных
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Модальное окно поиска
|
||||||
|
const SearchModal = () => (
|
||||||
|
<Modal
|
||||||
|
title="Поиск студентов"
|
||||||
|
open={isSearchModalVisible}
|
||||||
|
onCancel={() => setIsSearchModalVisible(false)}
|
||||||
|
footer={[
|
||||||
|
<Button key="reset" onClick={() => handleSearch('')}>
|
||||||
|
Сбросить поиск
|
||||||
|
</Button>,
|
||||||
|
<Button key="cancel" onClick={() => setIsSearchModalVisible(false)}>
|
||||||
|
Отмена
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
width={400}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||||
|
<div>
|
||||||
|
<p style={{ marginBottom: 8, color: '#666' }}>
|
||||||
|
Поиск по ФИО, email или группе:
|
||||||
|
</p>
|
||||||
|
<Search
|
||||||
|
placeholder="Введите текст для поиска..."
|
||||||
|
allowClear
|
||||||
|
enterButton="Найти"
|
||||||
|
size="large"
|
||||||
|
defaultValue={filters.searchText}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{filters.searchText && (
|
||||||
|
<div style={{
|
||||||
|
padding: '8px 12px',
|
||||||
|
backgroundColor: '#f0f7ff',
|
||||||
|
borderRadius: 6,
|
||||||
|
border: '1px solid #1890ff'
|
||||||
|
}}>
|
||||||
|
<span style={{ color: '#1890ff', fontSize: 12 }}>
|
||||||
|
Активный поиск: "{filters.searchText}"
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 24, backgroundColor: '#ffffff', minHeight: '100vh' }}>
|
||||||
|
{/* Шапка с навигацией */}
|
||||||
|
<Row justify="space-between" align="middle" style={{ marginBottom: 24 }}>
|
||||||
|
<Col>
|
||||||
|
<Title level={2} style={{ margin: 0, color: '#262626' }}>Электронный журнал</Title>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{/* Выбор курса */}
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
marginBottom: 24,
|
||||||
|
borderRadius: 8,
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
|
||||||
|
}}
|
||||||
|
styles={{ body: { padding: '16px 24px' } }}
|
||||||
|
>
|
||||||
|
<Space size="middle" align="center">
|
||||||
|
<span style={{ fontWeight: 500, fontSize: 16 }}>Выберите курс:</span>
|
||||||
|
<Select
|
||||||
|
placeholder="Выберите курс"
|
||||||
|
style={{ width: 300 }}
|
||||||
|
onChange={handleCourseChange}
|
||||||
|
options={courses.map(course => ({
|
||||||
|
value: course.id,
|
||||||
|
label: course.title
|
||||||
|
}))}
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Статистика */}
|
||||||
|
{selectedCourse && (
|
||||||
|
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||||
|
<Col span={4}>
|
||||||
|
<Card
|
||||||
|
style={{ borderRadius: 8, textAlign: 'center' }}
|
||||||
|
styles={{ body: { padding: '20px 16px' } }}
|
||||||
|
>
|
||||||
|
<Statistic
|
||||||
|
title="Всего студентов"
|
||||||
|
value={statistics.totalStudents}
|
||||||
|
styles={{ content: { color: '#1890ff', fontSize: '28px' } }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={5}>
|
||||||
|
<Card
|
||||||
|
style={{ borderRadius: 8, textAlign: 'center' }}
|
||||||
|
styles={{ body: { padding: '20px 16px' } }}
|
||||||
|
>
|
||||||
|
<Statistic
|
||||||
|
title="Средний балл"
|
||||||
|
value={statistics.averageGrade}
|
||||||
|
styles={{ content: { color: '#52c41a', fontSize: '28px' } }}
|
||||||
|
suffix="баллов"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={5}>
|
||||||
|
<Card
|
||||||
|
style={{ borderRadius: 8, textAlign: 'center' }}
|
||||||
|
styles={{ body: { padding: '20px 16px' } }}
|
||||||
|
>
|
||||||
|
<Statistic
|
||||||
|
title="Выполнено заданий"
|
||||||
|
value={statistics.completedAssignments}
|
||||||
|
styles={{ content: { color: '#fa8c16', fontSize: '28px' } }}
|
||||||
|
suffix={`/ ${statistics.totalAssignments}`}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={5}>
|
||||||
|
<Card
|
||||||
|
style={{ borderRadius: 8, textAlign: 'center' }}
|
||||||
|
styles={{ body: { padding: '20px 16px' } }}
|
||||||
|
>
|
||||||
|
<Statistic
|
||||||
|
title="На проверке"
|
||||||
|
value={statistics.pendingReview}
|
||||||
|
styles={{ content: { color: '#fa541c', fontSize: '28px' } }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={5}>
|
||||||
|
<Card
|
||||||
|
style={{ borderRadius: 8, textAlign: 'center' }}
|
||||||
|
styles={{ body: { padding: '20px 16px' } }}
|
||||||
|
>
|
||||||
|
<Statistic
|
||||||
|
title="Процент выполнения"
|
||||||
|
value={((statistics.completedAssignments / statistics.totalAssignments) * 100).toFixed(1)}
|
||||||
|
styles={{ content: { color: '#722ed1', fontSize: '28px' } }}
|
||||||
|
suffix="%"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Информация о выбранном курсе */}
|
||||||
|
{selectedCourse && (
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
marginBottom: 24,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: '#f0f7ff',
|
||||||
|
border: '1px solid #1890ff'
|
||||||
|
}}
|
||||||
|
styles={{ body: { padding: '16px 24px' } }}
|
||||||
|
>
|
||||||
|
<Row justify="space-between" align="middle">
|
||||||
|
<Col>
|
||||||
|
<Space>
|
||||||
|
<Title level={4} style={{ margin: 0, color: '#1890ff' }}>
|
||||||
|
{courses.find(c => c.id === selectedCourse)?.title}
|
||||||
|
</Title>
|
||||||
|
<Tag color="blue" style={{ fontSize: 14, padding: '4px 8px' }}>
|
||||||
|
Активный курс
|
||||||
|
</Tag>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<Space>
|
||||||
|
{filters.groups.length > 0 && (
|
||||||
|
<Badge count={filters.groups.length} showZero={false}>
|
||||||
|
<Tag color="orange">Фильтр по группам</Tag>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{filters.searchText && (
|
||||||
|
<Tag color="purple">
|
||||||
|
<Space>
|
||||||
|
<SearchOutlined />
|
||||||
|
Поиск: "{filters.searchText}"
|
||||||
|
</Space>
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{(filters.groups.length > 0 || filters.searchText) && (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setFilters({
|
||||||
|
groups: [],
|
||||||
|
searchText: "",
|
||||||
|
progressRange: [0, 100]
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Сбросить фильтры
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Таблица студентов */}
|
||||||
|
{selectedCourse && (
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
borderRadius: 8,
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
|
||||||
|
}}
|
||||||
|
styles={{ body: { padding: 0 } }}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={filteredAndSortedStudents.map(student => ({ ...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"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Сообщение при невыбранном курсе */}
|
||||||
|
{!selectedCourse && (
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: '#fafafa'
|
||||||
|
}}
|
||||||
|
styles={{ body: { padding: '60px' } }}
|
||||||
|
>
|
||||||
|
<Title level={3} style={{ color: '#8c8c8c' }}>
|
||||||
|
Выберите курс для просмотра журнала
|
||||||
|
</Title>
|
||||||
|
<p style={{ color: '#8c8c8c', fontSize: 16 }}>
|
||||||
|
Пожалуйста, выберите курс из выпадающего списка выше чтобы увидеть успеваемость студентов
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Модальное окно поиска */}
|
||||||
|
<SearchModal />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GradebookPage;
|
||||||
@ -1,41 +1,64 @@
|
|||||||
import {Button, Col, Flex, Form, Input, Typography} from "antd";
|
import { Button, Col, Flex, Form, Input, Typography } from "antd";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import useLoginPage from "./useLoginPage.js";
|
import useLoginPage from "./useLoginPage.js";
|
||||||
|
|
||||||
const {Title} = Typography;
|
const { Title } = Typography;
|
||||||
|
|
||||||
const LoginPage = () => {
|
const LoginPage = () => {
|
||||||
const {
|
const {
|
||||||
pageContainerStyle,
|
pageContainerStyle,
|
||||||
onFinish
|
onFinish,
|
||||||
|
isLoading
|
||||||
} = useLoginPage();
|
} = useLoginPage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex vertical align={"center"} justify={"center"}>
|
<Flex
|
||||||
|
vertical
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
style={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
padding: '20px',
|
||||||
|
backgroundColor: '#f0f2f5'
|
||||||
|
}}
|
||||||
|
gap={24}
|
||||||
|
>
|
||||||
<Col style={pageContainerStyle}>
|
<Col style={pageContainerStyle}>
|
||||||
<Title>Аутентификация</Title>
|
<Title style={{ textAlign: 'center', marginBottom: 24 }}>Аутентификация</Title>
|
||||||
<Form name={"login"} onFinish={onFinish}>
|
<Form name="login" onFinish={onFinish}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="login"
|
name="login"
|
||||||
rules={[{required: true, message: "Введите логин"}]}
|
rules={[{ required: true, message: "Введите логин" }]}
|
||||||
>
|
>
|
||||||
<Input
|
<Input placeholder="Логин" size="large" />
|
||||||
placeholder={"Логин"}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item name="password">
|
||||||
name="password"
|
<Input.Password placeholder="Пароль" size="large" />
|
||||||
>
|
|
||||||
<Input.Password placeholder={"Пароль"}/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button type="primary" htmlType="submit" block>
|
<Button
|
||||||
Войти
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
|
Войти
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
|
<Flex align="center" justify="center" style={{ width: '100%' }}>
|
||||||
|
<span style={{ color: '#8c8c8c', marginRight: 8 }}>Нет аккаунта?</span>
|
||||||
|
<Link to="/register">
|
||||||
|
<Button type="link" style={{ padding: 0, fontSize: 14, fontWeight: 500 }}>
|
||||||
|
Зарегистрироваться
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
</Flex>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LoginPage;
|
export default LoginPage;
|
||||||
@ -1,12 +1,12 @@
|
|||||||
import {useNavigate} from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import {useDispatch, useSelector} from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import {useLoginMutation} from "../../../Api/authApi.js";
|
import { useLoginMutation } from "../../../Api/authApi.js";
|
||||||
import {useEffect, useRef} from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import {notification} from "antd";
|
import { notification } from "antd";
|
||||||
import {checkAuth, setError, setUser} from "../../../Redux/Slices/authSlice.js";
|
import { checkAuth, setError, setUser } from "../../../Redux/Slices/authSlice.js";
|
||||||
|
import { message } from "antd";
|
||||||
|
|
||||||
|
const LoginPage = () => {
|
||||||
const useLoginPage = () => {
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [loginUser, { isLoading }] = useLoginMutation();
|
const [loginUser, { isLoading }] = useLoginMutation();
|
||||||
@ -14,7 +14,11 @@ const useLoginPage = () => {
|
|||||||
const hasRedirected = useRef(false);
|
const hasRedirected = useRef(false);
|
||||||
|
|
||||||
const pageContainerStyle = {
|
const pageContainerStyle = {
|
||||||
paddingTop: screen.xs ? "100px" : "200px",
|
width: 400,
|
||||||
|
padding: 24,
|
||||||
|
borderRadius: 8,
|
||||||
|
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)",
|
||||||
|
backgroundColor: "white",
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -26,6 +30,7 @@ const useLoginPage = () => {
|
|||||||
}, [user, userData, isLoading, navigate]);
|
}, [user, userData, isLoading, navigate]);
|
||||||
|
|
||||||
const onFinish = async (loginData) => {
|
const onFinish = async (loginData) => {
|
||||||
|
// РЕАЛЬНАЯ АВТОРИЗАЦИЯ
|
||||||
try {
|
try {
|
||||||
const response = await loginUser(loginData).unwrap();
|
const response = await loginUser(loginData).unwrap();
|
||||||
const token = response.access_token || response.token;
|
const token = response.access_token || response.token;
|
||||||
@ -50,8 +55,9 @@ const useLoginPage = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
pageContainerStyle,
|
pageContainerStyle,
|
||||||
onFinish
|
onFinish,
|
||||||
|
isLoading
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useLoginPage;
|
export default LoginPage;
|
||||||
146
web/src/Components/Pages/RegisterPage/RegisterPage.jsx
Normal file
146
web/src/Components/Pages/RegisterPage/RegisterPage.jsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import {Button, Col, Flex, Form, Input, Select, Typography} from "antd";
|
||||||
|
import {UserOutlined, MailOutlined, LockOutlined} from "@ant-design/icons";
|
||||||
|
import useRegisterPage from "./useRegisterPage.js";
|
||||||
|
|
||||||
|
const {Title, Text} = Typography;
|
||||||
|
|
||||||
|
const RegisterPage = () => {
|
||||||
|
const {
|
||||||
|
pageContainerStyle,
|
||||||
|
onFinish
|
||||||
|
} = useRegisterPage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex vertical align={"center"} justify={"center"}>
|
||||||
|
<Col style={pageContainerStyle}>
|
||||||
|
<Title>Регистрация</Title>
|
||||||
|
<Text type="secondary" style={{display: "block", marginBottom: 24}}>
|
||||||
|
Создайте новый аккаунт для работы с платформой
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
name={"register"}
|
||||||
|
onFinish={onFinish}
|
||||||
|
layout="vertical"
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="Роль"
|
||||||
|
name="role"
|
||||||
|
rules={[{required: true, message: "Выберите роль"}]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder="Выберите роль"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<Select.Option value="student">Студент</Select.Option>
|
||||||
|
<Select.Option value="teacher">Преподаватель</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Имя"
|
||||||
|
name="firstName"
|
||||||
|
rules={[{required: true, message: "Введите имя"}]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<UserOutlined />}
|
||||||
|
placeholder="Иван"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Фамилия"
|
||||||
|
name="lastName"
|
||||||
|
rules={[{required: true, message: "Введите фамилию"}]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<UserOutlined />}
|
||||||
|
placeholder="Иванов"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Email"
|
||||||
|
name="email"
|
||||||
|
rules={[
|
||||||
|
{required: true, message: "Введите email"},
|
||||||
|
{type: "email", message: "Введите корректный email"}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<MailOutlined />}
|
||||||
|
placeholder="example@mail.com"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Логин"
|
||||||
|
name="login"
|
||||||
|
rules={[{required: true, message: "Введите логин"}]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<UserOutlined />}
|
||||||
|
placeholder="Логин"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Пароль"
|
||||||
|
name="password"
|
||||||
|
rules={[
|
||||||
|
{required: true, message: "Введите пароль"},
|
||||||
|
{min: 6, message: "Пароль должен содержать минимум 6 символов"}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
prefix={<LockOutlined />}
|
||||||
|
placeholder="Пароль"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Подтверждение пароля"
|
||||||
|
name="confirmPassword"
|
||||||
|
dependencies={['password']}
|
||||||
|
rules={[
|
||||||
|
{required: true, message: "Подтвердите пароль"},
|
||||||
|
({getFieldValue}) => ({
|
||||||
|
validator(_, value) {
|
||||||
|
if (!value || getFieldValue('password') === value) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('Пароли не совпадают'));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
prefix={<LockOutlined />}
|
||||||
|
placeholder="Подтвердите пароль"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item style={{marginBottom: 8}}>
|
||||||
|
<Button type="primary" htmlType="submit" block size="large">
|
||||||
|
Зарегистрироваться
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item style={{marginBottom: 0, textAlign: "center"}}>
|
||||||
|
<Text type="secondary">
|
||||||
|
Уже есть аккаунт? <a href="/login">Войти</a>
|
||||||
|
</Text>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Col>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RegisterPage;
|
||||||
27
web/src/Components/Pages/RegisterPage/useRegisterPage.js
Normal file
27
web/src/Components/Pages/RegisterPage/useRegisterPage.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { message } from 'antd';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
const useRegisterPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const pageContainerStyle = {
|
||||||
|
width: 450,
|
||||||
|
padding: 40,
|
||||||
|
borderRadius: 12,
|
||||||
|
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.1)",
|
||||||
|
backgroundColor: "white",
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFinish = (values) => {
|
||||||
|
console.log('Регистрация:', values);
|
||||||
|
message.success('Регистрация выполнена успешно!');
|
||||||
|
navigate('/login');
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageContainerStyle,
|
||||||
|
onFinish
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useRegisterPage;
|
||||||
Loading…
x
Reference in New Issue
Block a user