сделал шаблон для участников
This commit is contained in:
parent
49cc307f2c
commit
49b88ba505
@ -10,6 +10,11 @@ class ProjectMembersRepository:
|
|||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
self.db = db
|
self.db = db
|
||||||
|
|
||||||
|
async def get_all(self) -> Sequence[ProjectMember]:
|
||||||
|
stmt = select(ProjectMember)
|
||||||
|
result = await self.db.execute(stmt)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
async def get_by_id(self, project_member_id: int) -> Optional[ProjectMember]:
|
async def get_by_id(self, project_member_id: int) -> Optional[ProjectMember]:
|
||||||
stmt = select(ProjectMember).filter_by(id=project_member_id)
|
stmt = select(ProjectMember).filter_by(id=project_member_id)
|
||||||
result = await self.db.execute(stmt)
|
result = await self.db.execute(stmt)
|
||||||
@ -43,5 +48,7 @@ class ProjectMembersRepository:
|
|||||||
for project_member in project_members:
|
for project_member in project_members:
|
||||||
await self.db.delete(project_member)
|
await self.db.delete(project_member)
|
||||||
|
|
||||||
|
async def delete_member(self, project_member: ProjectMember) -> ProjectMember:
|
||||||
|
await self.db.delete(project_member)
|
||||||
await self.db.commit()
|
await self.db.commit()
|
||||||
return project_members
|
return project_member
|
||||||
|
|||||||
@ -11,6 +11,19 @@ from app.infrastructure.project_members_service import ProjectMembersService
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
'/',
|
||||||
|
response_model=list[ProjectMemberEntity],
|
||||||
|
summary='Get all project members',
|
||||||
|
description='Returns all project members',
|
||||||
|
)
|
||||||
|
async def get_all_project_members(
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
project_members_service = ProjectMembersService(db)
|
||||||
|
return await project_members_service.get_all_project_members()
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'/by-project/{project_id}/',
|
'/by-project/{project_id}/',
|
||||||
response_model=list[ProjectMemberEntity],
|
response_model=list[ProjectMemberEntity],
|
||||||
@ -84,3 +97,17 @@ async def delete_project_members(
|
|||||||
service = ProjectMembersService(db)
|
service = ProjectMembersService(db)
|
||||||
await service.delete_project_members_by_project_id(project_id)
|
await service.delete_project_members_by_project_id(project_id)
|
||||||
return {"message": "All project members have been successfully deleted."}
|
return {"message": "All project members have been successfully deleted."}
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
'/member/{member_id}/',
|
||||||
|
summary='Delete a single project member by ID',
|
||||||
|
description='Deletes a specific project member by their unique ID',
|
||||||
|
)
|
||||||
|
async def delete_single_project_member(
|
||||||
|
member_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
user=Depends(require_admin),
|
||||||
|
):
|
||||||
|
service = ProjectMembersService(db)
|
||||||
|
deleted_member = await service.delete_project_member_by_id(member_id)
|
||||||
|
return deleted_member
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
from app.database.session import get_db
|
|
||||||
from app.domain.entities.register import RegisterEntity
|
|
||||||
from app.domain.entities.user import UserEntity
|
|
||||||
from app.infrastructure.users_service import UsersService
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
'/',
|
|
||||||
response_model=Optional[UserEntity],
|
|
||||||
summary='User Registration',
|
|
||||||
description='Performs user registration in the system',
|
|
||||||
)
|
|
||||||
async def register_user(
|
|
||||||
user_data: RegisterEntity,
|
|
||||||
db: AsyncSession = Depends(get_db)
|
|
||||||
):
|
|
||||||
users_service = UsersService(db)
|
|
||||||
user = await users_service.register_user(user_data)
|
|
||||||
return user
|
|
||||||
@ -21,7 +21,6 @@ router = APIRouter()
|
|||||||
)
|
)
|
||||||
async def get_all_teams(
|
async def get_all_teams(
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
user=Depends(get_current_user),
|
|
||||||
):
|
):
|
||||||
teams_service = TeamsService(db)
|
teams_service = TeamsService(db)
|
||||||
return await teams_service.get_all_teams()
|
return await teams_service.get_all_teams()
|
||||||
@ -33,7 +32,7 @@ async def get_all_teams(
|
|||||||
summary='Get active team',
|
summary='Get active team',
|
||||||
description='Returns active team',
|
description='Returns active team',
|
||||||
)
|
)
|
||||||
async def get_all_teams(
|
async def get_active_team(
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
teams_service = TeamsService(db)
|
teams_service = TeamsService(db)
|
||||||
@ -61,7 +60,7 @@ async def create_team(
|
|||||||
summary='Make team active',
|
summary='Make team active',
|
||||||
description='Makes team active',
|
description='Makes team active',
|
||||||
)
|
)
|
||||||
async def update_team(
|
async def set_active_team(
|
||||||
team_id: int,
|
team_id: int,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
user=Depends(require_admin),
|
user=Depends(require_admin),
|
||||||
|
|||||||
@ -1,15 +1,29 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, Body
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.database.session import get_db
|
from app.database.session import get_db
|
||||||
|
from app.domain.entities.register import RegisterEntity
|
||||||
from app.domain.entities.user import UserEntity
|
from app.domain.entities.user import UserEntity
|
||||||
from app.infrastructure.dependencies import get_current_user
|
from app.infrastructure.dependencies import get_current_user
|
||||||
from app.infrastructure.users_service import UsersService
|
from app.infrastructure.users_service import UsersService
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
'/',
|
||||||
|
response_model=Optional[UserEntity],
|
||||||
|
summary='User Registration',
|
||||||
|
description='Performs user registration in the system',
|
||||||
|
)
|
||||||
|
async def register_user(
|
||||||
|
user_data: RegisterEntity,
|
||||||
|
db: AsyncSession = Depends(get_db)
|
||||||
|
):
|
||||||
|
users_service = UsersService(db)
|
||||||
|
user = await users_service.register_user(user_data)
|
||||||
|
return user
|
||||||
|
|
||||||
@router.put(
|
@router.put(
|
||||||
'/{user_id}/',
|
'/{user_id}/',
|
||||||
@ -17,9 +31,9 @@ router = APIRouter()
|
|||||||
summary='Change user password',
|
summary='Change user password',
|
||||||
description='Change user password',
|
description='Change user password',
|
||||||
)
|
)
|
||||||
async def create_user(
|
async def change_user_password(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
new_password: str,
|
new_password: str = Body(..., embed=True), # <--- ИЗМЕНЕНИЕ ЗДЕСЬ! Явно указывает, что в теле запроса
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
user=Depends(get_current_user),
|
user=Depends(get_current_user),
|
||||||
):
|
):
|
||||||
|
|||||||
@ -16,6 +16,13 @@ class ProjectMembersService:
|
|||||||
self.projects_repository = ProjectsRepository(db)
|
self.projects_repository = ProjectsRepository(db)
|
||||||
self.profiles_repository = ProfilesRepository(db)
|
self.profiles_repository = ProfilesRepository(db)
|
||||||
|
|
||||||
|
async def get_all_project_members(self) -> list[ProjectMemberEntity]:
|
||||||
|
project_members = await self.project_members_repository.get_all()
|
||||||
|
return [
|
||||||
|
self.model_to_entity(project_member)
|
||||||
|
for project_member in project_members
|
||||||
|
]
|
||||||
|
|
||||||
async def get_project_members_by_project_id(self, project_id: int) -> list[ProjectMemberEntity]:
|
async def get_project_members_by_project_id(self, project_id: int) -> list[ProjectMemberEntity]:
|
||||||
project_members = await self.project_members_repository.get_by_project_id(project_id)
|
project_members = await self.project_members_repository.get_by_project_id(project_id)
|
||||||
return [
|
return [
|
||||||
@ -107,6 +114,17 @@ class ProjectMembersService:
|
|||||||
for project_member in project_members
|
for project_member in project_members
|
||||||
]
|
]
|
||||||
|
|
||||||
|
async def delete_project_member_by_id(self, member_id: int) -> Optional[ProjectMemberEntity]:
|
||||||
|
member = await self.project_members_repository.get_by_id(member_id)
|
||||||
|
if not member:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f'Project member with ID {member_id} not found'
|
||||||
|
)
|
||||||
|
await self.project_members_repository.delete_member(member)
|
||||||
|
return self.model_to_entity(member)
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def model_to_entity(project: ProjectMember) -> ProjectMemberEntity:
|
def model_to_entity(project: ProjectMember) -> ProjectMemberEntity:
|
||||||
return ProjectMemberEntity(
|
return ProjectMemberEntity(
|
||||||
|
|||||||
@ -7,7 +7,6 @@ from app.contollers.profiles_router import router as profiles_router
|
|||||||
from app.contollers.project_files_router import router as project_files_router
|
from app.contollers.project_files_router import router as project_files_router
|
||||||
from app.contollers.project_members_router import router as project_members_router
|
from app.contollers.project_members_router import router as project_members_router
|
||||||
from app.contollers.projects_router import router as projects_router
|
from app.contollers.projects_router import router as projects_router
|
||||||
from app.contollers.register_router import router as register_router
|
|
||||||
from app.contollers.rss_router import router as rss_router
|
from app.contollers.rss_router import router as rss_router
|
||||||
from app.contollers.teams_router import router as team_router
|
from app.contollers.teams_router import router as team_router
|
||||||
from app.contollers.users_router import router as users_router
|
from app.contollers.users_router import router as users_router
|
||||||
@ -35,7 +34,6 @@ def start_app():
|
|||||||
api_app.include_router(project_members_router, prefix=f'{settings.PREFIX}/project_members',
|
api_app.include_router(project_members_router, prefix=f'{settings.PREFIX}/project_members',
|
||||||
tags=['project_members'])
|
tags=['project_members'])
|
||||||
api_app.include_router(projects_router, prefix=f'{settings.PREFIX}/projects', tags=['projects'])
|
api_app.include_router(projects_router, prefix=f'{settings.PREFIX}/projects', tags=['projects'])
|
||||||
api_app.include_router(register_router, prefix=f'{settings.PREFIX}/register', tags=['register'])
|
|
||||||
api_app.include_router(rss_router, prefix=f'{settings.PREFIX}/rss', tags=['rss_router'])
|
api_app.include_router(rss_router, prefix=f'{settings.PREFIX}/rss', tags=['rss_router'])
|
||||||
api_app.include_router(team_router, prefix=f'{settings.PREFIX}/teams', tags=['teams'])
|
api_app.include_router(team_router, prefix=f'{settings.PREFIX}/teams', tags=['teams'])
|
||||||
api_app.include_router(users_router, prefix=f'{settings.PREFIX}/users', tags=['users'])
|
api_app.include_router(users_router, prefix=f'{settings.PREFIX}/users', tags=['users'])
|
||||||
|
|||||||
@ -11,11 +11,12 @@ const loginUser = async (loginData) => {
|
|||||||
|
|
||||||
return { access_token, user_id };
|
return { access_token, user_id };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.response?.status === 401) {
|
if (error.response) {
|
||||||
|
if (error.response.status === 403) {
|
||||||
throw new Error("Неверное имя пользователя или пароль");
|
throw new Error("Неверное имя пользователя или пароль");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
throw new Error(error.message);
|
throw new Error(error.message || "Произошла неизвестная ошибка при входе.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
40
WEB/src/api/project_members/createProjectMember.js
Normal file
40
WEB/src/api/project_members/createProjectMember.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import CONFIG from "@/core/config.js";
|
||||||
|
|
||||||
|
|
||||||
|
const createProjectMember = async (projectMemberData) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
|
||||||
|
if (!projectMemberData.project_id) {
|
||||||
|
throw new Error("Отсутствует ID проекта для создания участника проекта.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestPayload = [projectMemberData];
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${CONFIG.BASE_URL}/project_members/${projectMemberData.project_id}/`,
|
||||||
|
requestPayload,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return response.data; // Бэкенд возвращает список, но мы ожидаем один созданный объект
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Нет доступа для создания участников проекта (401): Требуется авторизация.");
|
||||||
|
} else if (error.response?.status === 403) {
|
||||||
|
throw new Error("Доступ запрещён (403): Недостаточно прав (требуются права администратора).");
|
||||||
|
} else if (error.response?.status === 400) {
|
||||||
|
throw new Error(`Ошибка запроса: ${error.response.data.detail || error.message}`);
|
||||||
|
}
|
||||||
|
throw new Error(`Ошибка при создании участников проекта: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createProjectMember;
|
||||||
30
WEB/src/api/project_members/deleteProjectMember.js
Normal file
30
WEB/src/api/project_members/deleteProjectMember.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import CONFIG from "@/core/config.js";
|
||||||
|
|
||||||
|
const deleteProjectMember = async (projectId) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
const response = await axios.delete(
|
||||||
|
`${CONFIG.BASE_URL}/project_members/${projectId}/`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Нет доступа для удаления участников проекта (401): Требуется авторизация.");
|
||||||
|
} else if (error.response?.status === 403) {
|
||||||
|
throw new Error("Доступ запрещён (403): Недостаточно прав.");
|
||||||
|
} else if (error.response?.status === 405) {
|
||||||
|
throw new Error("Ошибка метода (405): Сервер не разрешает DELETE для этого ресурса. Проверьте конфигурацию API и соответствие URL.");
|
||||||
|
} else if (error.response?.status === 400 || error.response?.status === 422) {
|
||||||
|
throw new Error(`Ошибка запроса: ${error.response.data.detail || error.message}`);
|
||||||
|
}
|
||||||
|
throw new Error(`Ошибка при удалении участников проекта: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default deleteProjectMember;
|
||||||
30
WEB/src/api/project_members/deleteSingleProjectMember.js
Normal file
30
WEB/src/api/project_members/deleteSingleProjectMember.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import CONFIG from "@/core/config.js";
|
||||||
|
|
||||||
|
const deleteSingleProjectMember = async (memberId) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
const response = await axios.delete(
|
||||||
|
`${CONFIG.BASE_URL}/project_members/member/${memberId}/`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Нет доступа для удаления участника проекта (401): Требуется авторизация.");
|
||||||
|
} else if (error.response?.status === 403) {
|
||||||
|
throw new Error("Доступ запрещён (403): Недостаточно прав.");
|
||||||
|
} else if (error.response?.status === 404) { // Добавлено 404, если участник не найден
|
||||||
|
throw new Error("Участник проекта не найден (404).");
|
||||||
|
} else if (error.response?.status === 400 || error.response?.status === 422) {
|
||||||
|
throw new Error(`Ошибка запроса: ${error.response.data.detail || error.message}`);
|
||||||
|
}
|
||||||
|
throw new Error(`Ошибка при удалении участника проекта: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default deleteSingleProjectMember;
|
||||||
26
WEB/src/api/project_members/getAllProjectMembers.js
Normal file
26
WEB/src/api/project_members/getAllProjectMembers.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import CONFIG from "@/core/config.js";
|
||||||
|
|
||||||
|
const getAllProjectMembers = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("access_token"); // Получаем токен из localStorage
|
||||||
|
const response = await axios.get(`${CONFIG.BASE_URL}/project_members/`, { // Запрос к эндпоинту членов проекта
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`, // Отправляем токен для авторизации
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data; // Возвращаем полученные данные
|
||||||
|
} catch (error) {
|
||||||
|
// Обработка ошибок в зависимости от статуса ответа
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Нет доступа к членам проекта (401). Требуется авторизация.");
|
||||||
|
} else if (error.response?.status === 403) {
|
||||||
|
throw new new Error("Доступ запрещён (403). У вас нет прав для просмотра членов проекта.");
|
||||||
|
}
|
||||||
|
// В случае других ошибок, выбрасываем стандартную ошибку
|
||||||
|
throw new Error(error.message || "Неизвестная ошибка при получении членов проекта.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getAllProjectMembers;
|
||||||
24
WEB/src/api/project_members/getProjectMemberByProject.js
Normal file
24
WEB/src/api/project_members/getProjectMemberByProject.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import CONFIG from "@/core/config.js";
|
||||||
|
|
||||||
|
const getProjectMembersByProject = async (projectId) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
const response = await axios.get(`${CONFIG.BASE_URL}/project_members/by-project/${projectId}/`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Нет доступа к участникам проекта (401): Требуется авторизация.");
|
||||||
|
} else if (error.response?.status === 403) {
|
||||||
|
throw new Error("Доступ запрещён (403): Недостаточно прав.");
|
||||||
|
}
|
||||||
|
throw new Error(`Ошибка при получении участников проекта: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getProjectMembersByProject;
|
||||||
24
WEB/src/api/project_members/getProjectMembersByProfile.js
Normal file
24
WEB/src/api/project_members/getProjectMembersByProfile.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import CONFIG from "@/core/config.js";
|
||||||
|
|
||||||
|
const getProjectMembersByProfile = async (profileId) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
const response = await axios.get(`${CONFIG.BASE_URL}/project_members/by-profile/${profileId}/`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Нет доступа к данным профиля (401): Требуется авторизация.");
|
||||||
|
} else if (error.response?.status === 403) {
|
||||||
|
throw new Error("Доступ запрещён (403): Недостаточно прав.");
|
||||||
|
}
|
||||||
|
throw new Error(`Ошибка при получении участников проекта по профилю: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getProjectMembersByProfile;
|
||||||
52
WEB/src/api/project_members/updateProjectMember.js
Normal file
52
WEB/src/api/project_members/updateProjectMember.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
// @/api/project_members/updateProjectMember.js
|
||||||
|
import axios from "axios";
|
||||||
|
import CONFIG from "@/core/config.js";
|
||||||
|
|
||||||
|
// Эта функция теперь будет принимать project_id и ПОЛНЫЙ СПИСОК projectMembersData
|
||||||
|
// для данного проекта.
|
||||||
|
const updateProjectMember = async (projectId, projectMembersData) => { // Изменено: теперь принимает project_id и список
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("access_token");
|
||||||
|
|
||||||
|
// Убедимся, что project_id присутствует
|
||||||
|
if (!projectId) {
|
||||||
|
throw new Error("Отсутствует ID проекта для обновления списка участников.");
|
||||||
|
}
|
||||||
|
// Убедимся, что projectMembersData - это массив
|
||||||
|
if (!Array.isArray(projectMembersData)) {
|
||||||
|
throw new Error("Данные для обновления участников проекта должны быть массивом.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Бэкенд ожидает, что каждый объект в списке также будет содержать project_id,
|
||||||
|
// и что этот project_id будет совпадать с projectId из URL.
|
||||||
|
const payloadWithProjectIds = projectMembersData.map(member => ({
|
||||||
|
...member,
|
||||||
|
project_id: projectId // Убедимся, что project_id установлен в каждом объекте
|
||||||
|
}));
|
||||||
|
|
||||||
|
const response = await axios.put(
|
||||||
|
`${CONFIG.BASE_URL}/project_members/${projectId}/`, // URL теперь использует project_id
|
||||||
|
payloadWithProjectIds, // Отправляем полный список с project_id в каждом элементе
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error("Нет доступа для обновления участников проекта (401): Требуется авторизация.");
|
||||||
|
} else if (error.response?.status === 403) {
|
||||||
|
throw new Error("Доступ запрещён (403): Недостаточно прав.");
|
||||||
|
} else if (error.response?.status === 400 || error.response?.status === 422) {
|
||||||
|
console.error("Backend Error Detail:", error.response.data); // Выводим детали ошибки бэкенда
|
||||||
|
throw new Error(`Ошибка запроса при обновлении: ${error.response.data.detail || error.message}`);
|
||||||
|
}
|
||||||
|
throw new Error(`Ошибка при обновлении участников проекта: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default updateProjectMember;
|
||||||
27
WEB/src/api/teams/downloadTeamPhotoFile.js
Normal file
27
WEB/src/api/teams/downloadTeamPhotoFile.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
|
const downloadTeamPhotoFile = async (teamId) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token')
|
||||||
|
|
||||||
|
const response = await axios.get(
|
||||||
|
`${CONFIG.BASE_URL}/teams/${teamId}/file/`,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
},
|
||||||
|
responseType: 'blob'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.detail || error.message
|
||||||
|
console.error(`Ошибка загрузки файла логотипа команды с ID ${teamId}:`, errorMessage)
|
||||||
|
throw new Error(`Не удалось загрузить файл логотипа команды: ${errorMessage}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default downloadTeamPhotoFile
|
||||||
28
WEB/src/api/teams/getActiveTeam.js
Normal file
28
WEB/src/api/teams/getActiveTeam.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import CONFIG from '@/core/config.js';
|
||||||
|
|
||||||
|
const getActiveTeam = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
const response = await axios.get(
|
||||||
|
`${CONFIG.BASE_URL}/teams/active/`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error('Нет доступа для получения активной команды (401): Требуется авторизация.');
|
||||||
|
} else if (error.response?.status === 403) {
|
||||||
|
throw new Error('Доступ запрещён (403): Недостаточно прав.');
|
||||||
|
} else if (error.response?.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new Error(`Ошибка при получении активной команды: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getActiveTeam;
|
||||||
32
WEB/src/api/teams/setActiveTeam.js
Normal file
32
WEB/src/api/teams/setActiveTeam.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import CONFIG from '@/core/config.js';
|
||||||
|
|
||||||
|
const setActiveTeam = async (teamId) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
const response = await axios.put(
|
||||||
|
`${CONFIG.BASE_URL}/teams/${teamId}/set-active/`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
throw new Error('Нет доступа для установки активной команды (401): Требуется авторизация.');
|
||||||
|
} else if (error.response?.status === 403) {
|
||||||
|
throw new Error('Доступ запрещён (403): Недостаточно прав (требуются права администратора).');
|
||||||
|
} else if (error.response?.status === 400) {
|
||||||
|
throw new Error(`Ошибка запроса: ${error.response.data.detail || error.message}`);
|
||||||
|
} else if (error.response?.status === 404) {
|
||||||
|
throw new Error(`Команда с ID ${teamId} не найдена.`);
|
||||||
|
}
|
||||||
|
throw new Error(`Ошибка при установке активной команды: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default setActiveTeam;
|
||||||
30
WEB/src/api/teams/uploadTeamPhoto.js
Normal file
30
WEB/src/api/teams/uploadTeamPhoto.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
|
const uploadTeamPhoto = async (teamId, file) => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token')
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${CONFIG.BASE_URL}/teams/${teamId}/upload/`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.detail || error.message
|
||||||
|
console.error(`Ошибка загрузки фотографии для команды ${teamId}:`, errorMessage)
|
||||||
|
throw new Error(`Не удалось загрузить фотографию команды: ${errorMessage}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default uploadTeamPhoto
|
||||||
@ -1,14 +1,15 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import CONFIG from "@/core/config.js";
|
import CONFIG from "@/core/config.js";
|
||||||
|
|
||||||
const changeUserPassword = async (userId, newPasswordData) => {
|
const changeUserPassword = async (userId, newPassword) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem("access_token");
|
const token = localStorage.getItem("access_token");
|
||||||
const response = await axios.patch(
|
const response = await axios.put(
|
||||||
`${CONFIG.BASE_URL}/users/${userId}/password`,
|
`${CONFIG.BASE_URL}/users/${userId}/`,
|
||||||
newPasswordData,
|
{ new_password: newPassword },
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -19,6 +20,11 @@ const changeUserPassword = async (userId, newPasswordData) => {
|
|||||||
throw new Error(error.response.data.detail);
|
throw new Error(error.response.data.detail);
|
||||||
} else if (error.response?.status === 403) {
|
} else if (error.response?.status === 403) {
|
||||||
throw new Error("Доступ запрещён (403)");
|
throw new Error("Доступ запрещён (403)");
|
||||||
|
} else if (error.response?.status === 422) {
|
||||||
|
const errorMessage = error.response.data.detail ?
|
||||||
|
error.response.data.detail.map(err => `${err.loc.join('.')} - ${err.msg}`).join('; ') :
|
||||||
|
'Неизвестная ошибка валидации';
|
||||||
|
throw new Error(`Ошибка валидации данных (422): ${errorMessage}`);
|
||||||
}
|
}
|
||||||
throw new Error(error.message);
|
throw new Error(error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,23 @@
|
|||||||
import axios from "axios";
|
import axios from 'axios';
|
||||||
import CONFIG from "@/core/config.js";
|
import CONFIG from '@/core/config.js';
|
||||||
|
|
||||||
const registerUser = async (userData) => {
|
const registerUser = async (userData) => {
|
||||||
try {
|
try {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${CONFIG.BASE_URL}/users/register`,
|
`${CONFIG.BASE_URL}/users/`,
|
||||||
userData
|
userData,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.response?.status === 400) {
|
throw new Error(error.response?.data?.detail || error.message);
|
||||||
throw new Error(error.response.data.detail);
|
|
||||||
}
|
|
||||||
throw new Error(error.message);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -40,7 +40,8 @@ import {
|
|||||||
QItemLabel,
|
QItemLabel,
|
||||||
QItem,
|
QItem,
|
||||||
QImg,
|
QImg,
|
||||||
QFile
|
QFile,
|
||||||
|
QSelect
|
||||||
} from 'quasar'
|
} from 'quasar'
|
||||||
|
|
||||||
|
|
||||||
@ -60,7 +61,7 @@ app.use(Quasar, {
|
|||||||
QSeparator, QCardActions, QDialog, QIcon, QSpace,
|
QSeparator, QCardActions, QDialog, QIcon, QSpace,
|
||||||
QAvatar, QTooltip, QBanner, QSlideTransition, QToggle,
|
QAvatar, QTooltip, QBanner, QSlideTransition, QToggle,
|
||||||
QList, QSpinnerDots, QCarouselSlide, QCarousel,
|
QList, QSpinnerDots, QCarouselSlide, QCarousel,
|
||||||
QItemSection, QItemLabel, QItem, QImg, QFile
|
QItemSection, QItemLabel, QItem, QImg, QFile, QSelect
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
Ripple
|
Ripple
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
<q-tab name="teams" label="Команды" />
|
<q-tab name="teams" label="Команды" />
|
||||||
<q-tab name="projects" label="Проекты" />
|
<q-tab name="projects" label="Проекты" />
|
||||||
<q-tab name="contests" label="Конкурсы" />
|
<q-tab name="contests" label="Конкурсы" />
|
||||||
|
<q-tab name="project_members" label="Участники проектов" />
|
||||||
</q-tabs>
|
</q-tabs>
|
||||||
</q-header>
|
</q-header>
|
||||||
|
|
||||||
@ -27,7 +28,14 @@
|
|||||||
<q-btn
|
<q-btn
|
||||||
label="Создание пользователя"
|
label="Создание пользователя"
|
||||||
color="primary"
|
color="primary"
|
||||||
@click="createHandler"
|
@click="createNewUserHandler"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
color="primary"
|
||||||
|
icon="cached"
|
||||||
|
title="Поменять пароль у пользователя"
|
||||||
|
@click="changePasswordHandler"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<q-table
|
<q-table
|
||||||
@ -110,6 +118,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<q-tab-panel name="project_members">
|
||||||
|
<div class="violet-card q-pa-md">
|
||||||
|
<div class="q-gutter-sm q-mb-sm row items-center justify-between">
|
||||||
|
<q-btn
|
||||||
|
label="Привязать участника к проекту"
|
||||||
|
color="primary"
|
||||||
|
@click="createHandler"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<q-table
|
||||||
|
title="Участники проектов"
|
||||||
|
:rows="projectMembers"
|
||||||
|
:columns="projectMemberColumns"
|
||||||
|
row-key="id"
|
||||||
|
@row-click="onRowClick"
|
||||||
|
:loading="loadingProjectMembers"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
</q-tab-panels>
|
</q-tab-panels>
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
|
|
||||||
@ -121,7 +151,7 @@
|
|||||||
Редактирование команды {{ dialogData.title || '' }}
|
Редактирование команды {{ dialogData.title || '' }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="dialogType === 'profiles'">
|
<template v-else-if="dialogType === 'profiles'">
|
||||||
Редактирование пользователя {{ dialogData.name || dialogData.login || '' }}
|
Редактирование пользователя {{ `${dialogData.first_name} ${dialogData.last_name}` }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="dialogType === 'projects'">
|
<template v-else-if="dialogType === 'projects'">
|
||||||
Редактирование проекта {{ dialogData.title || '' }}
|
Редактирование проекта {{ dialogData.title || '' }}
|
||||||
@ -129,6 +159,9 @@
|
|||||||
<template v-else-if="dialogType === 'contests'">
|
<template v-else-if="dialogType === 'contests'">
|
||||||
Редактирование конкурса {{ dialogData.title || '' }}
|
Редактирование конкурса {{ dialogData.title || '' }}
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="dialogType === 'project_members'">
|
||||||
|
Привязка участника к проекту
|
||||||
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
Редактирование {{ dialogType }}
|
Редактирование {{ dialogType }}
|
||||||
</template>
|
</template>
|
||||||
@ -141,9 +174,59 @@
|
|||||||
<template v-if="dialogType === 'teams'">
|
<template v-if="dialogType === 'teams'">
|
||||||
<q-input v-model="dialogData.title" label="Название команды" dense autofocus clearable />
|
<q-input v-model="dialogData.title" label="Название команды" dense autofocus clearable />
|
||||||
<q-input v-model="dialogData.description" label="Описание" dense clearable type="textarea" class="q-mt-sm" />
|
<q-input v-model="dialogData.description" label="Описание" dense clearable type="textarea" class="q-mt-sm" />
|
||||||
<q-input v-model="dialogData.logo" label="Логотип" dense clearable class="q-mt-sm" />
|
|
||||||
<q-input v-model="dialogData.git_url" label="Git URL" dense clearable class="q-mt-sm" />
|
<q-input v-model="dialogData.git_url" label="Git URL" dense clearable class="q-mt-sm" />
|
||||||
<q-toggle v-model="dialogData.is_active" label="Активна" dense class="q-mt-sm" />
|
<q-toggle
|
||||||
|
v-model="dialogData.is_active"
|
||||||
|
label="Активна"
|
||||||
|
dense
|
||||||
|
class="q-mt-sm"
|
||||||
|
@update:model-value="handleTeamIsActiveToggle"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-separator class="q-my-md" />
|
||||||
|
<div class="text-h6 q-mb-sm">Логотип команды</div>
|
||||||
|
|
||||||
|
<div v-if="loadingTeamLogo" class="text-center q-py-md">
|
||||||
|
<q-spinner-dots color="primary" size="2em" />
|
||||||
|
<div>Загрузка логотипа...</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!dialogData.logoUrl" class="text-center q-py-md text-grey-7">
|
||||||
|
Логотип не выбран.
|
||||||
|
</div>
|
||||||
|
<div v-else class="q-gutter-md q-mb-md row wrap justify-center">
|
||||||
|
<q-card class="col-auto" style="width: 120px; height: 120px;">
|
||||||
|
<q-img
|
||||||
|
:src="dialogData.logoUrl"
|
||||||
|
alt="Team Logo"
|
||||||
|
style="width: 100%; height: 100%; object-fit: cover;"
|
||||||
|
/>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-file
|
||||||
|
v-model="newTeamLogoFile"
|
||||||
|
label="Выберите новый логотип"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
accept="image/*"
|
||||||
|
@update:model-value="handleNewTeamLogoSelected"
|
||||||
|
class="q-mt-sm"
|
||||||
|
:rules="dialogData.id ? [] : [val => !!val || 'Логотип обязателен']"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon v-if="newTeamLogoFile" name="check" color="positive" />
|
||||||
|
<q-icon name="photo" />
|
||||||
|
</template>
|
||||||
|
</q-file>
|
||||||
|
<q-btn
|
||||||
|
v-if="newTeamLogoFile"
|
||||||
|
label="Загрузить новый логотип"
|
||||||
|
color="primary"
|
||||||
|
class="q-mt-sm full-width"
|
||||||
|
@click="uploadNewTeamLogo"
|
||||||
|
:loading="uploadingLogo"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="dialogType === 'profiles'">
|
<template v-else-if="dialogType === 'profiles'">
|
||||||
@ -153,8 +236,27 @@
|
|||||||
<q-input v-model="dialogData.birthday" label="День рождения" dense clearable class="q-mt-sm" type="date" />
|
<q-input v-model="dialogData.birthday" label="День рождения" dense clearable class="q-mt-sm" type="date" />
|
||||||
<q-input v-model="dialogData.email" label="Почта" dense clearable class="q-mt-sm" />
|
<q-input v-model="dialogData.email" label="Почта" dense clearable class="q-mt-sm" />
|
||||||
<q-input v-model="dialogData.phone" label="Телефон" dense clearable class="q-mt-sm" />
|
<q-input v-model="dialogData.phone" label="Телефон" dense clearable class="q-mt-sm" />
|
||||||
<q-input v-model="dialogData.role_id" label="Роль" dense clearable class="q-mt-sm" />
|
<q-select
|
||||||
<q-input v-model="dialogData.team_id" label="Команда" dense clearable class="q-mt-sm" />
|
v-model="dialogData.role_id"
|
||||||
|
label="Роль"
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
class="q-mt-sm"
|
||||||
|
:options="[{ label: 'Администратор', value: 1 }, { label: 'Участник', value: 2 }]" emit-value
|
||||||
|
map-options
|
||||||
|
/>
|
||||||
|
<q-select
|
||||||
|
v-model="dialogData.team_id"
|
||||||
|
label="Команда"
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
class="q-mt-sm"
|
||||||
|
:options="teams"
|
||||||
|
option-value="id"
|
||||||
|
option-label="title"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
/>
|
||||||
|
|
||||||
<q-separator class="q-my-md" />
|
<q-separator class="q-my-md" />
|
||||||
<div class="text-h6 q-mb-sm">Фотографии профиля</div>
|
<div class="text-h6 q-mb-sm">Фотографии профиля</div>
|
||||||
@ -286,8 +388,27 @@
|
|||||||
<q-input v-model="dialogData.web_url" label="URL сайта" dense clearable class="q-mt-sm" />
|
<q-input v-model="dialogData.web_url" label="URL сайта" dense clearable class="q-mt-sm" />
|
||||||
<q-input v-model="dialogData.results" label="Результаты" dense clearable class="q-mt-sm" />
|
<q-input v-model="dialogData.results" label="Результаты" dense clearable class="q-mt-sm" />
|
||||||
<q-toggle v-model="dialogData.is_win" label="Победа (Да/Нет)" dense class="q-mt-sm" />
|
<q-toggle v-model="dialogData.is_win" label="Победа (Да/Нет)" dense class="q-mt-sm" />
|
||||||
<q-input v-model="dialogData.project_id" label="Проект" dense clearable class="q-mt-sm" />
|
<q-select
|
||||||
<q-input v-model="dialogData.status_id" label="Статус" dense clearable class="q-mt-sm" />
|
v-model="dialogData.project_id"
|
||||||
|
label="Проект"
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
class="q-mt-sm"
|
||||||
|
:options="allProjects"
|
||||||
|
option-value="id"
|
||||||
|
option-label="title"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
/>
|
||||||
|
<q-select
|
||||||
|
v-model="dialogData.status_id"
|
||||||
|
label="Статус"
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
class="q-mt-sm"
|
||||||
|
:options="[{ label: 'Завершен', value: 1 }, { label: 'В процессе', value: 2 }, { label: 'Ожидает начала', value: 3 }]" emit-value
|
||||||
|
map-options
|
||||||
|
/>
|
||||||
|
|
||||||
<q-separator class="q-my-md" />
|
<q-separator class="q-my-md" />
|
||||||
<div class="text-h6 q-mb-sm">Фотографии карусели конкурса</div>
|
<div class="text-h6 q-mb-sm">Фотографии карусели конкурса</div>
|
||||||
@ -407,6 +528,34 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="dialogType === 'project_members'">
|
||||||
|
<q-input v-model="dialogData.description" label="Описание" dense autofocus clearable type="textarea" />
|
||||||
|
<q-select
|
||||||
|
v-model="dialogData.project_id"
|
||||||
|
label="Проект"
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
class="q-mt-sm"
|
||||||
|
:options="allProjects"
|
||||||
|
option-value="id"
|
||||||
|
option-label="title"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
/>
|
||||||
|
<q-select
|
||||||
|
v-model="dialogData.profile_id"
|
||||||
|
label="Профиль"
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
class="q-mt-sm"
|
||||||
|
:options="allProfiles"
|
||||||
|
option-value="id"
|
||||||
|
:option-label="item => `${item.first_name} ${item.last_name}`"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<pre>{{ dialogData }}</pre>
|
<pre>{{ dialogData }}</pre>
|
||||||
</template>
|
</template>
|
||||||
@ -441,10 +590,17 @@
|
|||||||
color="negative"
|
color="negative"
|
||||||
@click="deleteItem"
|
@click="deleteItem"
|
||||||
/>
|
/>
|
||||||
|
<q-btn
|
||||||
|
v-if="dialogType === 'project_members' && dialogData.id"
|
||||||
|
flat
|
||||||
|
label="Удалить"
|
||||||
|
color="negative"
|
||||||
|
@click="deleteItem"
|
||||||
|
/>
|
||||||
<q-space />
|
<q-space />
|
||||||
<q-btn flat label="Закрыть" color="primary" @click="closeDialog" />
|
<q-btn flat label="Закрыть" color="primary" @click="closeDialog" />
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="['teams', 'profiles', 'projects', 'contests'].includes(dialogType)"
|
v-if="['teams', 'profiles', 'projects', 'contests', 'project_members'].includes(dialogType)"
|
||||||
flat
|
flat
|
||||||
label="Сохранить"
|
label="Сохранить"
|
||||||
color="primary"
|
color="primary"
|
||||||
@ -454,6 +610,122 @@
|
|||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
|
<q-dialog v-model="createUserDialogVisible" persistent>
|
||||||
|
<q-card style="min-width: 350px; max-width: 700px;">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">
|
||||||
|
Создание нового пользователя
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<q-card-section class="q-pt-none">
|
||||||
|
<q-input v-model="newUserData.first_name" label="Имя" dense autofocus clearable class="q-mt-md" />
|
||||||
|
<q-input v-model="newUserData.last_name" label="Фамилия" dense clearable class="q-mt-sm" />
|
||||||
|
<q-input v-model="newUserData.patronymic" label="Отчество" dense clearable class="q-mt-sm" />
|
||||||
|
<q-input v-model="newUserData.birthday" label="День рождения" dense clearable class="q-mt-sm" type="date" />
|
||||||
|
<q-input v-model="newUserData.email" label="Почта" dense clearable class="q-mt-sm" />
|
||||||
|
<q-input v-model="newUserData.phone" label="Телефон" dense clearable class="q-mt-sm" />
|
||||||
|
|
||||||
|
<q-select
|
||||||
|
v-model="newUserData.role_id"
|
||||||
|
label="Роль"
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
class="q-mt-sm"
|
||||||
|
:options="[{ label: 'Администратор', value: 1 }, { label: 'Участник', value: 2 }]"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
/>
|
||||||
|
<q-select
|
||||||
|
v-model="newUserData.team_id"
|
||||||
|
label="Команда"
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
class="q-mt-sm"
|
||||||
|
:options="teams"
|
||||||
|
option-value="id"
|
||||||
|
option-label="title"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-input v-model="newUserData.login" label="Логин" dense clearable class="q-mt-sm" />
|
||||||
|
<q-input
|
||||||
|
v-model="newUserData.password"
|
||||||
|
label="Пароль"
|
||||||
|
type="password"
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
class="q-mt-sm"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model="confirmNewUserPassword"
|
||||||
|
label="Подтвердите пароль"
|
||||||
|
type="password"
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
class="q-mt-sm"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn flat label="Отмена" color="primary" @click="createUserDialogVisible = false" />
|
||||||
|
<q-btn flat label="Создать" color="primary" @click="saveNewUser" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
|
<q-dialog v-model="changePasswordDialogVisible" persistent>
|
||||||
|
<q-card style="min-width: 350px">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">
|
||||||
|
Смена пароля для пользователя
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<q-card-section class="q-pt-none">
|
||||||
|
<q-select
|
||||||
|
v-model="selectedUserForPasswordChange"
|
||||||
|
label="Выберите пользователя"
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
class="q-mt-md"
|
||||||
|
:options="allProfiles"
|
||||||
|
option-value="id"
|
||||||
|
:option-label="item => `${item.first_name} ${item.last_name} (${item.email})`"
|
||||||
|
options-dense
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-model="newPasswordField"
|
||||||
|
label="Новый пароль"
|
||||||
|
type="password"
|
||||||
|
dense
|
||||||
|
autofocus
|
||||||
|
clearable
|
||||||
|
class="q-mt-md"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model="confirmPasswordField"
|
||||||
|
label="Подтвердите пароль"
|
||||||
|
type="password"
|
||||||
|
dense
|
||||||
|
clearable
|
||||||
|
class="q-mt-sm"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn flat label="Отмена" color="primary" @click="changePasswordDialogVisible = false" />
|
||||||
|
<q-btn flat label="Сохранить пароль" color="primary" @click="saveNewPassword" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
:icon="'logout'"
|
:icon="'logout'"
|
||||||
class="fixed-bottom-right q-ma-md"
|
class="fixed-bottom-right q-ma-md"
|
||||||
@ -465,18 +737,19 @@
|
|||||||
</q-layout>
|
</q-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { Notify, useQuasar } from 'quasar'
|
import { Notify, useQuasar } from 'quasar'
|
||||||
|
|
||||||
import registerUser from '@/api/users/registerUser.js'
|
import registerUser from '@/api/users/registerUser.js'
|
||||||
import changePassword from '@/api/users/changeUserPassword.js'
|
import changeUserPassword from '@/api/users/changeUserPassword.js'
|
||||||
|
|
||||||
import fetchTeams from '@/api/teams/getTeams.js'
|
import fetchTeams from '@/api/teams/getTeams.js'
|
||||||
import updateTeam from '@/api/teams/updateTeam.js'
|
import updateTeam from '@/api/teams/updateTeam.js'
|
||||||
import deleteTeamById from '@/api/teams/deleteTeam.js'
|
import deleteTeamById from '@/api/teams/deleteTeam.js'
|
||||||
import createTeam from '@/api/teams/createTeam.js'
|
import createTeam from '@/api/teams/createTeam.js'
|
||||||
|
import setActiveTeam from '@/api/teams/setActiveTeam.js'
|
||||||
|
import uploadTeamLogo from '@/api/teams/uploadTeamPhoto.js'
|
||||||
|
|
||||||
import fetchProjects from '@/api/projects/getProjects.js'
|
import fetchProjects from '@/api/projects/getProjects.js'
|
||||||
import updateProject from '@/api/projects/updateProject.js'
|
import updateProject from '@/api/projects/updateProject.js'
|
||||||
@ -513,6 +786,15 @@ import uploadProjectFile from '@/api/projects/project_files/uploadProjectFile.js
|
|||||||
import deleteProjectFile from '@/api/projects/project_files/deleteProjectFile.js'
|
import deleteProjectFile from '@/api/projects/project_files/deleteProjectFile.js'
|
||||||
import downloadProjectFile from '@/api/projects/project_files/downloadProjectFile.js'
|
import downloadProjectFile from '@/api/projects/project_files/downloadProjectFile.js'
|
||||||
|
|
||||||
|
import getProjectMemberByProject from "@/api/project_members/getProjectMemberByProject.js";
|
||||||
|
import getProjectMemberByProfile from "@/api/project_members/getProjectMembersByProfile.js";
|
||||||
|
import createProjectMember from "@/api/project_members/createProjectMember.js";
|
||||||
|
import deleteProjectMember from "@/api/project_members/deleteProjectMember.js";
|
||||||
|
import updateProjectMember from "@/api/project_members/updateProjectMember.js";
|
||||||
|
import getAllProjectMembers from "@/api/project_members/getAllProjectMembers.js";
|
||||||
|
import deleteSingleProjectMember from "@/api/project_members/deleteSingleProjectMember.js"; // НОВЫЙ ИМПОРТ
|
||||||
|
|
||||||
|
|
||||||
import router from "@/router/index.js";
|
import router from "@/router/index.js";
|
||||||
import CONFIG from '@/core/config.js'
|
import CONFIG from '@/core/config.js'
|
||||||
|
|
||||||
@ -520,6 +802,21 @@ const $q = useQuasar()
|
|||||||
|
|
||||||
const tab = ref('profiles')
|
const tab = ref('profiles')
|
||||||
|
|
||||||
|
const createUserDialogVisible = ref(false);
|
||||||
|
const newUserData = ref({ // Модель для новых данных пользователя
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
patronymic: '',
|
||||||
|
birthday: null, // Используйте null для даты
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
role_id: null, // null для q-select, если по умолчанию не выбрано
|
||||||
|
team_id: null, // null для q-select
|
||||||
|
login: '',
|
||||||
|
password: '',
|
||||||
|
});
|
||||||
|
const confirmNewUserPassword = ref('');
|
||||||
|
|
||||||
const profiles = ref([])
|
const profiles = ref([])
|
||||||
const loadingProfiles = ref(false)
|
const loadingProfiles = ref(false)
|
||||||
const profileColumns = [
|
const profileColumns = [
|
||||||
@ -543,6 +840,10 @@ const teamColumns = [
|
|||||||
{ name: 'is_active', label: 'Активна', field: 'is_active', sortable: true },
|
{ name: 'is_active', label: 'Активна', field: 'is_active', sortable: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const newTeamLogoFile = ref(null);
|
||||||
|
const loadingTeamLogo = ref(false);
|
||||||
|
const uploadingLogo = ref(false);
|
||||||
|
|
||||||
const projects = ref([])
|
const projects = ref([])
|
||||||
const loadingProjects = ref(false)
|
const loadingProjects = ref(false)
|
||||||
const projectColumns = [
|
const projectColumns = [
|
||||||
@ -557,17 +858,53 @@ const contestColumns = [
|
|||||||
{ name: 'title', label: 'Название конкурса', field: 'title', sortable: true },
|
{ name: 'title', label: 'Название конкурса', field: 'title', sortable: true },
|
||||||
{ name: 'description', label: 'Описание', field: 'description', sortable: true },
|
{ name: 'description', label: 'Описание', field: 'description', sortable: true },
|
||||||
{ name: 'web_url', label: 'URL сайта', field: 'web_url', sortable: true },
|
{ name: 'web_url', label: 'URL сайта', field: 'web_url', sortable: true },
|
||||||
{ name: 'photo', label: 'Фото', field: 'photo', sortable: true },
|
|
||||||
{ name: 'results', label: 'Результаты', field: 'results', sortable: true },
|
{ name: 'results', label: 'Результаты', field: 'results', sortable: true },
|
||||||
{ name: 'is_win', label: 'Победа', field: 'is_win', sortable: true },
|
{ name: 'is_win', label: 'Победа', field: 'is_win', sortable: true },
|
||||||
{ name: 'project_id', label: 'Проект', field: 'project_id', sortable: true },
|
{ name: 'project_id', label: 'Проект', field: 'project_id', sortable: true },
|
||||||
{ name: 'status_id', label: 'Статус', field: 'status_id', sortable: true },
|
{ name: 'status_id', label: 'Статус', field: 'status_id', sortable: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const projectMembers = ref([])
|
||||||
|
const loadingProjectMembers = ref(false)
|
||||||
|
const projectMemberColumns = [
|
||||||
|
{ name: 'description', label: 'Описание', field: 'description', sortable: true },
|
||||||
|
{
|
||||||
|
name: 'project_id',
|
||||||
|
label: 'Проект',
|
||||||
|
field: 'project_id',
|
||||||
|
sortable: true,
|
||||||
|
// Форматирование для отображения названия проекта
|
||||||
|
format: (val, row) => {
|
||||||
|
const project = allProjects.value.find(p => p.id === val);
|
||||||
|
return project ? project.title : 'Неизвестный проект';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'profile_id',
|
||||||
|
label: 'Профиль',
|
||||||
|
field: 'profile_id',
|
||||||
|
sortable: true,
|
||||||
|
// Форматирование для отображения имени профиля
|
||||||
|
format: (val, row) => {
|
||||||
|
const profile = allProfiles.value.find(p => p.id === val);
|
||||||
|
return profile ? `${profile.first_name} ${profile.last_name}` : 'Неизвестный профиль';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const allProjects = ref([]); // Для списка проектов в q-select
|
||||||
|
const allProfiles = ref([]); // Для списка профилей в q-select
|
||||||
|
|
||||||
|
const changePasswordDialogVisible = ref(false);
|
||||||
|
const selectedUserForPasswordChange = ref(null);
|
||||||
|
const newPasswordField = ref('');
|
||||||
|
const confirmPasswordField = ref('');
|
||||||
|
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const dialogData = ref({})
|
const dialogData = ref({})
|
||||||
const dialogType = ref('')
|
const dialogType = ref('')
|
||||||
|
|
||||||
|
|
||||||
const profilePhotos = ref([])
|
const profilePhotos = ref([])
|
||||||
const loadingProfilePhotos = ref(false)
|
const loadingProfilePhotos = ref(false)
|
||||||
const newProfilePhotoFile = ref(null)
|
const newProfilePhotoFile = ref(null)
|
||||||
@ -586,12 +923,15 @@ const projectFiles = ref([])
|
|||||||
const loadingProjectFiles = ref(false)
|
const loadingProjectFiles = ref(false)
|
||||||
const newProjectFile = ref(null)
|
const newProjectFile = ref(null)
|
||||||
|
|
||||||
|
// TO DO
|
||||||
|
// ДОБАВИТЬ ПРАВИЛЬНЫЙ URL
|
||||||
const getPhotoUrl = (photoId, type) => {
|
const getPhotoUrl = (photoId, type) => {
|
||||||
if (type === 'profile') {
|
if (type === 'profile') {
|
||||||
return `${CONFIG.BASE_URL}/profile_photos/${photoId}/file`;
|
return `${CONFIG.BASE_URL}/profile_photos/${photoId}/file`;
|
||||||
} else if (type === 'contest') {
|
} else if (type === 'contest') {
|
||||||
return `${CONFIG.BASE_URL}/contest_carousel_photos/${photoId}/file`;
|
return `${CONFIG.BASE_URL}/contest_carousel_photos/${photoId}/file`;
|
||||||
|
} else if (type === 'teams') {
|
||||||
|
return `${CONFIG.BASE_URL}//${photoId}/file`;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@ -676,6 +1016,9 @@ function handleNewProjectFileSelected(file) {
|
|||||||
newProjectFile.value = file;
|
newProjectFile.value = file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleNewTeamLogoSelected(file) {
|
||||||
|
newTeamLogoFile.value = file;
|
||||||
|
}
|
||||||
|
|
||||||
async function uploadNewProfilePhoto() {
|
async function uploadNewProfilePhoto() {
|
||||||
if (!newProfilePhotoFile.value || !dialogData.value.id) {
|
if (!newProfilePhotoFile.value || !dialogData.value.id) {
|
||||||
@ -708,6 +1051,40 @@ async function uploadNewProfilePhoto() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function uploadNewTeamLogo() {
|
||||||
|
if (!newTeamLogoFile.value || !dialogData.value.id) {
|
||||||
|
Notify.create({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Выберите файл логотипа и убедитесь, что команда выбрана.',
|
||||||
|
icon: 'warning',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadingLogo.value = true; // Используем существующий индикатор
|
||||||
|
try {
|
||||||
|
const uploadedLogo = await uploadTeamLogo(dialogData.value.id, newTeamLogoFile.value);
|
||||||
|
|
||||||
|
dialogData.value.logoUrl = `${CONFIG.BASE_URL}/teams/${dialogData.value.id}/logo`;
|
||||||
|
|
||||||
|
newTeamLogoFile.value = null; // Очищаем выбранный файл
|
||||||
|
Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Логотип команды успешно загружен!',
|
||||||
|
icon: 'check_circle',
|
||||||
|
});
|
||||||
|
await loadData();
|
||||||
|
} catch (error) {
|
||||||
|
Notify.create({
|
||||||
|
type: 'negative',
|
||||||
|
message: `Ошибка загрузки логотипа команды: ${error.message}`,
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
uploadingLogo.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function uploadNewContestPhoto() {
|
async function uploadNewContestPhoto() {
|
||||||
if (!newContestPhotoFile.value || !dialogData.value.id) {
|
if (!newContestPhotoFile.value || !dialogData.value.id) {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
@ -959,25 +1336,39 @@ async function downloadExistingProjectFile(fileId) {
|
|||||||
|
|
||||||
|
|
||||||
function openEdit(type, row) {
|
function openEdit(type, row) {
|
||||||
dialogType.value = type
|
dialogType.value = type;
|
||||||
if (row) {
|
if (row) {
|
||||||
dialogData.value = JSON.parse(JSON.stringify(row))
|
dialogData.value = JSON.parse(JSON.stringify(row));
|
||||||
|
if (type === 'teams' && dialogData.value.logo) {
|
||||||
|
dialogData.value.logoUrl = `${CONFIG.BASE_URL}/teams/${dialogData.value.id}/logo`;
|
||||||
|
} else if (type === 'teams') {
|
||||||
|
dialogData.value.logoUrl = null;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (type === 'teams') {
|
if (type === 'teams') {
|
||||||
dialogData.value = { title: '', description: '', logo: '', git_url: '', is_active: '' }
|
dialogData.value = {
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
logo: '',
|
||||||
|
logoUrl: null,
|
||||||
|
git_url: '',
|
||||||
|
is_active: true,
|
||||||
|
};
|
||||||
} else if (type === 'projects') {
|
} else if (type === 'projects') {
|
||||||
dialogData.value = { title: '', description: '', repository_url: '' }
|
dialogData.value = { title: '', description: '', repository_url: '' };
|
||||||
projectFiles.value = [];
|
projectFiles.value = [];
|
||||||
} else if (type === 'profiles') {
|
} else if (type === 'profiles') {
|
||||||
dialogData.value = { first_name: '', last_name: '', patronymic: '', birthday: '', email: '', phone: '', role_id: null, team_id: null }
|
dialogData.value = { first_name: '', last_name: '', patronymic: '', birthday: '', email: '', phone: '', role_id: null, team_id: null };
|
||||||
profilePhotos.value = [];
|
profilePhotos.value = [];
|
||||||
} else if (type === 'contests') {
|
} else if (type === 'contests') {
|
||||||
dialogData.value = { title: '', description: '', web_url: '', photo: '', results: '', is_win: false, project_id: null, status_id: null }
|
dialogData.value = { title: '', description: '', web_url: '', photo: '', results: '', is_win: false, project_id: null, status_id: null };
|
||||||
contestPhotos.value = [];
|
contestPhotos.value = [];
|
||||||
contestFiles.value = [];
|
contestFiles.value = [];
|
||||||
|
} else if (type === 'project_members') {
|
||||||
|
dialogData.value = { description: '', project_id: null, profile_id: null };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true;
|
||||||
|
|
||||||
if (type === 'profiles' && dialogData.value.id) {
|
if (type === 'profiles' && dialogData.value.id) {
|
||||||
loadProfilePhotos(dialogData.value.id);
|
loadProfilePhotos(dialogData.value.id);
|
||||||
@ -986,7 +1377,24 @@ function openEdit(type, row) {
|
|||||||
loadContestFiles(dialogData.value.id);
|
loadContestFiles(dialogData.value.id);
|
||||||
} else if (type === 'projects' && dialogData.value.id) {
|
} else if (type === 'projects' && dialogData.value.id) {
|
||||||
loadProjectFiles(dialogData.value.id);
|
loadProjectFiles(dialogData.value.id);
|
||||||
} else {
|
} else if (type === 'project_members' || type === 'profiles' || type === 'contests') {
|
||||||
|
if (allProjects.value.length === 0) {
|
||||||
|
fetchProjects().then(data => {
|
||||||
|
allProjects.value = data || [];
|
||||||
|
}).catch(error => {
|
||||||
|
Notify.create({ type: 'negative', message: `Ошибка загрузки проектов для списка: ${error.message}` });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (allProfiles.value.length === 0) {
|
||||||
|
fetchProfiles().then(data => {
|
||||||
|
allProfiles.value = data || [];
|
||||||
|
}).catch(error => {
|
||||||
|
Notify.create({ type: 'negative', message: `Ошибка загрузки профилей для списка: ${error.message}` });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
profilePhotos.value = [];
|
profilePhotos.value = [];
|
||||||
contestPhotos.value = [];
|
contestPhotos.value = [];
|
||||||
contestFiles.value = [];
|
contestFiles.value = [];
|
||||||
@ -1016,12 +1424,13 @@ async function saveChanges() {
|
|||||||
try {
|
try {
|
||||||
if (dialogType.value === 'teams') {
|
if (dialogType.value === 'teams') {
|
||||||
if (dialogData.value.id) {
|
if (dialogData.value.id) {
|
||||||
await updateTeam(dialogData.value)
|
await updateTeam(dialogData.value);
|
||||||
const idx = teams.value.findIndex(t => t.id === dialogData.value.id)
|
const idx = teams.value.findIndex(t => t.id === dialogData.value.id);
|
||||||
if (idx !== -1) teams.value[idx] = JSON.parse(JSON.stringify(dialogData.value))
|
if (idx !== -1) teams.value[idx] = JSON.parse(JSON.stringify(dialogData.value));
|
||||||
} else {
|
} else {
|
||||||
const newTeam = await createTeam(dialogData.value)
|
const newTeamData = { ...dialogData.value, is_active: false };
|
||||||
teams.value.push(newTeam)
|
const newTeam = await createTeam(newTeamData);
|
||||||
|
teams.value.push(newTeam);
|
||||||
}
|
}
|
||||||
} else if (dialogType.value === 'projects') {
|
} else if (dialogType.value === 'projects') {
|
||||||
if (dialogData.value.id) {
|
if (dialogData.value.id) {
|
||||||
@ -1050,6 +1459,54 @@ async function saveChanges() {
|
|||||||
const newContest = await createContest(dialogData.value)
|
const newContest = await createContest(dialogData.value)
|
||||||
contests.value.push(newContest)
|
contests.value.push(newContest)
|
||||||
}
|
}
|
||||||
|
} else if (dialogType.value === 'project_members') {
|
||||||
|
const memberData = {
|
||||||
|
id: dialogData.value.id,
|
||||||
|
description: dialogData.value.description,
|
||||||
|
project_id: parseInt(dialogData.value.project_id),
|
||||||
|
profile_id: parseInt(dialogData.value.profile_id)
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDuplicate = projectMembers.value.some(pm => {
|
||||||
|
if (pm.id === memberData.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return pm.project_id === memberData.project_id && pm.profile_id === memberData.profile_id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isDuplicate) {
|
||||||
|
Notify.create({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Этот профиль уже является участником данного проекта.',
|
||||||
|
icon: 'warning',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dialogData.value.id) {
|
||||||
|
if (!memberData.project_id) {
|
||||||
|
Notify.create({ type: 'negative', message: 'Невозможно обновить: ID проекта для участника отсутствует.', icon: 'error' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentMembersForProject = await getProjectMemberByProject(memberData.project_id) || [];
|
||||||
|
const updatedMembersList = currentMembersForProject.map(member => {
|
||||||
|
if (member.id === memberData.id) {
|
||||||
|
return { ...member, ...memberData };
|
||||||
|
}
|
||||||
|
return member;
|
||||||
|
});
|
||||||
|
await updateProjectMember(memberData.project_id, updatedMembersList);
|
||||||
|
await loadData('project_members');
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const newMemberPayload = {
|
||||||
|
description: memberData.description,
|
||||||
|
project_id: memberData.project_id,
|
||||||
|
profile_id: memberData.profile_id
|
||||||
|
};
|
||||||
|
|
||||||
|
const newProjectMemberResponse = await createProjectMember(newMemberPayload);
|
||||||
|
projectMembers.value.push(newProjectMemberResponse[0] || newProjectMemberResponse);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
closeDialog()
|
closeDialog()
|
||||||
Notify.create({
|
Notify.create({
|
||||||
@ -1067,6 +1524,170 @@ async function saveChanges() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleTeamIsActiveToggle(newValue) {
|
||||||
|
// Проверяем, что это команда, она существует (есть ID) и флажок активен
|
||||||
|
if (dialogType.value === 'teams' && dialogData.value.id && newValue) {
|
||||||
|
try {
|
||||||
|
await setActiveTeam(dialogData.value.id);
|
||||||
|
Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: `Команда "${dialogData.value.title}" успешно установлена как активная.`,
|
||||||
|
});
|
||||||
|
await loadData('teams'); // Вызываем loadData для обновления списка команд
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Ошибка при установке активной команды:", error);
|
||||||
|
Notify.create({
|
||||||
|
type: 'negative',
|
||||||
|
message: `Ошибка при установке активной команды: ${error.message}`,
|
||||||
|
});
|
||||||
|
dialogData.value.is_active = !newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveNewUser() {
|
||||||
|
// Валидация входных данных
|
||||||
|
if (!newUserData.value.first_name || !newUserData.value.last_name || !newUserData.value.email || !newUserData.value.login || !newUserData.value.password || !newUserData.value.role_id) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'Пожалуйста, заполните все обязательные поля (Имя, Фамилия, Почта, Логин, Пароль, Роль).',
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationError = validatePassword(newUserData.value.password);
|
||||||
|
if (validationError) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: `Ошибка пароля: ${validationError}`,
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newUserData.value.password !== confirmNewUserPassword.value) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'Пароли не совпадают.',
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dataToSend = { ...newUserData.value };
|
||||||
|
if (dataToSend.birthday) {
|
||||||
|
if (typeof dataToSend.birthday === 'string' && dataToSend.birthday.includes('/')) {
|
||||||
|
dataToSend.birthday = dataToSend.birthday.replace(/\//g, '-');
|
||||||
|
} else if (dataToSend.birthday instanceof Date) {
|
||||||
|
dataToSend.birthday = dataToSend.birthday.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dataToSend.birthday = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await registerUser(dataToSend);
|
||||||
|
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Пользователь успешно создан!',
|
||||||
|
icon: 'check_circle',
|
||||||
|
});
|
||||||
|
createUserDialogVisible.value = false;
|
||||||
|
await loadData('profiles'); // Обновляем список пользователей
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при создании пользователя:', error.message);
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: `Ошибка при создании пользователя: ${error.message}`,
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function changePasswordHandler() {
|
||||||
|
selectedUserForPasswordChange.value = null;
|
||||||
|
newPasswordField.value = '';
|
||||||
|
confirmPasswordField.value = '';
|
||||||
|
changePasswordDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function validatePassword(password) {
|
||||||
|
if (password.length < 8) {
|
||||||
|
return 'Пароль должен быть не менее 8 символов.';
|
||||||
|
}
|
||||||
|
if (!/[A-Z]/.test(password)) { // Проверка на заглавные буквы
|
||||||
|
return 'Пароль должен содержать хотя бы одну заглавную букву.';
|
||||||
|
}
|
||||||
|
if (!/[a-z]/.test(password)) { // Проверка на строчные буквы
|
||||||
|
return 'Пароль должен содержать хотя бы одну строчную букву.';
|
||||||
|
}
|
||||||
|
if (!/\d/.test(password)) { // Проверка на цифры
|
||||||
|
return 'Пароль должен содержать хотя бы одну цифру.';
|
||||||
|
}
|
||||||
|
if (!/[!@#$%^&*()_+]/.test(password)) { // Проверка на спецсимволы
|
||||||
|
return 'Пароль должен содержать хотя бы один специальный символ (!@#$%^&*()_+).';
|
||||||
|
}
|
||||||
|
if (!/[a-zA-Z]/.test(password)) { // Проверка на наличие хотя бы одной буквы (латиницы)
|
||||||
|
return 'Пароль должен содержать хотя бы одну букву (латиницу).';
|
||||||
|
}
|
||||||
|
return ''; // Пустая строка означает, что валидация прошла успешно
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function saveNewPassword() {
|
||||||
|
if (!selectedUserForPasswordChange.value) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'Пожалуйста, выберите пользователя.',
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationError = validatePassword(newPasswordField.value);
|
||||||
|
if (validationError) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: validationError,
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPasswordField.value !== confirmPasswordField.value) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'Пароли не совпадают.',
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await changeUserPassword(selectedUserForPasswordChange.value.id, newPasswordField.value);
|
||||||
|
$q.notify({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Пароль успешно изменен!',
|
||||||
|
icon: 'check_circle',
|
||||||
|
});
|
||||||
|
changePasswordDialogVisible.value = false;
|
||||||
|
selectedUserForPasswordChange.value = null;
|
||||||
|
newPasswordField.value = '';
|
||||||
|
confirmPasswordField.value = '';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при смене пароля:', error.message);
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: `Ошибка при смене пароля: ${error.message}`,
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadData(name) {
|
async function loadData(name) {
|
||||||
if (name === 'teams') {
|
if (name === 'teams') {
|
||||||
loadingTeams.value = true
|
loadingTeams.value = true
|
||||||
@ -1100,6 +1721,7 @@ async function loadData(name) {
|
|||||||
loadingProfiles.value = true
|
loadingProfiles.value = true
|
||||||
try {
|
try {
|
||||||
profiles.value = await fetchProfiles() || []
|
profiles.value = await fetchProfiles() || []
|
||||||
|
allProfiles.value = await fetchProfiles() || []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
profiles.value = []
|
profiles.value = []
|
||||||
Notify.create({
|
Notify.create({
|
||||||
@ -1124,6 +1746,27 @@ async function loadData(name) {
|
|||||||
} finally {
|
} finally {
|
||||||
loadingContests.value = false
|
loadingContests.value = false
|
||||||
}
|
}
|
||||||
|
} else if (name === 'project_members') {
|
||||||
|
loadingProjectMembers.value = true;
|
||||||
|
try {
|
||||||
|
projectMembers.value = await getAllProjectMembers() || [];
|
||||||
|
|
||||||
|
// Эти запросы нужны для корректного отображения названий в таблице и q-select
|
||||||
|
allProjects.value = await fetchProjects() || [];
|
||||||
|
allProfiles.value = await fetchProfiles() || [];
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
projectMembers.value = [];
|
||||||
|
allProjects.value = [];
|
||||||
|
allProfiles.value = [];
|
||||||
|
Notify.create({
|
||||||
|
type: 'negative',
|
||||||
|
message: `Ошибка загрузки участников проектов или связанных данных: ${error.message}`,
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
loadingProjectMembers.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1131,7 +1774,12 @@ async function deleteItem() {
|
|||||||
if (!dialogData.value.id) return
|
if (!dialogData.value.id) return
|
||||||
$q.dialog({
|
$q.dialog({
|
||||||
title: 'Подтверждение удаления',
|
title: 'Подтверждение удаления',
|
||||||
message: `Вы уверены, что хотите удалить ${dialogType.value === 'profiles' ? 'пользователя' : dialogType.value}?`,
|
message: `Вы уверены, что хотите удалить
|
||||||
|
${dialogType.value === 'profiles' ? 'пользователя' : dialogType.value
|
||||||
|
=== 'project_members' ? 'участника проекта' : dialogType.value
|
||||||
|
=== 'teams' ? 'команду' : dialogType.value
|
||||||
|
=== 'projects' ? 'проект' : dialogType.value
|
||||||
|
=== 'contests' ? 'конкурс' : dialogType.value}?`,
|
||||||
cancel: true,
|
cancel: true,
|
||||||
persistent: true,
|
persistent: true,
|
||||||
ok: {
|
ok: {
|
||||||
@ -1156,6 +1804,9 @@ async function deleteItem() {
|
|||||||
} else if (dialogType.value === 'contests') {
|
} else if (dialogType.value === 'contests') {
|
||||||
await deleteContestById(dialogData.value.id)
|
await deleteContestById(dialogData.value.id)
|
||||||
contests.value = contests.value.filter(c => c.id !== dialogData.value.id)
|
contests.value = contests.value.filter(c => c.id !== dialogData.value.id)
|
||||||
|
} else if (dialogType.value === 'project_members') {
|
||||||
|
await deleteSingleProjectMember(dialogData.value.id);
|
||||||
|
projectMembers.value = projectMembers.value.filter(pm => pm.id !== dialogData.value.id);
|
||||||
}
|
}
|
||||||
closeDialog()
|
closeDialog()
|
||||||
Notify.create({
|
Notify.create({
|
||||||
@ -1174,6 +1825,38 @@ async function deleteItem() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createNewUserHandler() {
|
||||||
|
newUserData.value = {
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
patronymic: '',
|
||||||
|
birthday: null,
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
role_id: null,
|
||||||
|
team_id: null,
|
||||||
|
login: '',
|
||||||
|
password: '',
|
||||||
|
};
|
||||||
|
confirmNewUserPassword.value = '';
|
||||||
|
|
||||||
|
if (teams.value.length === 0 || allProfiles.value.length === 0) {
|
||||||
|
try {
|
||||||
|
await loadData('teams');
|
||||||
|
await loadData('profiles');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки данных для формы создания пользователя:', error);
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'Не удалось загрузить списки команд или профилей.',
|
||||||
|
icon: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createUserDialogVisible.value = true; // Открываем новый диалог
|
||||||
|
}
|
||||||
|
|
||||||
function createHandler() {
|
function createHandler() {
|
||||||
dialogData.value = {};
|
dialogData.value = {};
|
||||||
openEdit(tab.value, null)
|
openEdit(tab.value, null)
|
||||||
|
|||||||
@ -60,7 +60,7 @@
|
|||||||
<q-card-section class="q-pa-md">
|
<q-card-section class="q-pa-md">
|
||||||
<div class="text-h6 text-indigo-10 q-mb-md">Активность команды за последний год</div>
|
<div class="text-h6 text-indigo-10 q-mb-md">Активность команды за последний год</div>
|
||||||
|
|
||||||
<div class="months-row flex" style="margin-left: 40px; margin-bottom: 4px; user-select: none;">
|
<div class="months-row flex" style="margin-left: 60px; margin-bottom: 4px; user-select: none;">
|
||||||
<div
|
<div
|
||||||
v-for="(monthLabel, idx) in monthLabels"
|
v-for="(monthLabel, idx) in monthLabels"
|
||||||
:key="monthLabel"
|
:key="monthLabel"
|
||||||
@ -72,7 +72,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="activity-grid-row row no-wrap">
|
<div class="activity-grid-row row no-wrap">
|
||||||
<div class="weekdays-column column q-pr-sm" style="width: 40px; user-select: none; justify-content: space-around;">
|
<div class="weekdays-column column q-pr-sm" style="width: 30px; user-select: none; justify-content: space-around;">
|
||||||
<div
|
<div
|
||||||
v-for="(day, idx) in weekDays"
|
v-for="(day, idx) in weekDays"
|
||||||
:key="day"
|
:key="day"
|
||||||
@ -168,8 +168,21 @@ const activityData = ref([]);
|
|||||||
const dayHeight = 14;
|
const dayHeight = 14;
|
||||||
const squareSize = ref(12);
|
const squareSize = ref(12);
|
||||||
|
|
||||||
// Подписи месяцев (с июня 2024 по май 2025)
|
function getDynamicMonthLabelsShort() {
|
||||||
const monthLabels = ['июн.', 'июл.', 'авг.', 'сент.', 'окт.', 'нояб.', 'дек.', 'янв.', 'февр.', 'март', 'апр.', 'май'];
|
const monthNames = [
|
||||||
|
'янв.', 'февр.', 'март', 'апр.', 'май', 'июн.',
|
||||||
|
'июл.', 'авг.', 'сент.', 'окт.', 'нояб.', 'дек.'
|
||||||
|
];
|
||||||
|
const currentMonthIndex = new Date().getMonth();
|
||||||
|
|
||||||
|
// Создаем массив из 12 элементов, затем используем map для получения названий месяцев
|
||||||
|
return Array.from({ length: 12 }, (_, i) => {
|
||||||
|
const monthIndex = (currentMonthIndex + i) % 12;
|
||||||
|
return monthNames[monthIndex];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthLabels = getDynamicMonthLabelsShort();
|
||||||
|
|
||||||
// Дни недели (пн, ср, пт, как в Gitea)
|
// Дни недели (пн, ср, пт, как в Gitea)
|
||||||
const weekDays = ['пн', 'ср', 'пт'];
|
const weekDays = ['пн', 'ср', 'пт'];
|
||||||
@ -227,9 +240,14 @@ async function loadTeamData() {
|
|||||||
try {
|
try {
|
||||||
const teams = await fetchTeams();
|
const teams = await fetchTeams();
|
||||||
const activeTeam = teams.find(team => team.is_active === true);
|
const activeTeam = teams.find(team => team.is_active === true);
|
||||||
|
|
||||||
if (activeTeam) {
|
if (activeTeam) {
|
||||||
teamName.value = activeTeam.name;
|
teamName.value = activeTeam.title || 'Название не указано';
|
||||||
teamLogo.value = activeTeam.logo;
|
teamLogo.value = activeTeam.logo || '';
|
||||||
|
|
||||||
|
// Вы также можете сохранить другие данные активной команды, если они нужны:
|
||||||
|
// teamDescription.value = activeTeam.description || '';
|
||||||
|
// teamGitUrl.value = activeTeam.git_url || '';
|
||||||
} else {
|
} else {
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
@ -252,10 +270,17 @@ async function loadMembers() {
|
|||||||
try {
|
try {
|
||||||
const profiles = await fetchProfiles();
|
const profiles = await fetchProfiles();
|
||||||
members.value = profiles.map(profile => ({
|
members.value = profiles.map(profile => ({
|
||||||
id: profile.id,
|
id: profile.id, // ID всегда остается
|
||||||
name: profile.name || 'Без имени',
|
name: `${profile.first_name || ''} ${profile.last_name || ''}`.trim() || 'Без имени', // Объединяем имя и фамилию
|
||||||
role: profile.role || 'Участник',
|
role: profile.role_id || 'Участник', // Используем role_id
|
||||||
avatar: profile.avatar || 'https://randomuser.me/api/portraits/men/1.jpg',
|
avatar: profile.avatar || 'https://randomuser.me/api/portraits/men/1.jpg', // Аватар остался прежним
|
||||||
|
|
||||||
|
// Добавляем остальные поля, которые могут быть полезны
|
||||||
|
patronymic: profile.patronymic || '',
|
||||||
|
birthday: profile.birthday || '',
|
||||||
|
email: profile.email || '',
|
||||||
|
phone: profile.phone || '',
|
||||||
|
team_id: profile.team_id || null,
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка загрузки участников:', error);
|
console.error('Ошибка загрузки участников:', error);
|
||||||
@ -437,4 +462,7 @@ function decreaseScale() {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
position: absolute; /* Для точного позиционирования */
|
position: absolute; /* Для точного позиционирования */
|
||||||
}
|
}
|
||||||
|
.weekdays-column {
|
||||||
|
margin-top: -15px; /* Попробуйте разные значения, например, -10px, -30px, пока не найдете оптимальное */
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -53,8 +53,6 @@ const authorisation = async () => {
|
|||||||
|
|
||||||
const roleId = profileResponse.data.role_id
|
const roleId = profileResponse.data.role_id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Успешный вход!',
|
message: 'Успешный вход!',
|
||||||
@ -67,15 +65,23 @@ const authorisation = async () => {
|
|||||||
await router.push('/')
|
await router.push('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (roleId === 1) {
|
|
||||||
await router.push('/admin')
|
|
||||||
} else {
|
|
||||||
await router.push('/')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
let errorMessage = 'Ошибка входа';
|
||||||
|
if (error.response) {
|
||||||
|
if (error.response.status === 401) {
|
||||||
|
errorMessage = 'Неверное имя пользователя или пароль.';
|
||||||
|
} else if (error.response.data && error.response.data.detail) {
|
||||||
|
errorMessage = error.response.data.detail;
|
||||||
|
} else {
|
||||||
|
errorMessage = 'Произошла ошибка при попытке входа. Пожалуйста, попробуйте еще раз.';
|
||||||
|
}
|
||||||
|
} else if (error.message) {
|
||||||
|
errorMessage = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
Notify.create({
|
Notify.create({
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
message: error.message || 'Ошибка входа',
|
message: errorMessage,
|
||||||
icon: 'error'
|
icon: 'error'
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
@ -209,8 +215,6 @@ const authorisation = async () => {
|
|||||||
animation: marquee-move var(--marquee-duration) linear infinite;
|
animation: marquee-move var(--marquee-duration) linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.marquee-text {
|
.marquee-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user