добавил в апи управление курсами, студентами курсов и учителями курсов

This commit is contained in:
Андрей Дувакин 2025-11-28 16:16:18 +05:00
parent 0a8e027b87
commit bb6537533b
14 changed files with 749 additions and 119 deletions

View 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

View 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

View 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

View 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)

View 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

View 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

View 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

View File

@ -12,7 +12,7 @@ class Course(PhotoAbstract):
title: Mapped[str] = mapped_column(String(250), nullable=False) title: Mapped[str] = mapped_column(String(250), nullable=False)
description: Mapped[str] = mapped_column(String(1000), nullable=True) description: Mapped[str] = mapped_column(String(1000), nullable=True)
teachers: Mapped[List['CourseTeacher']] = relationship('CourseTeacher', back_populates='course') teachers: Mapped[List['CourseTeacher']] = relationship('CourseTeacher', back_populates='course', lazy='select')
enrollments: Mapped[List['Enrollment']] = relationship('Enrollment', back_populates='course') enrollments: Mapped[List['Enrollment']] = relationship('Enrollment', back_populates='course', lazy='select')
lessons: Mapped[List['Lesson']] = relationship('Lesson', back_populates='course') lessons: Mapped[List['Lesson']] = relationship('Lesson', back_populates='course')
tasks: Mapped[List['Task']] = relationship('Task', back_populates='course') tasks: Mapped[List['Task']] = relationship('Task', back_populates='course')

View 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

View 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)

View File

@ -5,7 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from starlette import status from starlette import status
from app.application.users_repository import UsersRepository 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.database.session import get_db
from app.domain.models.users import User from app.domain.models.users import User
from app.settings import get_auth_data, Settings 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='Ошибка доступа') raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Ошибка доступа')
return user 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

View 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

View File

