diff --git a/api/app/application/statuses_repository.py b/api/app/application/statuses_repository.py
index f189d03..38c82fa 100644
--- a/api/app/application/statuses_repository.py
+++ b/api/app/application/statuses_repository.py
@@ -1,4 +1,4 @@
-from typing import Optional
+from typing import Optional, List, Sequence
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -10,6 +10,11 @@ class StatusesRepository:
def __init__(self, db: AsyncSession) -> None:
self.db = db
+ async def get_all(self) -> Sequence[Status]:
+ query = select(Status)
+ results = await self.db.execute(query)
+ return results.scalars().all()
+
async def get_by_id(self, status_id: int) -> Optional[Status]:
query = (
select(Status)
diff --git a/api/app/controllers/statuses_router.py b/api/app/controllers/statuses_router.py
new file mode 100644
index 0000000..6fdb1e0
--- /dev/null
+++ b/api/app/controllers/statuses_router.py
@@ -0,0 +1,26 @@
+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.statuses import StatusRead
+from app.domain.models import User
+from app.infrastructure.dependencies import require_auth_user
+from app.infrastructure.statuses_servise import StatusesService
+
+statuses_router = APIRouter()
+
+
+@statuses_router.get(
+ '/',
+ response_model=List[StatusRead],
+ summary='Return all statuses',
+ description='Return all statuses',
+)
+async def get_statuses(
+ db: AsyncSession = Depends(get_db),
+ user: User = Depends(require_auth_user),
+):
+ statuses_service = StatusesService(db)
+ return await statuses_service.get_all()
diff --git a/api/app/controllers/users_router.py b/api/app/controllers/users_router.py
index 224e5a5..b4d59ad 100644
--- a/api/app/controllers/users_router.py
+++ b/api/app/controllers/users_router.py
@@ -72,6 +72,7 @@ async def change_password(
users_service = UsersService(db)
return await users_service.change_password(user_id, password_data, user)
+
@users_router.post(
'/create/',
response_model=Optional[UserRead],
diff --git a/api/app/domain/entities/users.py b/api/app/domain/entities/users.py
index df2d91c..5e5d910 100644
--- a/api/app/domain/entities/users.py
+++ b/api/app/domain/entities/users.py
@@ -34,8 +34,11 @@ class UserUpdate(BaseModel):
first_name: str = Field(max_length=250)
last_name: str = Field(max_length=250)
patronymic: Optional[str] = Field(default=None, max_length=250)
+ login: str = Field(max_length=250)
email: Optional[EmailStr] = None
birthdate: date
+ role_id: Optional[int] = Field(default=None)
+ status_id: Optional[int] = Field(default=None)
class PasswordChangeRequest(BaseModel):
diff --git a/api/app/infrastructure/statuses_servise.py b/api/app/infrastructure/statuses_servise.py
new file mode 100644
index 0000000..28ec290
--- /dev/null
+++ b/api/app/infrastructure/statuses_servise.py
@@ -0,0 +1,21 @@
+from typing import List
+
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.application.statuses_repository import StatusesRepository
+from app.domain.entities.statuses import StatusRead
+
+
+class StatusesService:
+ def __init__(self, db: AsyncSession):
+ self.statuses_repository = StatusesRepository(db)
+
+ async def get_all(self) -> List[StatusRead]:
+ statuses = await self.statuses_repository.get_all()
+ response = []
+ for status in statuses:
+ response.append(
+ StatusRead.model_validate(status)
+ )
+
+ return response
diff --git a/api/app/infrastructure/users_service.py b/api/app/infrastructure/users_service.py
index 634da3b..3a110ee 100644
--- a/api/app/infrastructure/users_service.py
+++ b/api/app/infrastructure/users_service.py
@@ -42,7 +42,7 @@ class UsersService:
detail='Пользователь не найден',
)
- if current_user.id != user_model.id and not current_user.role.title != self.settings.root_role_name:
+ if current_user.id != user_model.id and current_user.role.title != self.settings.root_role_name:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='Доступ запрещен',
@@ -54,6 +54,10 @@ class UsersService:
user_model.email = user.email
user_model.birthdate = user.birthdate
+ if current_user.role.title == self.settings.root_role_name and user.role_id is not None:
+ user_model.role_id = user.role_id
+ user_model.status_id = user.status_id
+
user_model = await self.users_repository.update(user_model)
return UserRead.model_validate(user_model)
@@ -68,7 +72,7 @@ class UsersService:
detail='Пользователь не найден',
)
- if current_user.id != user_model.id and not current_user.role.title != self.settings.root_role_name:
+ if current_user.id != user_model.id and current_user.role.title != self.settings.root_role_name:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='Доступ запрещен',
diff --git a/api/app/main.py b/api/app/main.py
index c4669ec..3c4f2d4 100644
--- a/api/app/main.py
+++ b/api/app/main.py
@@ -4,6 +4,7 @@ from starlette.middleware.cors import CORSMiddleware
from app.controllers.auth_router import auth_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
from app.controllers.users_router import users_router
from app.settings import Settings
@@ -23,6 +24,7 @@ def start_app():
api_app.include_router(auth_router, prefix=f'{settings.prefix}/auth', tags=['auth'])
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'])
api_app.include_router(users_router, prefix=f'{settings.prefix}/users', tags=['users'])
return api_app
diff --git a/web/src/Api/statusesApi.js b/web/src/Api/statusesApi.js
new file mode 100644
index 0000000..75fede2
--- /dev/null
+++ b/web/src/Api/statusesApi.js
@@ -0,0 +1,20 @@
+import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/query/react";
+import CONFIG from "../Core/сonfig.js";
+import {baseQueryWithAuth} from "./baseQuery.js";
+
+
+export const statusesApi = createApi({
+ reducerPath: 'statusesApi',
+ baseQuery: baseQueryWithAuth,
+ tagTypes: ['Statuses'],
+ endpoints: (builder) => ({
+ getStatuses: builder.query({
+ query: () => '/statuses/',
+ providesTags: ['Statuses'],
+ }),
+ }),
+});
+
+export const {
+ useGetStatusesQuery,
+} = statusesApi;
\ No newline at end of file
diff --git a/web/src/Api/usersApi.js b/web/src/Api/usersApi.js
index 4328428..280b620 100644
--- a/web/src/Api/usersApi.js
+++ b/web/src/Api/usersApi.js
@@ -13,6 +13,7 @@ export const usersApi = createApi({
}),
getAuthenticatedUserData: builder.query({
query: () => "/users/me/",
+ providesTags: ["user"],
}),
updateUser: builder.mutation({
query: ({userId, ...data}) => ({
diff --git a/web/src/Components/Pages/AdminPage/AdminPage.jsx b/web/src/Components/Pages/AdminPage/AdminPage.jsx
index 05f117d..33d3290 100644
--- a/web/src/Components/Pages/AdminPage/AdminPage.jsx
+++ b/web/src/Components/Pages/AdminPage/AdminPage.jsx
@@ -3,6 +3,7 @@ import {ControlOutlined, PlusOutlined} from "@ant-design/icons";
import useAdminPage from "./useAdminPage.js";
import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import CreateUserModalForm from "./CreateUserModalForm/CreateUserModalForm.jsx";
+import UpdateUserModalForm from "./UpdateUserModalForm/UpdateUserModalForm.jsx";
const {Title} = Typography;
@@ -15,6 +16,7 @@ const AdminPage = () => {
isLoading,
isError,
openCreateModal,
+ currentUser,
} = useAdminPage();
const columns = [
@@ -47,6 +49,13 @@ const AdminPage = () => {
filters: rolesData.map(role => ({text: role.title, value: role.title})),
onFilter: (value, record) => record.role.title === value,
},
+ {
+ title: "Статус",
+ dataIndex: ["status", "title"],
+ key: "status",
+ filters: rolesData.map(status => ({text: status.title, value: status.title})),
+ onFilter: (value, record) => record.status.title === value,
+ },
{
title: "Статус",
dataIndex: "status",
@@ -56,9 +65,9 @@ const AdminPage = () => {
title: "Действия",
key: "actions",
render: (_, record) => (
- ) : null)
),
},
];
@@ -97,6 +106,7 @@ const AdminPage = () => {
+
)
};
diff --git a/web/src/Components/Pages/AdminPage/CreateUserModalForm/useCreateUserModalForm.js b/web/src/Components/Pages/AdminPage/CreateUserModalForm/useCreateUserModalForm.js
index bd5a68f..559bfdd 100644
--- a/web/src/Components/Pages/AdminPage/CreateUserModalForm/useCreateUserModalForm.js
+++ b/web/src/Components/Pages/AdminPage/CreateUserModalForm/useCreateUserModalForm.js
@@ -36,8 +36,6 @@ const useCreateUserModalForm = () => {
: null,
};
- console.log(payload);
-
try {
await registerUser(payload).unwrap();
notification.success({
diff --git a/web/src/Components/Pages/AdminPage/UpdateUserModalForm/UpdateUserModalForm.jsx b/web/src/Components/Pages/AdminPage/UpdateUserModalForm/UpdateUserModalForm.jsx
new file mode 100644
index 0000000..21966db
--- /dev/null
+++ b/web/src/Components/Pages/AdminPage/UpdateUserModalForm/UpdateUserModalForm.jsx
@@ -0,0 +1,147 @@
+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 dayjs from "dayjs";
+import LoadingIndicator from "../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
+
+
+const UpdateUserModalForm = () => {
+ const {
+ modalVisible,
+ handleCancel,
+ handleFinish,
+ userForm,
+ passwordForm,
+ roles,
+ isLoading,
+ isError,
+ isLoadingUpdate,
+ isErrorUpdate,
+ handlePasswordFinish,
+ statusesData,
+ } = useUpdateUserModalForm();
+
+ if (isLoading) {
+ return
+ }
+
+ if (isError) {
+ return
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ format="DD.MM.YYYY"
+ style={{width: "100%"}}
+ size="large"
+ maxDate={dayjs()}
+ />
+
+
+
+
+
+
+
+
+
+ Сохранить изменения
+
+
+
+
+
+
+ ({
+ validator(_, value) {
+ if (!value || getFieldValue("password") === value) {
+ return Promise.resolve();
+ }
+ return Promise.reject(new Error("Пароли не совпадают"));
+ },
+ }),
+ ]}
+ >
+
+
+
+
+ Изменить пароль
+
+
+ Отмена
+
+
+
+
+ )
+};
+
+export default UpdateUserModalForm;
\ No newline at end of file
diff --git a/web/src/Components/Pages/AdminPage/UpdateUserModalForm/useUpdateUserModalForm.js b/web/src/Components/Pages/AdminPage/UpdateUserModalForm/useUpdateUserModalForm.js
new file mode 100644
index 0000000..d749512
--- /dev/null
+++ b/web/src/Components/Pages/AdminPage/UpdateUserModalForm/useUpdateUserModalForm.js
@@ -0,0 +1,137 @@
+import {useDispatch, useSelector} from "react-redux";
+import {Form, notification} from "antd";
+import {setSelectedUserToUpdate} from "../../../../Redux/Slices/usersSlice.js";
+import {useGetAllRolesQuery} from "../../../../Api/rolesApi.js";
+import {useEffect} from "react";
+import dayjs from "dayjs";
+import {useUpdateUserMutation, useUpdateUserPasswordMutation} from "../../../../Api/usersApi.js";
+import {useGetStatusesQuery} from "../../../../Api/statusesApi.js";
+
+
+const useUpdateUserModalForm = () => {
+ const dispatch = useDispatch();
+ const [userForm] = Form.useForm();
+ const [passwordForm] = Form.useForm();
+
+ const {
+ selectedUserToUpdate
+ } = useSelector(state => state.users);
+
+ const modalVisible = selectedUserToUpdate !== null;
+
+ const handleCancel = () => {
+ dispatch(setSelectedUserToUpdate(null));
+ };
+
+ const [
+ updateUser,
+ {
+ isLoading: isLoadingUpdate,
+ isError: isErrorUpdate,
+ }
+ ] = useUpdateUserMutation();
+
+ const [
+ changeUserPassword,
+ {
+ isLoading: isLoadingChangePassword,
+ isError: isErrorChangePassword,
+ }
+ ] = useUpdateUserPasswordMutation();
+
+ const {
+ data: statusesData = [],
+ isLoading: statusesIsLoading,
+ isError: statusesIsError,
+ } = useGetStatusesQuery(undefined);
+
+ useEffect(() => {
+ if (selectedUserToUpdate) {
+ const { role, ...userWithoutRole } = selectedUserToUpdate;
+
+ userForm.setFieldsValue({
+ ...userWithoutRole,
+ role_id: role?.id,
+ birthdate: selectedUserToUpdate.birthdate
+ ? dayjs(selectedUserToUpdate.birthdate)
+ : null,
+ });
+ }
+ }, [selectedUserToUpdate, userForm]);
+
+ const handlePasswordFinish = async () => {
+ const values = passwordForm.getFieldsValue();
+ const payload = {
+ ...values,
+ };
+
+ try {
+ await changeUserPassword({userId: selectedUserToUpdate.id, ...payload}).unwrap();
+ notification.success({
+ title: "Пароль изменен",
+ description: "Пароль успешно изменен",
+ placement: "topRight",
+ });
+ passwordForm.resetFields();
+ handleCancel();
+ } catch (error) {
+ notification.error({
+ title: "Ошибка изменения пароля",
+ description: error?.data?.detail || "Не удалось изменить пароль",
+ placement: "topRight",
+ });
+ }
+ };
+
+ const handleFinish = async () => {
+ const values = userForm.getFieldsValue();
+ const payload = {
+ ...values,
+ birthdate: values.birthdate
+ ? values.birthdate.format("YYYY-MM-DD")
+ : null,
+ };
+
+ try {
+ await updateUser({userId: selectedUserToUpdate.id, ...payload}).unwrap();
+ notification.success({
+ title: "Пользователь изменен",
+ description: "Пользователь успешно изменен",
+ placement: "topRight",
+ });
+ userForm.resetFields();
+ handleCancel();
+ } catch (error) {
+ notification.error({
+ title: "Ошибка изменения пользователя",
+ description: error?.data?.detail || "Не удалось изменить пользователя",
+ placement: "topRight",
+ });
+ }
+ };
+
+ const {
+ data: roles = [],
+ isLoading: isLoadingRoles,
+ isError: isErrorRoles,
+ } = useGetAllRolesQuery(undefined, {
+ pollingInterval: 60000,
+ });
+
+ return {
+ modalVisible,
+ handleCancel,
+ handleFinish,
+ userForm,
+ passwordForm,
+ roles,
+ isLoading: isLoadingRoles | statusesIsLoading,
+ isError: isErrorRoles | statusesIsError,
+ isLoadingUpdate,
+ isErrorUpdate,
+ handlePasswordFinish,
+ statusesData,
+ };
+};
+
+export default useUpdateUserModalForm;
\ No newline at end of file
diff --git a/web/src/Components/Pages/AdminPage/useAdminPage.js b/web/src/Components/Pages/AdminPage/useAdminPage.js
index e9ae146..c84335c 100644
--- a/web/src/Components/Pages/AdminPage/useAdminPage.js
+++ b/web/src/Components/Pages/AdminPage/useAdminPage.js
@@ -1,4 +1,4 @@
-import {useGetAllUsersQuery} from "../../../Api/usersApi.js";
+import {useGetAllUsersQuery, useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js";
import {useGetAllRolesQuery} from "../../../Api/rolesApi.js";
import {useMemo, useState} from "react";
import {useDispatch} from "react-redux";
@@ -7,6 +7,11 @@ import {setOpenModalCreateUser, setSelectedUserToUpdate} from "../../../Redux/Sl
const useAdminPage = () => {
const dispatch = useDispatch();
+ const {
+ data: currentUser = {},
+ isLoading: currentUserIsLoading,
+ isError: currentUserIsError,
+ } = useGetAuthenticatedUserDataQuery(undefined);
const [
searchString,
@@ -56,9 +61,10 @@ const useAdminPage = () => {
rolesData,
filteredUsers,
handleSearch,
- isLoading: usersIsLoading | rolesIsLoading,
- isError: usersIsError | rolesIsError,
+ isLoading: usersIsLoading | rolesIsLoading | currentUserIsLoading,
+ isError: usersIsError | rolesIsError | currentUserIsError,
openCreateModal,
+ currentUser,
};
};
diff --git a/web/src/Components/Pages/ProfilePage/ProfilePage.jsx b/web/src/Components/Pages/ProfilePage/ProfilePage.jsx
index e7bbc50..dfed89c 100644
--- a/web/src/Components/Pages/ProfilePage/ProfilePage.jsx
+++ b/web/src/Components/Pages/ProfilePage/ProfilePage.jsx
@@ -148,10 +148,8 @@ const ProfilePage = () => {
- {/* Карточка смены пароля */}
Смена пароля}
- bordered={false}
style={{borderRadius: 12, boxShadow: "0 4px 12px rgba(0,0,0,0.05)"}}
>