заготовка под управление курсами
This commit is contained in:
parent
bb6537533b
commit
44ceb93729
@ -3,6 +3,7 @@ from typing import Optional, List
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.domain.models import Role
|
||||||
from app.domain.models.users import User
|
from app.domain.models.users import User
|
||||||
|
|
||||||
|
|
||||||
@ -31,6 +32,15 @@ class UsersRepository:
|
|||||||
result = await self.db.execute(query)
|
result = await self.db.execute(query)
|
||||||
return result.scalars().first()
|
return result.scalars().first()
|
||||||
|
|
||||||
|
async def get_by_role_name(self, role_name: str) -> List[User]:
|
||||||
|
query = (
|
||||||
|
select(User)
|
||||||
|
.join(User.role)
|
||||||
|
.filter(Role.title == role_name)
|
||||||
|
)
|
||||||
|
result = await self.db.execute(query)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
async def create(self, user: User) -> User:
|
async def create(self, user: User) -> User:
|
||||||
self.db.add(user)
|
self.db.add(user)
|
||||||
await self.db.commit()
|
await self.db.commit()
|
||||||
|
|||||||
@ -86,3 +86,18 @@ async def create_user(
|
|||||||
):
|
):
|
||||||
register_service = RegisterService(db)
|
register_service = RegisterService(db)
|
||||||
return await register_service.create_user(user)
|
return await register_service.create_user(user)
|
||||||
|
|
||||||
|
|
||||||
|
@users_router.get(
|
||||||
|
'/role/{role_name}/',
|
||||||
|
response_model=List[UserRead],
|
||||||
|
summary='Return all users with given role',
|
||||||
|
description='Return all users with given role',
|
||||||
|
)
|
||||||
|
async def get_users_by_role_name(
|
||||||
|
role_name: str,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: User = Depends(require_auth_user),
|
||||||
|
):
|
||||||
|
users_service = UsersService(db)
|
||||||
|
return await users_service.get_by_role_name(role_name)
|
||||||
|
|||||||
@ -95,3 +95,11 @@ class UsersService:
|
|||||||
user_model = await self.users_repository.update(user_model)
|
user_model = await self.users_repository.update(user_model)
|
||||||
|
|
||||||
return UserRead.model_validate(user_model)
|
return UserRead.model_validate(user_model)
|
||||||
|
|
||||||
|
async def get_by_role_name(self, role_name: str) -> List[UserRead]:
|
||||||
|
users = await self.users_repository.get_by_role_name(role_name)
|
||||||
|
response = []
|
||||||
|
for user in users:
|
||||||
|
response.append(UserRead.model_validate(user))
|
||||||
|
|
||||||
|
return response
|
||||||
|
|||||||
74
web/src/Api/coursesApi.js
Normal file
74
web/src/Api/coursesApi.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import CONFIG from "../Core/сonfig.js";
|
||||||
|
import {baseQueryWithAuth} from "./baseQuery.js";
|
||||||
|
import {createApi} from "@reduxjs/toolkit/query/react";
|
||||||
|
|
||||||
|
|
||||||
|
export const coursesApi = createApi({
|
||||||
|
reducerPath: 'coursesApi',
|
||||||
|
baseQuery: baseQueryWithAuth,
|
||||||
|
endpoints: (builder) => ({
|
||||||
|
getAllCourses: builder.query({
|
||||||
|
query: () => ({
|
||||||
|
url: "/courses/",
|
||||||
|
method: "GET",
|
||||||
|
}),
|
||||||
|
providesTags: ['course'],
|
||||||
|
}),
|
||||||
|
createCourse: builder.mutation({
|
||||||
|
query: (data) => ({
|
||||||
|
url: "/courses/",
|
||||||
|
method: "POST",
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
invalidatesTags: ['course'],
|
||||||
|
}),
|
||||||
|
updateCourse: builder.mutation({
|
||||||
|
query: ({courseId, ...data}) => ({
|
||||||
|
url: `/courses/${courseId}/`,
|
||||||
|
method: "PUT",
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
invalidatesTags: ['course'],
|
||||||
|
}),
|
||||||
|
getCourseTeachers: builder.query({
|
||||||
|
query: (courseId) => ({
|
||||||
|
url: `/courses/${courseId}/teachers/`,
|
||||||
|
method: "GET",
|
||||||
|
}),
|
||||||
|
providesTags: ['teacher'],
|
||||||
|
}),
|
||||||
|
replaceCourseTeachers: builder.mutation({
|
||||||
|
query: ({courseId, ...data}) => ({
|
||||||
|
url: `/courses/${courseId}/teachers/`,
|
||||||
|
method: "PUT",
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
invalidatesTags: ['teacher'],
|
||||||
|
}),
|
||||||
|
getCourseStudents: builder.query({
|
||||||
|
query: (courseId) => ({
|
||||||
|
url: `/courses/${courseId}/students/`,
|
||||||
|
method: "GET",
|
||||||
|
}),
|
||||||
|
providesTags: ['student'],
|
||||||
|
}),
|
||||||
|
replaceCourseStudents: builder.mutation({
|
||||||
|
query: ({courseId, ...data}) => ({
|
||||||
|
url: `/courses/${courseId}/students/`,
|
||||||
|
method: "PUT",
|
||||||
|
body: data,
|
||||||
|
}),
|
||||||
|
invalidatesTags: ['student'],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
useGetAllCoursesQuery,
|
||||||
|
useCreateCourseMutation,
|
||||||
|
useUpdateCourseMutation,
|
||||||
|
useGetCourseTeachersQuery,
|
||||||
|
useReplaceCourseTeachersMutation,
|
||||||
|
useGetCourseStudentsQuery,
|
||||||
|
useReplaceCourseStudentsMutation,
|
||||||
|
} = coursesApi;
|
||||||
@ -39,6 +39,13 @@ export const usersApi = createApi({
|
|||||||
}),
|
}),
|
||||||
invalidatesTags: ["user"],
|
invalidatesTags: ["user"],
|
||||||
}),
|
}),
|
||||||
|
getUsersByRoleName: builder.query({
|
||||||
|
query: (roleName) => ({
|
||||||
|
url: `/users/role/${roleName}/`,
|
||||||
|
method: "GET",
|
||||||
|
}),
|
||||||
|
providesTags: ["user"],
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -48,4 +55,5 @@ export const {
|
|||||||
useUpdateUserMutation,
|
useUpdateUserMutation,
|
||||||
useUpdateUserPasswordMutation,
|
useUpdateUserPasswordMutation,
|
||||||
useCreateUserMutation,
|
useCreateUserMutation,
|
||||||
|
useGetUsersByRoleNameQuery,
|
||||||
} = usersApi;
|
} = usersApi;
|
||||||
@ -2,8 +2,8 @@ import {Button, Col, Flex, FloatButton, Input, Result, Row, Table, Tooltip, Typo
|
|||||||
import {ControlOutlined, PlusOutlined} from "@ant-design/icons";
|
import {ControlOutlined, PlusOutlined} from "@ant-design/icons";
|
||||||
import useAdminPage from "./useAdminPage.js";
|
import useAdminPage from "./useAdminPage.js";
|
||||||
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||||
import CreateUserModalForm from "./CreateUserModalForm/CreateUserModalForm.jsx";
|
import CreateUserModalForm from "./Components/CreateUserModalForm/CreateUserModalForm.jsx";
|
||||||
import UpdateUserModalForm from "./UpdateUserModalForm/UpdateUserModalForm.jsx";
|
import UpdateUserModalForm from "./Components/UpdateUserModalForm/UpdateUserModalForm.jsx";
|
||||||
|
|
||||||
const {Title} = Typography;
|
const {Title} = Typography;
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import {useDispatch, useSelector} from "react-redux";
|
import {useDispatch, useSelector} from "react-redux";
|
||||||
import {Form, notification} from "antd";
|
import {Form, notification} from "antd";
|
||||||
import {setOpenModalCreateUser, setSelectedUserToUpdate} from "../../../../Redux/Slices/usersSlice.js";
|
import {setOpenModalCreateUser, setSelectedUserToUpdate} from "../../../../../Redux/Slices/usersSlice.js";
|
||||||
import {useGetAllRolesQuery} from "../../../../Api/rolesApi.js";
|
import {useGetAllRolesQuery} from "../../../../../Api/rolesApi.js";
|
||||||
import {useCreateUserMutation} from "../../../../Api/usersApi.js";
|
import {useCreateUserMutation} from "../../../../../Api/usersApi.js";
|
||||||
|
|
||||||
|
|
||||||
const useCreateUserModalForm = () => {
|
const useCreateUserModalForm = () => {
|
||||||
@ -23,7 +23,7 @@ import {
|
|||||||
TagOutlined,
|
TagOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import LoadingIndicator from "../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
import LoadingIndicator from "../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||||
import useUpdateUserModalForm from "./useUpdateUserModalForm.js";
|
import useUpdateUserModalForm from "./useUpdateUserModalForm.js";
|
||||||
|
|
||||||
const {Title, Text} = Typography;
|
const {Title, Text} = Typography;
|
||||||
@ -1,11 +1,11 @@
|
|||||||
import {useDispatch, useSelector} from "react-redux";
|
import {useDispatch, useSelector} from "react-redux";
|
||||||
import {Form, notification} from "antd";
|
import {Form, notification} from "antd";
|
||||||
import {setSelectedUserToUpdate} from "../../../../Redux/Slices/usersSlice.js";
|
import {setSelectedUserToUpdate} from "../../../../../Redux/Slices/usersSlice.js";
|
||||||
import {useGetAllRolesQuery} from "../../../../Api/rolesApi.js";
|
import {useGetAllRolesQuery} from "../../../../../Api/rolesApi.js";
|
||||||
import {useEffect} from "react";
|
import {useEffect} from "react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import {useUpdateUserMutation, useUpdateUserPasswordMutation} from "../../../../Api/usersApi.js";
|
import {useUpdateUserMutation, useUpdateUserPasswordMutation} from "../../../../../Api/usersApi.js";
|
||||||
import {useGetStatusesQuery} from "../../../../Api/statusesApi.js";
|
import {useGetStatusesQuery} from "../../../../../Api/statusesApi.js";
|
||||||
|
|
||||||
|
|
||||||
const useUpdateUserModalForm = () => {
|
const useUpdateUserModalForm = () => {
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
import useCreateCourseModalForm from "./useCreateCourseModalForm.js";
|
||||||
|
import {Button, Col, Form, Input, Modal, Result, Row, Select} from "antd";
|
||||||
|
import TextArea from "antd/es/input/TextArea.js";
|
||||||
|
import LoadingIndicator from "../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||||
|
|
||||||
|
|
||||||
|
const CreateCourseModal = () => {
|
||||||
|
const {
|
||||||
|
openCreateCourseModal,
|
||||||
|
handleCancel,
|
||||||
|
form,
|
||||||
|
isLoadinging,
|
||||||
|
isError,
|
||||||
|
teachers,
|
||||||
|
students
|
||||||
|
} = useCreateCourseModalForm();
|
||||||
|
|
||||||
|
if (isLoadinging) {
|
||||||
|
return (
|
||||||
|
<LoadingIndicator/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<Result status="500" title="500" subTitle="Произошла ошибка при загрузке данных пользователя"/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={"Создание курса"}
|
||||||
|
open={openCreateCourseModal}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
footer={[
|
||||||
|
<Button key="cancel" onClick={handleCancel}>
|
||||||
|
Отмена
|
||||||
|
</Button>,
|
||||||
|
<Button
|
||||||
|
key="submit"
|
||||||
|
type="primary"
|
||||||
|
loading={isLoadinging}
|
||||||
|
// onClick={handleOk}
|
||||||
|
>
|
||||||
|
Создать
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
width={800}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
name="title"
|
||||||
|
label="Название курса"
|
||||||
|
rules={[{required: true, message: "Введите название"}]}
|
||||||
|
>
|
||||||
|
<Input size="large" placeholder="Введение в Python"/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="description" label="Описание">
|
||||||
|
<TextArea rows={4} placeholder="Курс для начинающих..."/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item name="teacher_ids" label="Преподаватели">
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
placeholder="Выберите преподавателей"
|
||||||
|
loading={isLoadinging}
|
||||||
|
>
|
||||||
|
{teachers.map((teacher) => (
|
||||||
|
<Select.Option key={teacher.id}
|
||||||
|
value={teacher.id}>{teacher.last_name} {teacher.first_name} - {teacher.login}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item name="student_ids" label="Студенты">
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
placeholder="Выберите студентов"
|
||||||
|
loading={isLoadinging}
|
||||||
|
>
|
||||||
|
{students.map((student) => (
|
||||||
|
<Select.Option key={student.id}
|
||||||
|
value={student.id}>{student.last_name} {student.first_name} - {student.login}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateCourseModal;
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
import {useDispatch, useSelector} from "react-redux";
|
||||||
|
import {setOpenCreateCourseModal} from "../../../../../Redux/Slices/coursesSlice.js";
|
||||||
|
import {Form} from "antd";
|
||||||
|
import {useGetUsersByRoleNameQuery} from "../../../../../Api/usersApi.js";
|
||||||
|
import {
|
||||||
|
useCreateCourseMutation,
|
||||||
|
useReplaceCourseStudentsMutation,
|
||||||
|
useReplaceCourseTeachersMutation
|
||||||
|
} from "../../../../../Api/coursesApi.js";
|
||||||
|
|
||||||
|
|
||||||
|
const useCreateCourseModalForm = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: teachers = [],
|
||||||
|
isLoading: isTeachersLoading,
|
||||||
|
isError: isTeachersError,
|
||||||
|
} = useGetUsersByRoleNameQuery("teacher", {
|
||||||
|
pollingInterval: 20000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: students = [],
|
||||||
|
isLoading: isStudentsLoading,
|
||||||
|
isError: isStudentsError,
|
||||||
|
} = useGetUsersByRoleNameQuery("student", {
|
||||||
|
pollingInterval: 20000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [createCourse, {isLoading: creatingCourse}] = useCreateCourseMutation();
|
||||||
|
const [replaceTeachers, {isLoading: replacingTeachers}] = useReplaceCourseTeachersMutation();
|
||||||
|
const [replaceStudents, {isLoading: replacingStudents}] = useReplaceCourseStudentsMutation();
|
||||||
|
|
||||||
|
const isLoading = isTeachersLoading || isStudentsLoading || creatingCourse || replacingTeachers || replacingStudents;
|
||||||
|
const isError = isTeachersError || isStudentsError;
|
||||||
|
|
||||||
|
const {
|
||||||
|
openCreateCourseModal
|
||||||
|
} = useSelector((state) => state.courses);
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
form.resetFields();
|
||||||
|
dispatch(setOpenCreateCourseModal(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
openCreateCourseModal,
|
||||||
|
handleCancel,
|
||||||
|
handleOk,
|
||||||
|
form,
|
||||||
|
teachers,
|
||||||
|
students,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCreateCourseModalForm;
|
||||||
@ -1,10 +1,152 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Col,
|
||||||
|
Empty,
|
||||||
|
Row,
|
||||||
|
Space,
|
||||||
|
Spin,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
Avatar, Result, FloatButton, Tooltip,
|
||||||
|
} from "antd";
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
TeamOutlined, BookOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import useCoursesPage from "./useCoursesPage.js";
|
||||||
|
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||||
|
import CreateCourseModalForm from "./Components/CreateCourseModalForm/CreateCourseModalForm.jsx";
|
||||||
|
|
||||||
|
const {Title, Text} = Typography;
|
||||||
|
|
||||||
const CoursesPage = () => {
|
const CoursesPage = () => {
|
||||||
|
const {
|
||||||
|
courses,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
isAdmin,
|
||||||
|
isTeacher,
|
||||||
|
openCreateModal,
|
||||||
|
openEditModal,
|
||||||
|
} = useCoursesPage();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<LoadingIndicator/>
|
||||||
CoursesPage
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return <Result status="500" title="500" subTitle="Произошла ошибка при загрузке курсов"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{minHeight: "100vh"}}>
|
||||||
|
<Row justify="space-between" align="middle" style={{marginBottom: 24}}>
|
||||||
|
<Col>
|
||||||
|
<Title level={2} style={{margin: 0}}>
|
||||||
|
<BookOutlined/> Курсы
|
||||||
|
</Title>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{courses.length === 0 ? (
|
||||||
|
<Empty
|
||||||
|
description="Курсов пока нет"
|
||||||
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||||
|
>
|
||||||
|
{(isAdmin || isTeacher) && (
|
||||||
|
<Button type="primary" onClick={openCreateModal}>
|
||||||
|
Создать первый курс
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Empty>
|
||||||
|
) : (
|
||||||
|
<Row gutter={[24, 24]}>
|
||||||
|
{courses.map((course) => (
|
||||||
|
<Col xs={24} sm={12} lg={8} xl={6} key={course.id}>
|
||||||
|
<Card
|
||||||
|
hoverable
|
||||||
|
cover={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: 160,
|
||||||
|
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: "white",
|
||||||
|
fontSize: 48,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{course.title[0].toUpperCase()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
isAdmin || isTeacher
|
||||||
|
? [
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
onClick={() => openEditModal(course)}
|
||||||
|
>
|
||||||
|
Редактировать
|
||||||
|
</Button>,
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Card.Meta
|
||||||
|
title={<Title level={4}>{course.title}</Title>}
|
||||||
|
description={
|
||||||
|
course.description || <Text type="secondary">Без описания</Text>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Space direction="vertical" style={{width: "100%", marginTop: 16}}>
|
||||||
|
<Space>
|
||||||
|
<TeamOutlined/>
|
||||||
|
<Text>
|
||||||
|
{course.teachers?.length || 0} учител
|
||||||
|
{course.teachers?.length === 1 ? "ь" : "ей"}
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<UserOutlined/>
|
||||||
|
<Text>
|
||||||
|
{course.enrollments?.length || 0} студент
|
||||||
|
{course.enrollments?.length === 1 ? "" : "ов"}
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
{course.teachers?.length > 0 && (
|
||||||
|
<div style={{marginTop: 16}}>
|
||||||
|
<Text type="secondary">Преподаватели:</Text>
|
||||||
|
<Avatar.Group maxCount={3} style={{marginTop: 8}}>
|
||||||
|
{course.teachers.map((t) => (
|
||||||
|
<Avatar key={t.teacher_id} style={{backgroundColor: "#1890ff"}}>
|
||||||
|
{t.teacher?.first_name?.[0] || "У"}
|
||||||
|
</Avatar>
|
||||||
|
))}
|
||||||
|
</Avatar.Group>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Tooltip title="Создать курс">
|
||||||
|
<FloatButton
|
||||||
|
icon={<PlusOutlined/>}
|
||||||
|
onClick={openCreateModal}
|
||||||
|
type="primary"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<CreateCourseModalForm/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
import {useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js";
|
||||||
|
import {useGetAllCoursesQuery} from "../../../Api/coursesApi.js";
|
||||||
|
import CONFIG from "../../../Core/сonfig.js";
|
||||||
|
import {ROLES} from "../../../Core/constants.js";
|
||||||
|
import {useDispatch} from "react-redux";
|
||||||
|
import {setOpenCreateCourseModal} from "../../../Redux/Slices/coursesSlice.js";
|
||||||
|
|
||||||
|
|
||||||
|
const useCoursesPage = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const {data: userData, isLoading: isUserLoading} = useGetAuthenticatedUserDataQuery();
|
||||||
|
const {data: courses = [], isLoading, isCoursesLoading, isError} = useGetAllCoursesQuery();
|
||||||
|
|
||||||
|
const isAdmin = userData?.role?.title === CONFIG.ROOT_ROLE_NAME;
|
||||||
|
const isTeacher = [CONFIG.ROOT_ROLE_NAME, ROLES.TEACHER].includes(userData?.role?.title);
|
||||||
|
|
||||||
|
const openCreateModal = () => {
|
||||||
|
dispatch(setOpenCreateCourseModal(true));
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeCreateModal = () => {
|
||||||
|
dispatch(setOpenCreateCourseModal(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
courses,
|
||||||
|
isLoading: isCoursesLoading || isUserLoading || isLoading,
|
||||||
|
isError,
|
||||||
|
isAdmin,
|
||||||
|
isTeacher,
|
||||||
|
openCreateModal,
|
||||||
|
closeModal: closeCreateModal,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCoursesPage;
|
||||||
5
web/src/Core/constants.js
Normal file
5
web/src/Core/constants.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const ROLES = {
|
||||||
|
ADMIN: "root",
|
||||||
|
TEACHER: "teacher",
|
||||||
|
STUDENT: "student",
|
||||||
|
}
|
||||||
23
web/src/Redux/Slices/coursesSlice.js
Normal file
23
web/src/Redux/Slices/coursesSlice.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import {createSlice} from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
selectedCourseToUpdate: null,
|
||||||
|
openCreateCourseModal: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const coursesSlice = createSlice({
|
||||||
|
name: "courses",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setSelectedCourseToUpdate(state, action) {
|
||||||
|
state.selectedCourseToUpdate = action.payload;
|
||||||
|
},
|
||||||
|
setOpenCreateCourseModal(state, action) {
|
||||||
|
state.openCreateCourseModal = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {setSelectedCourseToUpdate, setOpenCreateCourseModal} = coursesSlice.actions;
|
||||||
|
export default coursesSlice.reducer;
|
||||||
@ -1,10 +1,12 @@
|
|||||||
import {configureStore} from "@reduxjs/toolkit";
|
import {configureStore} from "@reduxjs/toolkit";
|
||||||
import authReducer from "./Slices/authSlice.js";
|
import authReducer from "./Slices/authSlice.js";
|
||||||
import usersReducer from "./Slices/usersSlice.js";
|
import usersReducer from "./Slices/usersSlice.js";
|
||||||
|
import coursesReducer from "./Slices/coursesSlice.js";
|
||||||
import {authApi} from "../Api/authApi.js";
|
import {authApi} from "../Api/authApi.js";
|
||||||
import {usersApi} from "../Api/usersApi.js";
|
import {usersApi} from "../Api/usersApi.js";
|
||||||
import {rolesApi} from "../Api/rolesApi.js";
|
import {rolesApi} from "../Api/rolesApi.js";
|
||||||
import {statusesApi} from "../Api/statusesApi.js";
|
import {statusesApi} from "../Api/statusesApi.js";
|
||||||
|
import {coursesApi} from "../Api/coursesApi.js";
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
@ -16,7 +18,10 @@ export const store = configureStore({
|
|||||||
|
|
||||||
[rolesApi.reducerPath]: rolesApi.reducer,
|
[rolesApi.reducerPath]: rolesApi.reducer,
|
||||||
|
|
||||||
[statusesApi.reducerPath]: statusesApi.reducer
|
[statusesApi.reducerPath]: statusesApi.reducer,
|
||||||
|
|
||||||
|
courses: coursesReducer,
|
||||||
|
[coursesApi.reducerPath]: coursesApi.reducer
|
||||||
},
|
},
|
||||||
middleware: (getDefaultMiddleware) => (
|
middleware: (getDefaultMiddleware) => (
|
||||||
getDefaultMiddleware().concat(
|
getDefaultMiddleware().concat(
|
||||||
@ -24,6 +29,7 @@ export const store = configureStore({
|
|||||||
usersApi.middleware,
|
usersApi.middleware,
|
||||||
rolesApi.middleware,
|
rolesApi.middleware,
|
||||||
statusesApi.middleware,
|
statusesApi.middleware,
|
||||||
|
coursesApi.middleware
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user