@ -2,6 +2,7 @@ from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware from starlette.middleware.cors import CORSMiddleware
from app.controllers.auth_router import auth_router 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.register_router import register_router
from app.controllers.roles_router import roles_router from app.controllers.roles_router import roles_router
from app.controllers.statuses_router import statuses_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(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(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(roles_router, prefix=f'{settings.prefix}/roles', tags=['roles'])
api_app.include_router(statuses_router, prefix=f'{settings.prefix}/statuses', tags=['statuses']) api_app.include_router(statuses_router, prefix=f'{settings.prefix}/statuses', tags=['statuses'])

View File

@ -1,9 +1,32 @@
import {Button, DatePicker, Form, Input, Modal, Result, Select, Tooltip, Typography} from "antd"; import {
import useUpdateUserModalForm from "./useUpdateUserModalForm.js"; Button,
import {CalendarOutlined, InfoCircleOutlined} from "@ant-design/icons"; 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 dayjs from "dayjs";
import LoadingIndicator from "../../../Widgets/LoadingIndicator/LoadingIndicator.jsx"; import LoadingIndicator from "../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import useUpdateUserModalForm from "./useUpdateUserModalForm.js";
const {Title, Text} = Typography;
const UpdateUserModalForm = () => { const UpdateUserModalForm = () => {
const { const {
@ -13,135 +36,237 @@ const UpdateUserModalForm = () => {
userForm, userForm,
passwordForm, passwordForm,
roles, roles,
statusesData,
isLoading, isLoading,
isError, isError,
isLoadingUpdate, isLoadingUpdate,
isErrorUpdate,
handlePasswordFinish, handlePasswordFinish,
statusesData,
} = useUpdateUserModalForm(); } = useUpdateUserModalForm();
if (isLoading) { if (isLoading) return <LoadingIndicator/>;
return <LoadingIndicator/>
}
if (isError) { if (isError) {
return <Result status="500" title="500" subTitle="Произошла ошибка при загрузке данных пользователя"/> return (
<Result status="500" title="500" subTitle="Ошибка загрузки данных пользователя"/>
);
} }
return ( return (
<Modal <Modal
title="Изменить пользователя"
open={modalVisible} open={modalVisible}
onCancel={handleCancel} onCancel={handleCancel}
footer={null} 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 <Row gutter={24}>
form={userForm} <Col span={24}>
onFinish={handleFinish} <Card
layout="vertical" title={<Title level={5}><UserOutlined/> Основная информация</Title>}
> style={{marginBottom: 24, borderRadius: 12, boxShadow: "0 2px 8px rgba(0,0,0,0.05)"}}
<Form.Item label="Фамилия" name="last_name" rules={[{required: true, message: "Введите фамилию"}]}> >
<Input/> <Form form={userForm} layout="vertical" onFinish={handleFinish}>
</Form.Item> <Row gutter={16}>
<Form.Item label="Имя" name="first_name" rules={[{required: true, message: "Введите имя"}]}> <Col xs={24} md={12}>
<Input/> <Form.Item
</Form.Item> name="last_name"
<Form.Item label="Отчество" name="patronymic"> label="Фамилия"
<Input/> rules={[{required: true, message: "Введите фамилию"}]}
</Form.Item> >
<Form.Item label="Логин" name="login" rules={[{required: true, message: "Введите логин"}]}> <Input prefix={<UserOutlined/>} size="large" placeholder="Иванов"/>
<Input disabled/> </Form.Item>
</Form.Item> </Col>
<Form.Item
name="email" <Col xs={24} md={12}>
label="Email" <Form.Item
rules={[{required: true, message: "Введите email", type: "email"}]}> name="first_name"
<Input/> label="Имя"
</Form.Item> rules={[{required: true, message: "Введите имя"}]}
<Form.Item >
name="birthdate" <Input prefix={<UserOutlined/>} size="large" placeholder="Иван"/>
label="Дата рождения" </Form.Item>
rules={[{required: true, message: "Введите дату рождения"}]} </Col>
>
<DatePicker <Col xs={24} md={12}>
suffixIcon={<CalendarOutlined/>} <Form.Item name="patronymic" label="Отчество">
format="DD.MM.YYYY" <Input prefix={<UserOutlined/>} size="large" placeholder="Иванович"/>
style={{width: "100%"}} </Form.Item>
size="large" </Col>
maxDate={dayjs()}
/> <Col xs={24} md={12}>
</Form.Item> <Form.Item
<Form.Item name="email"
name="role_id" label="Email"
label="Роль" rules={[
rules={[{required: true, message: "Выберите роль"}]} {required: true, message: "Введите email"},
> {type: "email", message: "Некорректный email"},
<Select> ]}
{roles.map((role) => ( >
<Select.Option key={role.id} value={role.id}> <Input prefix={<MailOutlined/>} size="large" placeholder="ivan@example.com"/>
{role.title} </Form.Item>
</Select.Option> </Col>
))}
</Select> <Col xs={24} md={12}>
</Form.Item> <Form.Item
<Form.Item name="birthdate"
name="status_id" label="Дата рождения"
label="Статус" rules={[{required: true, message: "Выберите дату рождения"}]}
rules={[{required: true, message: "Выберите статус"}]} >
> <DatePicker
<Select> suffixIcon={<CalendarOutlined/>}
{statusesData.map((status) => ( format="DD.MM.YYYY"
<Select.Option key={status.id} value={status.id}> placeholder="15.03.1995"
{status.title} style={{width: "100%"}}
</Select.Option> size="large"
))} maxDate={dayjs()}
</Select> />
</Form.Item> </Form.Item>
<Form.Item> </Col>
<Button type="primary" htmlType="submit" loading={isLoadingUpdate || isLoading}>
Сохранить изменения <Col xs={24} md={12}>
</Button> <Form.Item name="login" label="Логин">
</Form.Item> <Input disabled prefix={<UserOutlined/>} size="large"/>
</Form> </Form.Item>
<Form form={passwordForm} onFinish={handlePasswordFinish}> </Col>
<Typography.Title level={4}>Изменение пароля</Typography.Title>
<Form.Item <Col xs={24} md={12}>
name="password" <Form.Item
label="Пароль" name="role_id"
rules={[{required: true, message: "Введите пароль"}]} label={<><CrownOutlined/> Роль</>}
> rules={[{required: true, message: "Выберите роль"}]}
<Input.Password/> >
</Form.Item> <Select size="large" placeholder="Выберите роль">
<Form.Item {roles.map((role) => (
name="repeat_password" <Select.Option key={role.id} value={role.id}>
label="Подтверждение пароля" <Space>
dependencies={["password"]} <CrownOutlined style={{color: "#722ed1"}}/>
rules={[ {role.title}
{required: true, message: "Повторите пароль"}, </Space>
({getFieldValue}) => ({ </Select.Option>
validator(_, value) { ))}
if (!value || getFieldValue("password") === value) { </Select>
return Promise.resolve(); </Form.Item>
} </Col>
return Promise.reject(new Error("Пароли не совпадают"));
}, <Col xs={24} md={12}>
}), <Form.Item
]} name="status_id"
> label={<><TagOutlined/> Статус</>}
<Input.Password/> rules={[{required: true, message: "Выберите статус"}]}
</Form.Item> >
<Form.Item> <Select size="large" placeholder="Выберите статус">
<Button type="primary" htmlType="submit" loading={isLoadingUpdate || isLoading}> {statusesData.map((status) => (
Изменить пароль <Select.Option key={status.id} value={status.id}>
</Button> <Space>
<Button onClick={handleCancel} style={{marginLeft: 8}}> <div
Отмена style={{
</Button> width: 8,
</Form.Item> height: 8,
</Form> 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> </Modal>
) );
}; };
export default UpdateUserModalForm; export default UpdateUserModalForm;