добавил в апи управление курсами, студентами курсов и учителями курсов
This commit is contained in:
parent
0a8e027b87
commit
bb6537533b
51
api/app/application/course_teachers_repository.py
Normal file
51
api/app/application/course_teachers_repository.py
Normal file
@ -0,0 +1,51 @@
|
||||
from typing import Optional, List, Sequence
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.domain.models import CourseTeacher
|
||||
|
||||
|
||||
class CourseTeachersRepository:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def get_by_course_id(self, course_id: int) -> Sequence[CourseTeacher]:
|
||||
query = (
|
||||
select(CourseTeacher)
|
||||
.filter_by(course_id=course_id)
|
||||
)
|
||||
results = await self.db.execute(query)
|
||||
return results.scalars().all()
|
||||
|
||||
async def get_by_teacher_id(self, teacher_id: int) -> Sequence[CourseTeacher]:
|
||||
query = (
|
||||
select(CourseTeacher)
|
||||
.filter_by(teacher_id=teacher_id)
|
||||
)
|
||||
results = await self.db.execute(query)
|
||||
return results.scalars().all()
|
||||
|
||||
async def get_by_id(self, course_teacher_id: int) -> Optional[CourseTeacher]:
|
||||
query = (
|
||||
select(CourseTeacher)
|
||||
.filter_by(id=course_teacher_id)
|
||||
)
|
||||
results = await self.db.execute(query)
|
||||
return results.scalars().first()
|
||||
|
||||
async def create_list(self, course_teachers: List[CourseTeacher]) -> List[CourseTeacher]:
|
||||
self.db.add_all(course_teachers)
|
||||
await self.db.commit()
|
||||
|
||||
for course_teacher in course_teachers:
|
||||
await self.db.refresh(course_teacher)
|
||||
|
||||
return course_teachers
|
||||
|
||||
async def delete_list(self, course_teachers: List[CourseTeacher] | Sequence[CourseTeacher]) -> List[CourseTeacher]:
|
||||
for course_teacher in course_teachers:
|
||||
await self.db.delete(course_teacher)
|
||||
|
||||
await self.db.commit()
|
||||
return course_teachers
|
||||
40
api/app/application/courses_repository.py
Normal file
40
api/app/application/courses_repository.py
Normal file
@ -0,0 +1,40 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.domain.models import Course
|
||||
|
||||
|
||||
class CoursesRepository:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def get_all(self) -> List[Course]:
|
||||
query = select(Course)
|
||||
result = await self.db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
async def get_by_id(self, course_id: int) -> Optional[Course]:
|
||||
query = (
|
||||
select(Course)
|
||||
.order_by(id=course_id)
|
||||
)
|
||||
result = await self.db.execute(query)
|
||||
return result.scalars().first()
|
||||
|
||||
async def create(self, course: Course) -> Course:
|
||||
self.db.add(course)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(course)
|
||||
return course
|
||||
|
||||
async def update(self, course: Course) -> Course:
|
||||
await self.db.merge(course)
|
||||
await self.db.commit()
|
||||
return course
|
||||
|
||||
async def delete(self, course: Course) -> Course:
|
||||
await self.db.delete(course)
|
||||
await self.db.commit()
|
||||
return course
|
||||
43
api/app/application/enrollments_repository.py
Normal file
43
api/app/application/enrollments_repository.py
Normal file
@ -0,0 +1,43 @@
|
||||
from typing import List, Sequence
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.domain.models import Enrollment
|
||||
|
||||
|
||||
class EnrollmentsRepository:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def get_by_course_id(self, course_id: int) -> Sequence[Enrollment]:
|
||||
query = (
|
||||
select(Enrollment)
|
||||
.filter_by(course_id=course_id)
|
||||
)
|
||||
results = await self.db.execute(query)
|
||||
return results.scalars().all()
|
||||
|
||||
async def get_by_student_id(self, student_id: int) -> Sequence[Enrollment]:
|
||||
query = (
|
||||
select(Enrollment)
|
||||
.filter_by(student_id=student_id)
|
||||
)
|
||||
results = await self.db.execute(query)
|
||||
return results.scalars().all()
|
||||
|
||||
async def create_list(self, enrollments: List[Enrollment]) -> List[Enrollment]:
|
||||
self.db.add_all(enrollments)
|
||||
await self.db.commit()
|
||||
|
||||
for enrollment in enrollments:
|
||||
await self.db.refresh(enrollment)
|
||||
|
||||
return enrollments
|
||||
|
||||
async def delete_list(self, enrollments: List[Enrollment] | Sequence[Enrollment]) -> List[Enrollment]:
|
||||
for enrollment in enrollments:
|
||||
await self.db.delete(enrollment)
|
||||
|
||||
await self.db.commit()
|
||||
return enrollments
|
||||
125
api/app/controllers/courses_router.py
Normal file
125
api/app/controllers/courses_router.py
Normal file
@ -0,0 +1,125 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Response
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
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
|
||||
from app.domain.entities.enrollments import EnrollmentRead, EnrollmentCreate
|
||||
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
|
||||
from app.infrastructure.enrollments_service import EnrollmentsService
|
||||
|
||||
courses_router = APIRouter()
|
||||
|
||||
|
||||
@courses_router.get(
|
||||
'/',
|
||||
response_model=List[CourseRead],
|
||||
summary='Return all courses',
|
||||
description='Return all courses',
|
||||
)
|
||||
async def get_all_courses(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_auth_user),
|
||||
):
|
||||
courses_service = CoursesService(db)
|
||||
return await courses_service.get_all()
|
||||
|
||||
|
||||
@courses_router.post(
|
||||
'/',
|
||||
response_model=Optional[CourseRead],
|
||||
summary='Create a new course',
|
||||
description='Create a new course',
|
||||
)
|
||||
async def create_new_course(
|
||||
course: CourseCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_teacher),
|
||||
):
|
||||
courses_service = CoursesService(db)
|
||||
return await courses_service.create(course)
|
||||
|
||||
|
||||
@courses_router.put(
|
||||
'/{course_id}/',
|
||||
response_model=Optional[CourseRead],
|
||||
summary='Update a course',
|
||||
description='Update a course',
|
||||
)
|
||||
async def update_course(
|
||||
course_id: int,
|
||||
course: CourseUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_teacher),
|
||||
):
|
||||
courses_service = CoursesService(db)
|
||||
return await courses_service.update(course_id, course)
|
||||
|
||||
|
||||
@courses_router.get(
|
||||
'/{course_id}/teachers/',
|
||||
response_model=List[CourseTeacherRead],
|
||||
summary='Return all teachers',
|
||||
description='Return all teachers',
|
||||
)
|
||||
async def get_course_teachers(
|
||||
course_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_auth_user),
|
||||
):
|
||||
service = CourseTeachersService(db)
|
||||
teachers = await service.get_course_teachers_by_course_id(course_id)
|
||||
return teachers
|
||||
|
||||
|
||||
@courses_router.put(
|
||||
'/{course_id}/teachers/',
|
||||
response_model=List[CourseTeacherRead],
|
||||
summary='Replace all teachers in a course',
|
||||
description='Replace all teachers in a course',
|
||||
)
|
||||
async def replace_course_teachers(
|
||||
course_id: int,
|
||||
teachers: List[CourseTeacherCreate],
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_teacher),
|
||||
):
|
||||
service = CourseTeachersService(db)
|
||||
return await service.replace_course_teachers_list(teachers, course_id)
|
||||
|
||||
|
||||
@courses_router.get(
|
||||
'/{course_id}/students/',
|
||||
response_model=List[EnrollmentRead],
|
||||
summary='Return all students of the course',
|
||||
description='Return all students enrolled in the course',
|
||||
)
|
||||
async def get_course_students(
|
||||
course_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_auth_user),
|
||||
):
|
||||
service = EnrollmentsService(db)
|
||||
students = await service.get_course_students_by_course_id(course_id)
|
||||
return students
|
||||
|
||||
|
||||
@courses_router.put(
|
||||
'/{course_id}/students/',
|
||||
response_model=List[EnrollmentRead],
|
||||
summary='Replace all students in a course',
|
||||
description='Completely replace the list of enrolled students',
|
||||
)
|
||||
async def replace_course_students(
|
||||
course_id: int,
|
||||
students: List[EnrollmentCreate],
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(require_teacher),
|
||||
):
|
||||
service = EnrollmentsService(db)
|
||||
return await service.replace_course_students_list(students, course_id)
|
||||
15
api/app/domain/entities/course_teachers.py
Normal file
15
api/app/domain/entities/course_teachers.py
Normal file
@ -0,0 +1,15 @@
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
|
||||
|
||||
class CourseTeacherCreate(BaseModel):
|
||||
course_id: int = Field()
|
||||
teacher_id: int = Field()
|
||||
|
||||
|
||||
class CourseTeacherRead(BaseModel):
|
||||
id: int
|
||||
course_id: int
|
||||
teacher_id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
27
api/app/domain/entities/courses.py
Normal file
27
api/app/domain/entities/courses.py
Normal file
@ -0,0 +1,27 @@
|
||||
from typing import Optional, List
|
||||
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
|
||||
from app.domain.entities.course_teachers import CourseTeacherRead
|
||||
from app.domain.entities.enrollments import EnrollmentRead
|
||||
|
||||
|
||||
class CourseCreate(BaseModel):
|
||||
title: str = Field(max_length=250)
|
||||
description: Optional[str] = Field(default=None, max_length=1000)
|
||||
|
||||
|
||||
class CourseUpdate(CourseCreate):
|
||||
pass
|
||||
|
||||
|
||||
class CourseRead(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
description: str
|
||||
|
||||
teachers: List[CourseTeacherRead]
|
||||
enrollments: List[EnrollmentRead]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
18
api/app/domain/entities/enrollments.py
Normal file
18
api/app/domain/entities/enrollments.py
Normal file
@ -0,0 +1,18 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
|
||||
|
||||
class EnrollmentCreate(BaseModel):
|
||||
course_id: int = Field()
|
||||
student_id: int = Field()
|
||||
|
||||
|
||||
class EnrollmentRead(BaseModel):
|
||||
id: int
|
||||
course_id: int
|
||||
student_id: int
|
||||
enrollment_date: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@ -12,7 +12,7 @@ class Course(PhotoAbstract):
|
||||
title: Mapped[str] = mapped_column(String(250), nullable=False)
|
||||
description: Mapped[str] = mapped_column(String(1000), nullable=True)
|
||||
|
||||
teachers: Mapped[List['CourseTeacher']] = relationship('CourseTeacher', back_populates='course')
|
||||
enrollments: Mapped[List['Enrollment']] = relationship('Enrollment', back_populates='course')
|
||||
teachers: Mapped[List['CourseTeacher']] = relationship('CourseTeacher', back_populates='course', lazy='select')
|
||||
enrollments: Mapped[List['Enrollment']] = relationship('Enrollment', back_populates='course', lazy='select')
|
||||
lessons: Mapped[List['Lesson']] = relationship('Lesson', back_populates='course')
|
||||
tasks: Mapped[List['Task']] = relationship('Task', back_populates='course')
|
||||
|
||||
65
api/app/infrastructure/course_teachers_service.py
Normal file
65
api/app/infrastructure/course_teachers_service.py
Normal file
@ -0,0 +1,65 @@
|
||||
from typing import Optional, List
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.application.course_teachers_repository import CourseTeachersRepository
|
||||
from app.application.courses_repository import CoursesRepository
|
||||
from app.application.users_repository import UsersRepository
|
||||
from app.domain.entities.course_teachers import CourseTeacherRead, CourseTeacherCreate
|
||||
from app.domain.models import CourseTeacher
|
||||
|
||||
|
||||
class CourseTeachersService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.course_teachers_repository = CourseTeachersRepository(db)
|
||||
self.courses_repository = CoursesRepository(db)
|
||||
self.users_repository = UsersRepository(db)
|
||||
|
||||
async def get_course_teachers_by_course_id(self, course_id: int) -> Optional[List[CourseTeacherRead]]:
|
||||
course = await self.courses_repository.get_by_id(course_id)
|
||||
|
||||
if course is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Курс с таким ID не найден')
|
||||
|
||||
course_teachers = await self.course_teachers_repository.get_by_course_id(course.id)
|
||||
|
||||
response = []
|
||||
for course_teacher in course_teachers:
|
||||
response.append(
|
||||
CourseTeacherRead.model_validate(course_teacher)
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
async def replace_course_teachers_list(
|
||||
self, course_teachers: List[CourseTeacherCreate], course_id: int
|
||||
) -> Optional[List[CourseTeacherRead]]:
|
||||
course = await self.courses_repository.get_by_id(course_id)
|
||||
|
||||
if course is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Курс с таким ID не найден')
|
||||
|
||||
old_course_teachers = await self.course_teachers_repository.get_by_course_id(course.id)
|
||||
await self.course_teachers_repository.delete_list(old_course_teachers)
|
||||
|
||||
course_teachers_models = []
|
||||
for course_teacher in course_teachers:
|
||||
teacher = await self.users_repository.get_by_id(course_teacher.teacher_id)
|
||||
if teacher is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Пользователь с таким ID не найден')
|
||||
|
||||
course_teachers_models.append(CourseTeacher(
|
||||
course_id=course_id,
|
||||
teacher_id=course_teacher.teacher_id,
|
||||
))
|
||||
|
||||
course_teachers_models = await self.course_teachers_repository.create_list(course_teachers_models)
|
||||
|
||||
response = []
|
||||
for course_teacher in course_teachers_models:
|
||||
response.append(
|
||||
CourseTeacherRead.model_validate(course_teacher)
|
||||
)
|
||||
|
||||
return response
|
||||
46
api/app/infrastructure/courses_service.py
Normal file
46
api/app/infrastructure/courses_service.py
Normal file
@ -0,0 +1,46 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.application.courses_repository import CoursesRepository
|
||||
from app.domain.entities.courses import CourseRead, CourseCreate
|
||||
from app.domain.models import Course
|
||||
|
||||
|
||||
class CoursesService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.courses_repository = CoursesRepository(db)
|
||||
|
||||
async def get_all(self) -> List[CourseRead]:
|
||||
courses = await self.courses_repository.get_all()
|
||||
response = []
|
||||
for course in courses:
|
||||
response.append(
|
||||
CourseRead.model_validate(course)
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
async def create(self, course: CourseCreate) -> Optional[CourseRead]:
|
||||
course_model = Course(
|
||||
title=course.title,
|
||||
description=course.description,
|
||||
)
|
||||
|
||||
course_model = await self.courses_repository.create(course_model)
|
||||
|
||||
return CourseRead.model_validate(course_model)
|
||||
|
||||
async def update(self, course_id: int, course: CourseCreate) -> Optional[CourseRead]:
|
||||
course_model = await self.courses_repository.get_by_id(course_id)
|
||||
|
||||
if course_model is None:
|
||||
raise HTTPException(status_code=404, detail='Курс с таким ID не найден')
|
||||
|
||||
course_model.title = course.title
|
||||
course_model.description = course.description
|
||||
|
||||
course_model = await self.courses_repository.update(course_model)
|
||||
|
||||
return CourseRead.model_validate(course_model)
|
||||
@ -5,7 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette import status
|
||||
|
||||
from app.application.users_repository import UsersRepository
|
||||
from app.core.constants import UserStatuses
|
||||
from app.core.constants import UserStatuses, UserRoles
|
||||
from app.database.session import get_db
|
||||
from app.domain.models.users import User
|
||||
from app.settings import get_auth_data, Settings
|
||||
@ -45,3 +45,10 @@ def require_admin(user: User = Depends(require_auth_user)):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Ошибка доступа')
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def require_teacher(user: User = Depends(require_auth_user)):
|
||||
if user.role.title not in [UserRoles.TEACHER, Settings().root_role_name]:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Ошибка доступа')
|
||||
|
||||
return user
|
||||
|
||||
66
api/app/infrastructure/enrollments_service.py
Normal file
66
api/app/infrastructure/enrollments_service.py
Normal file
@ -0,0 +1,66 @@
|
||||
from typing import Optional, List
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.application.enrollments_repository import EnrollmentsRepository
|
||||
from app.application.courses_repository import CoursesRepository
|
||||
from app.application.users_repository import UsersRepository
|
||||
from app.domain.entities.enrollments import EnrollmentRead, EnrollmentCreate
|
||||
from app.domain.models import Enrollment
|
||||
|
||||
|
||||
class EnrollmentsService:
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.enrollments_repository = EnrollmentsRepository(db)
|
||||
self.courses_repository = CoursesRepository(db)
|
||||
self.users_repository = UsersRepository(db)
|
||||
|
||||
async def get_course_students_by_course_id(self, course_id: int) -> Optional[List[EnrollmentRead]]:
|
||||
course = await self.courses_repository.get_by_id(course_id)
|
||||
|
||||
if course is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Курс с таким ID не найден')
|
||||
|
||||
enrollments = await self.enrollments_repository.get_by_course_id(course.id)
|
||||
|
||||
response = []
|
||||
for enrollment in enrollments:
|
||||
response.append(
|
||||
EnrollmentRead.model_validate(enrollment)
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
async def replace_course_students_list(
|
||||
self, enrollments: List[EnrollmentCreate], course_id: int
|
||||
) -> Optional[List[EnrollmentRead]]:
|
||||
course = await self.courses_repository.get_by_id(course_id)
|
||||
|
||||
if course is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Курс с таким ID не найден')
|
||||
|
||||
old_enrollments = await self.enrollments_repository.get_by_course_id(course.id)
|
||||
await self.enrollments_repository.delete_list(old_enrollments)
|
||||
|
||||
enrollments_models = []
|
||||
for enrollment in enrollments:
|
||||
student = await self.users_repository.get_by_id(enrollment.student_id)
|
||||
if student is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Пользователь с таким ID не найден')
|
||||
|
||||
enrollments_models.append(Enrollment(
|
||||
course_id=course_id,
|
||||
student_id=enrollment.student_id,
|
||||
enrollment_date=enrollment.enrollment_date,
|
||||
))
|
||||
|
||||
enrollments_models = await self.enrollments_repository.create_list(enrollments_models)
|
||||
|
||||
response = []
|
||||
for enrollment in enrollments_models:
|
||||
response.append(
|
||||
EnrollmentRead.model_validate(enrollment)
|
||||
)
|
||||
|
||||
return response
|
||||
@ -2,6 +2,7 @@ from fastapi import FastAPI
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.controllers.auth_router import auth_router
|
||||
from app.controllers.courses_router import courses_router
|
||||
from app.controllers.register_router import register_router
|
||||
from app.controllers.roles_router import roles_router
|
||||
from app.controllers.statuses_router import statuses_router
|
||||
@ -22,6 +23,7 @@ def start_app():
|
||||
)
|
||||
|
||||
api_app.include_router(auth_router, prefix=f'{settings.prefix}/auth', tags=['auth'])
|
||||
api_app.include_router(courses_router, prefix=f'{settings.prefix}/courses', tags=['courses'])
|
||||
api_app.include_router(register_router, prefix=f'{settings.prefix}/register', tags=['register'])
|
||||
api_app.include_router(roles_router, prefix=f'{settings.prefix}/roles', tags=['roles'])
|
||||
api_app.include_router(statuses_router, prefix=f'{settings.prefix}/statuses', tags=['statuses'])
|
||||
|
||||
@ -1,9 +1,32 @@
|
||||
import {Button, DatePicker, Form, Input, Modal, Result, Select, Tooltip, Typography} from "antd";
|
||||
import useUpdateUserModalForm from "./useUpdateUserModalForm.js";
|
||||
import {CalendarOutlined, InfoCircleOutlined} from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
DatePicker,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Typography,
|
||||
Avatar,
|
||||
Divider, Result,
|
||||
} from "antd";
|
||||
import {
|
||||
UserOutlined,
|
||||
MailOutlined,
|
||||
LockOutlined,
|
||||
CalendarOutlined,
|
||||
SaveOutlined,
|
||||
CrownOutlined,
|
||||
TagOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import dayjs from "dayjs";
|
||||
import LoadingIndicator from "../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
import useUpdateUserModalForm from "./useUpdateUserModalForm.js";
|
||||
|
||||
const {Title, Text} = Typography;
|
||||
|
||||
const UpdateUserModalForm = () => {
|
||||
const {
|
||||
@ -13,135 +36,237 @@ const UpdateUserModalForm = () => {
|
||||
userForm,
|
||||
passwordForm,
|
||||
roles,
|
||||
statusesData,
|
||||
isLoading,
|
||||
isError,
|
||||
isLoadingUpdate,
|
||||
isErrorUpdate,
|
||||
handlePasswordFinish,
|
||||
statusesData,
|
||||
} = useUpdateUserModalForm();
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingIndicator/>
|
||||
}
|
||||
|
||||
if (isLoading) return <LoadingIndicator/>;
|
||||
if (isError) {
|
||||
return <Result status="500" title="500" subTitle="Произошла ошибка при загрузке данных пользователя"/>
|
||||
return (
|
||||
<Result status="500" title="500" subTitle="Ошибка загрузки данных пользователя"/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Изменить пользователя"
|
||||
open={modalVisible}
|
||||
onCancel={handleCancel}
|
||||
footer={null}
|
||||
width={720}
|
||||
title={
|
||||
<Space>
|
||||
<Avatar size={36} icon={<UserOutlined/>} style={{backgroundColor: "#1890ff"}}/>
|
||||
<div>
|
||||
<Title level={4} style={{margin: 0}}>
|
||||
Редактирование пользователя
|
||||
</Title>
|
||||
<Text type="secondary">Изменение профиля и прав доступа</Text>
|
||||
</div>
|
||||
</Space>
|
||||
}
|
||||
closeIcon={null}
|
||||
>
|
||||
<Form
|
||||
form={userForm}
|
||||
onFinish={handleFinish}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item label="Фамилия" name="last_name" rules={[{required: true, message: "Введите фамилию"}]}>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Имя" name="first_name" rules={[{required: true, message: "Введите имя"}]}>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Отчество" name="patronymic">
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Логин" name="login" rules={[{required: true, message: "Введите логин"}]}>
|
||||
<Input disabled/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="email"
|
||||
label="Email"
|
||||
rules={[{required: true, message: "Введите email", type: "email"}]}>
|
||||
<Input/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="birthdate"
|
||||
label="Дата рождения"
|
||||
rules={[{required: true, message: "Введите дату рождения"}]}
|
||||
>
|
||||
<DatePicker
|
||||
suffixIcon={<CalendarOutlined/>}
|
||||
format="DD.MM.YYYY"
|
||||
style={{width: "100%"}}
|
||||
size="large"
|
||||
maxDate={dayjs()}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="role_id"
|
||||
label="Роль"
|
||||
rules={[{required: true, message: "Выберите роль"}]}
|
||||
>
|
||||
<Select>
|
||||
{roles.map((role) => (
|
||||
<Select.Option key={role.id} value={role.id}>
|
||||
{role.title}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="status_id"
|
||||
label="Статус"
|
||||
rules={[{required: true, message: "Выберите статус"}]}
|
||||
>
|
||||
<Select>
|
||||
{statusesData.map((status) => (
|
||||
<Select.Option key={status.id} value={status.id}>
|
||||
{status.title}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={isLoadingUpdate || isLoading}>
|
||||
Сохранить изменения
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Form form={passwordForm} onFinish={handlePasswordFinish}>
|
||||
<Typography.Title level={4}>Изменение пароля</Typography.Title>
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="Пароль"
|
||||
rules={[{required: true, message: "Введите пароль"}]}
|
||||
>
|
||||
<Input.Password/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="repeat_password"
|
||||
label="Подтверждение пароля"
|
||||
dependencies={["password"]}
|
||||
rules={[
|
||||
{required: true, message: "Повторите пароль"},
|
||||
({getFieldValue}) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue("password") === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error("Пароли не совпадают"));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={isLoadingUpdate || isLoading}>
|
||||
Изменить пароль
|
||||
</Button>
|
||||
<Button onClick={handleCancel} style={{marginLeft: 8}}>
|
||||
Отмена
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Row gutter={24}>
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={<Title level={5}><UserOutlined/> Основная информация</Title>}
|
||||
style={{marginBottom: 24, borderRadius: 12, boxShadow: "0 2px 8px rgba(0,0,0,0.05)"}}
|
||||
>
|
||||
<Form form={userForm} layout="vertical" onFinish={handleFinish}>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="last_name"
|
||||
label="Фамилия"
|
||||
rules={[{required: true, message: "Введите фамилию"}]}
|
||||
>
|
||||
<Input prefix={<UserOutlined/>} size="large" placeholder="Иванов"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="first_name"
|
||||
label="Имя"
|
||||
rules={[{required: true, message: "Введите имя"}]}
|
||||
>
|
||||
<Input prefix={<UserOutlined/>} size="large" placeholder="Иван"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="patronymic" label="Отчество">
|
||||
<Input prefix={<UserOutlined/>} size="large" placeholder="Иванович"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="email"
|
||||
label="Email"
|
||||
rules={[
|
||||
{required: true, message: "Введите email"},
|
||||
{type: "email", message: "Некорректный email"},
|
||||
]}
|
||||
>
|
||||
<Input prefix={<MailOutlined/>} size="large" placeholder="ivan@example.com"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="birthdate"
|
||||
label="Дата рождения"
|
||||
rules={[{required: true, message: "Выберите дату рождения"}]}
|
||||
>
|
||||
<DatePicker
|
||||
suffixIcon={<CalendarOutlined/>}
|
||||
format="DD.MM.YYYY"
|
||||
placeholder="15.03.1995"
|
||||
style={{width: "100%"}}
|
||||
size="large"
|
||||
maxDate={dayjs()}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item name="login" label="Логин">
|
||||
<Input disabled prefix={<UserOutlined/>} size="large"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="role_id"
|
||||
label={<><CrownOutlined/> Роль</>}
|
||||
rules={[{required: true, message: "Выберите роль"}]}
|
||||
>
|
||||
<Select size="large" placeholder="Выберите роль">
|
||||
{roles.map((role) => (
|
||||
<Select.Option key={role.id} value={role.id}>
|
||||
<Space>
|
||||
<CrownOutlined style={{color: "#722ed1"}}/>
|
||||
{role.title}
|
||||
</Space>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="status_id"
|
||||
label={<><TagOutlined/> Статус</>}
|
||||
rules={[{required: true, message: "Выберите статус"}]}
|
||||
>
|
||||
<Select size="large" placeholder="Выберите статус">
|
||||
{statusesData.map((status) => (
|
||||
<Select.Option key={status.id} value={status.id}>
|
||||
<Space>
|
||||
<div
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: status.title === "Активен" ? "#52c41a" : "#ff4d4f",
|
||||
display: "inline-block",
|
||||
marginRight: 8,
|
||||
}}
|
||||
/>
|
||||
{status.title}
|
||||
</Space>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item style={{marginBottom: 0, textAlign: "right"}}>
|
||||
<Space>
|
||||
<Button onClick={handleCancel}>Отмена</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={isLoadingUpdate}
|
||||
icon={<SaveOutlined/>}
|
||||
size="large"
|
||||
>
|
||||
Сохранить изменения
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Card
|
||||
title={<Title level={5}><LockOutlined/> Смена пароля</Title>}
|
||||
style={{borderRadius: 12, boxShadow: "0 2px 8px rgba(0,0,0,0.05)"}}
|
||||
>
|
||||
<Form form={passwordForm} layout="vertical" onFinish={handlePasswordFinish}>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="Новый пароль"
|
||||
rules={[
|
||||
{required: true, message: "Введите пароль"},
|
||||
{min: 8, message: "Минимум 8 символов"},
|
||||
]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined/>} size="large" placeholder="••••••••"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item
|
||||
name="repeat_password"
|
||||
label="Повторите пароль"
|
||||
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/>} size="large" placeholder="••••••••"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item style={{marginBottom: 0, textAlign: "right"}}>
|
||||
<Space>
|
||||
<Button onClick={handleCancel}>Отмена</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={isLoadingUpdate}
|
||||
icon={<LockOutlined/>}
|
||||
size="large"
|
||||
>
|
||||
Изменить пароль
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Modal>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateUserModalForm;
|
||||
Loading…
x
Reference in New Issue
Block a user