сделал шаблон для участников
This commit is contained in:
parent
49cc307f2c
commit
49b88ba505
@ -10,6 +10,11 @@ class ProjectMembersRepository:
|
||||
def __init__(self, db: AsyncSession):
|
||||
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]:
|
||||
stmt = select(ProjectMember).filter_by(id=project_member_id)
|
||||
result = await self.db.execute(stmt)
|
||||
@ -43,5 +48,7 @@ class ProjectMembersRepository:
|
||||
for project_member in project_members:
|
||||
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()
|
||||
return project_members
|
||||
return project_member
|
||||
|
||||
@ -11,6 +11,19 @@ from app.infrastructure.project_members_service import ProjectMembersService
|
||||
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(
|
||||
'/by-project/{project_id}/',
|
||||
response_model=list[ProjectMemberEntity],
|
||||
@ -84,3 +97,17 @@ async def delete_project_members(
|
||||
service = ProjectMembersService(db)
|
||||
await service.delete_project_members_by_project_id(project_id)
|
||||
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(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
teams_service = TeamsService(db)
|
||||
return await teams_service.get_all_teams()
|
||||
@ -33,7 +32,7 @@ async def get_all_teams(
|
||||
summary='Get active team',
|
||||
description='Returns active team',
|
||||
)
|
||||
async def get_all_teams(
|
||||
async def get_active_team(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
teams_service = TeamsService(db)
|
||||
@ -61,7 +60,7 @@ async def create_team(
|
||||
summary='Make team active',
|
||||
description='Makes team active',
|
||||
)
|
||||
async def update_team(
|
||||
async def set_active_team(
|
||||
team_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(require_admin),
|
||||
|
||||
@ -1,15 +1,29 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, Body
|
||||
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.dependencies import get_current_user
|
||||
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
|
||||
|
||||
@router.put(
|
||||
'/{user_id}/',
|
||||
@ -17,9 +31,9 @@ router = APIRouter()
|
||||
summary='Change user password',
|
||||
description='Change user password',
|
||||
)
|
||||
async def create_user(
|
||||
async def change_user_password(
|
||||
user_id: int,
|
||||
new_password: str,
|
||||
new_password: str = Body(..., embed=True), # <--- ИЗМЕНЕНИЕ ЗДЕСЬ! Явно указывает, что в теле запроса
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
|
||||
@ -16,6 +16,13 @@ class ProjectMembersService:
|
||||
self.projects_repository = ProjectsRepository(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]:
|
||||
project_members = await self.project_members_repository.get_by_project_id(project_id)
|
||||
return [
|
||||
@ -107,6 +114,17 @@ class ProjectMembersService:
|
||||
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
|
||||
def model_to_entity(project: ProjectMember) -> 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_members_router import router as project_members_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.teams_router import router as team_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',
|
||||
tags=['project_members'])
|
||||
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(team_router, prefix=f'{settings.PREFIX}/teams', tags=['teams'])
|
||||
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 };
|
||||
} catch (error) {
|
||||
if (error.response?.status === 401) {
|
||||
throw new Error("Неверное имя пользователя или пароль");
|
||||
if (error.response) {
|
||||
if (error.response.status === 403) {
|
||||
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 CONFIG from "@/core/config.js";
|
||||
|
||||
const changeUserPassword = async (userId, newPasswordData) => {
|
||||
const changeUserPassword = async (userId, newPassword) => {
|
||||
try {
|
||||
const token = localStorage.getItem("access_token");
|
||||
const response = await axios.patch(
|
||||
`${CONFIG.BASE_URL}/users/${userId}/password`,
|
||||
newPasswordData,
|
||||
const response = await axios.put(
|
||||
`${CONFIG.BASE_URL}/users/${userId}/`,
|
||||
{ new_password: newPassword },
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
@ -19,6 +20,11 @@ const changeUserPassword = async (userId, newPasswordData) => {
|
||||
throw new Error(error.response.data.detail);
|
||||
} else if (error.response?.status === 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);
|
||||
}
|
||||
|
||||
@ -1,18 +1,23 @@
|
||||
import axios from "axios";
|
||||
import CONFIG from "@/core/config.js";
|
||||
import axios from 'axios';
|
||||
import CONFIG from '@/core/config.js';
|
||||
|
||||
const registerUser = async (userData) => {
|
||||
try {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const response = await axios.post(
|
||||
`${CONFIG.BASE_URL}/users/register`,
|
||||
userData
|
||||
`${CONFIG.BASE_URL}/users/`,
|
||||
userData,
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error.response?.status === 400) {
|
||||
throw new Error(error.response.data.detail);
|
||||
}
|
||||
throw new Error(error.message);
|
||||
throw new Error(error.response?.data?.detail || error.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -40,7 +40,8 @@ import {
|
||||
QItemLabel,
|
||||
QItem,
|
||||
QImg,
|
||||
QFile
|
||||
QFile,
|
||||
QSelect
|
||||
} from 'quasar'
|
||||
|
||||
|
||||
@ -60,7 +61,7 @@ app.use(Quasar, {
|
||||
QSeparator, QCardActions, QDialog, QIcon, QSpace,
|
||||
QAvatar, QTooltip, QBanner, QSlideTransition, QToggle,
|
||||
QList, QSpinnerDots, QCarouselSlide, QCarousel,
|
||||
QItemSection, QItemLabel, QItem, QImg, QFile
|
||||
QItemSection, QItemLabel, QItem, QImg, QFile, QSelect
|
||||
},
|
||||
directives: {
|
||||
Ripple
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
<q-tab name="teams" label="Команды" />
|
||||
<q-tab name="projects" label="Проекты" />
|
||||
<q-tab name="contests" label="Конкурсы" />
|
||||
<q-tab name="project_members" label="Участники проектов" />
|
||||
</q-tabs>
|
||||
</q-header>
|
||||
|
||||
@ -27,7 +28,14 @@
|
||||
<q-btn
|
||||
label="Создание пользователя"
|
||||
color="primary"
|
||||
@click="createHandler"
|
||||
@click="createNewUserHandler"
|
||||
/>
|
||||
<q-btn
|
||||
round
|
||||
color="primary"
|
||||
icon="cached"
|
||||
title="Поменять пароль у пользователя"
|
||||
@click="changePasswordHandler"
|
||||
/>
|
||||
</div>
|
||||
<q-table
|
||||
@ -110,6 +118,28 @@
|
||||
</div>
|
||||
</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-page-container>
|
||||
|
||||
@ -121,7 +151,7 @@
|
||||
Редактирование команды {{ dialogData.title || '' }}
|
||||
</template>
|
||||
<template v-else-if="dialogType === 'profiles'">
|
||||
Редактирование пользователя {{ dialogData.name || dialogData.login || '' }}
|
||||
Редактирование пользователя {{ `${dialogData.first_name} ${dialogData.last_name}` }}
|
||||
</template>
|
||||
<template v-else-if="dialogType === 'projects'">
|
||||
Редактирование проекта {{ dialogData.title || '' }}
|
||||
@ -129,6 +159,9 @@
|
||||
<template v-else-if="dialogType === 'contests'">
|
||||
Редактирование конкурса {{ dialogData.title || '' }}
|
||||
</template>
|
||||
<template v-else-if="dialogType === 'project_members'">
|
||||
Привязка участника к проекту
|
||||
</template>
|
||||
<template v-else>
|
||||
Редактирование {{ dialogType }}
|
||||
</template>
|
||||
@ -141,9 +174,59 @@
|
||||
<template v-if="dialogType === 'teams'">
|
||||
<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.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-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 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.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.role_id" label="Роль" dense clearable class="q-mt-sm" />
|
||||
<q-input v-model="dialogData.team_id" label="Команда" dense clearable class="q-mt-sm" />
|
||||
<q-select
|
||||
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" />
|
||||
<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.results" label="Результаты" dense clearable 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-input v-model="dialogData.status_id" label="Статус" dense clearable class="q-mt-sm" />
|
||||
<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.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" />
|
||||
<div class="text-h6 q-mb-sm">Фотографии карусели конкурса</div>
|
||||
@ -407,6 +528,34 @@
|
||||
/>
|
||||
</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>
|
||||
<pre>{{ dialogData }}</pre>
|
||||
</template>
|
||||
@ -441,10 +590,17 @@
|
||||
color="negative"
|
||||
@click="deleteItem"
|
||||
/>
|
||||
<q-btn
|
||||
v-if="dialogType === 'project_members' && dialogData.id"
|
||||
flat
|
||||
label="Удалить"
|
||||
color="negative"
|
||||
@click="deleteItem"
|
||||
/>
|
||||
<q-space />
|
||||
<q-btn flat label="Закрыть" color="primary" @click="closeDialog" />
|
||||
<q-btn
|
||||
v-if="['teams', 'profiles', 'projects', 'contests'].includes(dialogType)"
|
||||
v-if="['teams', 'profiles', 'projects', 'contests', 'project_members'].includes(dialogType)"
|
||||
flat
|
||||
label="Сохранить"
|
||||
color="primary"
|
||||
@ -454,6 +610,122 @@
|
||||
</q-card>
|
||||
</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
|
||||
:icon="'logout'"
|
||||
class="fixed-bottom-right q-ma-md"
|
||||
@ -465,18 +737,19 @@
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { Notify, useQuasar } from 'quasar'
|
||||
|
||||
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 updateTeam from '@/api/teams/updateTeam.js'
|
||||
import deleteTeamById from '@/api/teams/deleteTeam.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 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 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 CONFIG from '@/core/config.js'
|
||||
|
||||
@ -520,6 +802,21 @@ const $q = useQuasar()
|
||||
|
||||
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 loadingProfiles = ref(false)
|
||||
const profileColumns = [
|
||||
@ -543,6 +840,10 @@ const teamColumns = [
|
||||
{ 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 loadingProjects = ref(false)
|
||||
const projectColumns = [
|
||||
@ -557,17 +858,53 @@ const contestColumns = [
|
||||
{ name: 'title', label: 'Название конкурса', field: 'title', sortable: true },
|
||||
{ name: 'description', label: 'Описание', field: 'description', 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: 'is_win', label: 'Победа', field: 'is_win', sortable: true },
|
||||
{ name: 'project_id', label: 'Проект', field: 'project_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 dialogData = ref({})
|
||||
const dialogType = ref('')
|
||||
|
||||
|
||||
const profilePhotos = ref([])
|
||||
const loadingProfilePhotos = ref(false)
|
||||
const newProfilePhotoFile = ref(null)
|
||||
@ -586,12 +923,15 @@ const projectFiles = ref([])
|
||||
const loadingProjectFiles = ref(false)
|
||||
const newProjectFile = ref(null)
|
||||
|
||||
|
||||
// TO DO
|
||||
// ДОБАВИТЬ ПРАВИЛЬНЫЙ URL
|
||||
const getPhotoUrl = (photoId, type) => {
|
||||
if (type === 'profile') {
|
||||
return `${CONFIG.BASE_URL}/profile_photos/${photoId}/file`;
|
||||
} else if (type === 'contest') {
|
||||
return `${CONFIG.BASE_URL}/contest_carousel_photos/${photoId}/file`;
|
||||
} else if (type === 'teams') {
|
||||
return `${CONFIG.BASE_URL}//${photoId}/file`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
@ -676,6 +1016,9 @@ function handleNewProjectFileSelected(file) {
|
||||
newProjectFile.value = file;
|
||||
}
|
||||
|
||||
function handleNewTeamLogoSelected(file) {
|
||||
newTeamLogoFile.value = file;
|
||||
}
|
||||
|
||||
async function uploadNewProfilePhoto() {
|
||||
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() {
|
||||
if (!newContestPhotoFile.value || !dialogData.value.id) {
|
||||
Notify.create({
|
||||
@ -959,25 +1336,39 @@ async function downloadExistingProjectFile(fileId) {
|
||||
|
||||
|
||||
function openEdit(type, row) {
|
||||
dialogType.value = type
|
||||
dialogType.value = type;
|
||||
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 {
|
||||
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') {
|
||||
dialogData.value = { title: '', description: '', repository_url: '' }
|
||||
dialogData.value = { title: '', description: '', repository_url: '' };
|
||||
projectFiles.value = [];
|
||||
} 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 = [];
|
||||
} 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 = [];
|
||||
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) {
|
||||
loadProfilePhotos(dialogData.value.id);
|
||||
@ -986,7 +1377,24 @@ function openEdit(type, row) {
|
||||
loadContestFiles(dialogData.value.id);
|
||||
} else if (type === 'projects' && 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 = [];
|
||||
contestPhotos.value = [];
|
||||
contestFiles.value = [];
|
||||
@ -1016,12 +1424,13 @@ async function saveChanges() {
|
||||
try {
|
||||
if (dialogType.value === 'teams') {
|
||||
if (dialogData.value.id) {
|
||||
await updateTeam(dialogData.value)
|
||||
const idx = teams.value.findIndex(t => t.id === dialogData.value.id)
|
||||
if (idx !== -1) teams.value[idx] = JSON.parse(JSON.stringify(dialogData.value))
|
||||
await updateTeam(dialogData.value);
|
||||
const idx = teams.value.findIndex(t => t.id === dialogData.value.id);
|
||||
if (idx !== -1) teams.value[idx] = JSON.parse(JSON.stringify(dialogData.value));
|
||||
} else {
|
||||
const newTeam = await createTeam(dialogData.value)
|
||||
teams.value.push(newTeam)
|
||||
const newTeamData = { ...dialogData.value, is_active: false };
|
||||
const newTeam = await createTeam(newTeamData);
|
||||
teams.value.push(newTeam);
|
||||
}
|
||||
} else if (dialogType.value === 'projects') {
|
||||
if (dialogData.value.id) {
|
||||
@ -1050,6 +1459,54 @@ async function saveChanges() {
|
||||
const newContest = await createContest(dialogData.value)
|
||||
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()
|
||||
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) {
|
||||
if (name === 'teams') {
|
||||
loadingTeams.value = true
|
||||
@ -1100,6 +1721,7 @@ async function loadData(name) {
|
||||
loadingProfiles.value = true
|
||||
try {
|
||||
profiles.value = await fetchProfiles() || []
|
||||
allProfiles.value = await fetchProfiles() || []
|
||||
} catch (error) {
|
||||
profiles.value = []
|
||||
Notify.create({
|
||||
@ -1124,6 +1746,27 @@ async function loadData(name) {
|
||||
} finally {
|
||||
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
|
||||
$q.dialog({
|
||||
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,
|
||||
persistent: true,
|
||||
ok: {
|
||||
@ -1156,6 +1804,9 @@ async function deleteItem() {
|
||||
} else if (dialogType.value === 'contests') {
|
||||
await deleteContestById(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()
|
||||
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() {
|
||||
dialogData.value = {};
|
||||
openEdit(tab.value, null)
|
||||
|
||||
@ -60,7 +60,7 @@
|
||||
<q-card-section class="q-pa-md">
|
||||
<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
|
||||
v-for="(monthLabel, idx) in monthLabels"
|
||||
:key="monthLabel"
|
||||
@ -72,7 +72,7 @@
|
||||
</div>
|
||||
|
||||
<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
|
||||
v-for="(day, idx) in weekDays"
|
||||
:key="day"
|
||||
@ -168,8 +168,21 @@ const activityData = ref([]);
|
||||
const dayHeight = 14;
|
||||
const squareSize = ref(12);
|
||||
|
||||
// Подписи месяцев (с июня 2024 по май 2025)
|
||||
const monthLabels = ['июн.', 'июл.', 'авг.', 'сент.', 'окт.', 'нояб.', 'дек.', 'янв.', 'февр.', 'март', 'апр.', 'май'];
|
||||
function getDynamicMonthLabelsShort() {
|
||||
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)
|
||||
const weekDays = ['пн', 'ср', 'пт'];
|
||||
@ -227,9 +240,14 @@ async function loadTeamData() {
|
||||
try {
|
||||
const teams = await fetchTeams();
|
||||
const activeTeam = teams.find(team => team.is_active === true);
|
||||
|
||||
if (activeTeam) {
|
||||
teamName.value = activeTeam.name;
|
||||
teamLogo.value = activeTeam.logo;
|
||||
teamName.value = activeTeam.title || 'Название не указано';
|
||||
teamLogo.value = activeTeam.logo || '';
|
||||
|
||||
// Вы также можете сохранить другие данные активной команды, если они нужны:
|
||||
// teamDescription.value = activeTeam.description || '';
|
||||
// teamGitUrl.value = activeTeam.git_url || '';
|
||||
} else {
|
||||
Notify.create({
|
||||
type: 'warning',
|
||||
@ -252,10 +270,17 @@ async function loadMembers() {
|
||||
try {
|
||||
const profiles = await fetchProfiles();
|
||||
members.value = profiles.map(profile => ({
|
||||
id: profile.id,
|
||||
name: profile.name || 'Без имени',
|
||||
role: profile.role || 'Участник',
|
||||
avatar: profile.avatar || 'https://randomuser.me/api/portraits/men/1.jpg',
|
||||
id: profile.id, // ID всегда остается
|
||||
name: `${profile.first_name || ''} ${profile.last_name || ''}`.trim() || 'Без имени', // Объединяем имя и фамилию
|
||||
role: profile.role_id || 'Участник', // Используем role_id
|
||||
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) {
|
||||
console.error('Ошибка загрузки участников:', error);
|
||||
@ -437,4 +462,7 @@ function decreaseScale() {
|
||||
flex-shrink: 0;
|
||||
position: absolute; /* Для точного позиционирования */
|
||||
}
|
||||
.weekdays-column {
|
||||
margin-top: -15px; /* Попробуйте разные значения, например, -10px, -30px, пока не найдете оптимальное */
|
||||
}
|
||||
</style>
|
||||
@ -53,8 +53,6 @@ const authorisation = async () => {
|
||||
|
||||
const roleId = profileResponse.data.role_id
|
||||
|
||||
|
||||
|
||||
Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Успешный вход!',
|
||||
@ -67,15 +65,23 @@ const authorisation = async () => {
|
||||
await router.push('/')
|
||||
}
|
||||
|
||||
if (roleId === 1) {
|
||||
await router.push('/admin')
|
||||
} else {
|
||||
await router.push('/')
|
||||
}
|
||||
} 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({
|
||||
type: 'negative',
|
||||
message: error.message || 'Ошибка входа',
|
||||
message: errorMessage,
|
||||
icon: 'error'
|
||||
})
|
||||
} finally {
|
||||
@ -209,8 +215,6 @@ const authorisation = async () => {
|
||||
animation: marquee-move var(--marquee-duration) linear infinite;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.marquee-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user