Compare commits

...

3 Commits

39 changed files with 2148 additions and 675 deletions

View File

@ -0,0 +1,30 @@
from typing import Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.models import CompanyProfileLogo
class CompanyProfileLogosRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_by_id(self, logo_id: int) -> Optional[CompanyProfileLogo]:
query = (
select(CompanyProfileLogo)
.filter_by(id=logo_id)
)
result = await self.db.execute(query)
return result.scalars().first()
async def create(self, company_profile_logo: CompanyProfileLogo) -> CompanyProfileLogo:
self.db.add(company_profile_logo)
await self.db.commit()
await self.db.refresh(company_profile_logo)
return company_profile_logo
async def delete(self, company_profile_logo: CompanyProfileLogo) -> CompanyProfileLogo:
await self.db.delete(company_profile_logo)
await self.db.commit()
return company_profile_logo

View File

@ -0,0 +1,30 @@
from typing import Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.models import CompanyProfilePhoto
class CompanyProfilePhotosRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_by_id(self, photo_id: int) -> Optional[CompanyProfilePhoto]:
query = (
select(CompanyProfilePhoto)
.filter_by(id=photo_id)
)
result = await self.db.execute(query)
return result.scalars().first()
async def create(self, company_profile_photos: CompanyProfilePhoto) -> CompanyProfilePhoto:
self.db.add(company_profile_photos)
await self.db.commit()
await self.db.refresh(company_profile_photos)
return company_profile_photos
async def delete(self, company_profile_photos: CompanyProfilePhoto) -> CompanyProfilePhoto:
await self.db.delete(company_profile_photos)
await self.db.commit()
return company_profile_photos

View File

@ -0,0 +1,39 @@
from typing import Sequence, List
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.models import CompanyProfileSocial
class CompanyProfileSocialsRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_by_company_profile_id(self, company_profile_id: int) -> Sequence[CompanyProfileSocial]:
query = (
select(CompanyProfileSocial)
.filter_by(company_id=company_profile_id)
)
result = await self.db.execute(query)
return result.scalars().all()
async def create_list(self, company_profile_socials: List[CompanyProfileSocial]) -> Sequence[CompanyProfileSocial]:
self.db.add_all(company_profile_socials)
await self.db.commit()
for company_profile_social in company_profile_socials:
await self.db.refresh(company_profile_social)
return company_profile_socials
async def delete_list(
self,
company_profile_socials: List[CompanyProfileSocial] | Sequence[CompanyProfileSocial]
) -> Sequence[CompanyProfileSocial]:
for company_profile_social in company_profile_socials:
await self.db.delete(company_profile_social)
await self.db.commit()
return company_profile_socials

View File

@ -4,6 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from app.domain.models import CompanyProfile, CompanyProfileSocial
class CompanyProfilesRepository:
def __init__(self, db: AsyncSession):
self.db = db
@ -16,24 +17,43 @@ class CompanyProfilesRepository:
joinedload(CompanyProfile.logo),
joinedload(CompanyProfile.official_photo),
joinedload(CompanyProfile.industry),
joinedload(CompanyProfile.socials)
joinedload(CompanyProfile.socials),
joinedload(CompanyProfile.verification_requests),
)
)
result = await self.db.execute(query)
return result.scalars().first()
async def get_company_by_creator_id(self, user_id: int) -> Optional[CompanyProfile]:
query = (
select(CompanyProfile)
.filter_by(creator_user_id=user_id)
.options(
joinedload(CompanyProfile.logo),
joinedload(CompanyProfile.official_photo),
joinedload(CompanyProfile.industry),
joinedload(CompanyProfile.socials),
joinedload(CompanyProfile.verification_requests),
)
)
result = await self.db.execute(query)
return result.scalars().first()
async def get_by_company_official_photo_id(self, logo_id: int) -> Optional[CompanyProfile]:
query = (
select(CompanyProfile)
.filter_by(official_photo_id=logo_id)
)
result = await self.db.execute(query)
return result.scalars().first()
async def create(self, company: CompanyProfile) -> CompanyProfile:
self.db.add(company)
await self.db.commit()
await self.db.refresh(company)
return company
async def add_socials(self, socials: List[CompanyProfileSocial]):
self.db.add_all(socials)
await self.db.commit()
async def delete_socials_by_company_id(self, company_id: int):
from sqlalchemy import delete
query = delete(CompanyProfileSocial).filter_by(company_id=company_id)
await self.db.execute(query)
async def update(self, company: CompanyProfile) -> CompanyProfile:
await self.db.merge(company)
await self.db.commit()
return company

View File

@ -0,0 +1,38 @@
from typing import Sequence
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.models import VerificationRequest
class VerificationRequestSRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_by_id(self, verification_request_id: int) -> VerificationRequest:
query = (
select(VerificationRequest)
.filter_by(id=verification_request_id)
)
result = await self.db.execute(query)
return result.scalars().first()
async def get_by_company_profile_id(self, company_profile_id: int) -> Sequence[VerificationRequest]:
query = (
select(VerificationRequest)
.filter_by(company_id=company_profile_id)
)
result = await self.db.execute(query)
return result.scalars().all()
async def create(self, verification_request: VerificationRequest) -> VerificationRequest:
self.db.add(verification_request)
await self.db.commit()
await self.db.refresh(verification_request)
return verification_request
async def update(self, verification_request: VerificationRequest) -> VerificationRequest:
await self.db.merge(verification_request)
await self.db.commit()
return verification_request

View File

@ -0,0 +1,54 @@
from fastapi import APIRouter, Depends, UploadFile, File
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import FileResponse
from app.database.session import get_db
from app.domain.entities.company_profile_photos import CompanyProfilePhotoRead
from app.domain.models import User
from app.infrastructure.company_profile_logos_router import CompanyProfileLogosService
from app.infrastructure.dependencies import require_employer
company_profile_logos_router = APIRouter()
@company_profile_logos_router.get(
'/file/{file_id}/',
response_class=FileResponse,
)
async def get_file(
file_id: int,
db: AsyncSession = Depends(get_db),
):
company_profile_logos_service = CompanyProfileLogosService(db)
return await company_profile_logos_service.get_file_by_id(file_id)
@company_profile_logos_router.post(
'/files/{company_profile_id}/upload/',
response_model=CompanyProfilePhotoRead,
summary='Upload a file',
description='Upload a file',
)
async def upload_file(
company_profile_id: int,
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
user: User = Depends(require_employer),
):
company_profile_logos_service = CompanyProfileLogosService(db)
return await company_profile_logos_service.upload_file(company_profile_id, file, user)
@company_profile_logos_router.delete(
'/files/{company_profile_id}/',
response_model=CompanyProfilePhotoRead,
summary='Delete a file',
description='Delete a file',
)
async def delete_file(
company_profile_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_employer),
):
company_profile_logos_service = CompanyProfileLogosService(db)
return await company_profile_logos_service.delete_file(company_profile_id, user)

View File

@ -0,0 +1,54 @@
from fastapi import APIRouter, Depends, UploadFile, File
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import FileResponse
from app.database.session import get_db
from app.domain.entities.company_profile_photos import CompanyProfilePhotoRead
from app.domain.models import User
from app.infrastructure.company_profile_photos_router import CompanyProfilePhotosService
from app.infrastructure.dependencies import require_employer
company_profile_photos_router = APIRouter()
@company_profile_photos_router.get(
'/file/{file_id}/',
response_class=FileResponse,
)
async def get_file(
file_id: int,
db: AsyncSession = Depends(get_db),
):
company_profile_photos_service = CompanyProfilePhotosService(db)
return await company_profile_photos_service.get_file_by_id(file_id)
@company_profile_photos_router.post(
'/files/{company_profile_id}/upload/',
response_model=CompanyProfilePhotoRead,
summary='Upload a file',
description='Upload a file',
)
async def upload_file(
company_profile_id: int,
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
user: User = Depends(require_employer),
):
company_profile_photos_service = CompanyProfilePhotosService(db)
return await company_profile_photos_service.upload_file(company_profile_id, file, user)
@company_profile_photos_router.delete(
'/files/{company_profile_id}/',
response_model=CompanyProfilePhotoRead,
summary='Delete a file',
description='Delete a file',
)
async def delete_file(
company_profile_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_employer),
):
company_profile_photos_service = CompanyProfilePhotosService(db)
return await company_profile_photos_service.delete_file(company_profile_id, user)

View File

@ -0,0 +1,43 @@
from typing import Optional, List
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db
from app.domain.entities.company_profile_socials import CompanyProfileSocialRead, CompanyProfileSocialCreate
from app.domain.models import User
from app.infrastructure.company_profile_socials_service import CompanyProfileSocialsService
from app.infrastructure.dependencies import require_employer
company_profile_socials_router = APIRouter()
@company_profile_socials_router.get(
'/{company_profile_id}/',
response_model=Optional[List[CompanyProfileSocialRead]],
summary='Get company profile socials',
description='Get company profile socials',
)
async def get_company_profile_socials(
company_profile_id: int,
db: AsyncSession = Depends(get_db),
):
company_profile_socials_service = CompanyProfileSocialsService(db)
return await company_profile_socials_service.get_by_company_profile_id(company_profile_id)
@company_profile_socials_router.post(
'/{company_profile_id}/',
response_model=Optional[List[CompanyProfileSocialRead]],
summary='Replace company profile social',
description='Replace company profile social',
)
async def replace_company_profile_social(
company_profile_id: int,
company_profile_socials: List[CompanyProfileSocialCreate],
db: AsyncSession = Depends(get_db),
user: User = Depends(require_employer),
):
company_profile_social_service = CompanyProfileSocialsService(db)
return await company_profile_social_service.replace_company_profile_socials(company_profile_socials,
company_profile_id, user)

View File

@ -1,44 +1,57 @@
from typing import Optional
from fastapi import APIRouter, Depends, status, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db
from app.domain.entities.company_profiles import CompanyProfileCreate, CompanyProfileRead
from app.domain.entities.company_profiles import CompanyProfileCreate, CompanyProfileRead, CompanyProfileUpdate
from app.domain.models import User
from app.infrastructure.company_profiles_service import CompanyProfilesService
from app.infrastructure.dependencies import require_auth_user
from app.infrastructure.dependencies import require_employer
# Это имя должно совпадать с тем, что импортируется в main.py
company_profiles_router = APIRouter()
@company_profiles_router.get(
'/me',
response_model=CompanyProfileRead,
summary='Получить профиль текущей компании',
'/me/',
response_model=Optional[CompanyProfileRead],
summary='Get a company profile',
description='Get a company profile',
)
async def get_my_company(
db: AsyncSession = Depends(get_db),
user: User = Depends(require_auth_user)
user: User = Depends(require_employer)
):
"""Возвращает профиль компании для авторизованного пользователя"""
service = CompanyProfilesService(db)
profile = await service.get_company_by_creator_id(user.id)
if not profile:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Профиль компании не найден"
)
return profile
company_profile_service = CompanyProfilesService(db)
return await company_profile_service.get_company_by_creator_id(user.id)
@company_profiles_router.post(
'/',
response_model=CompanyProfileRead,
status_code=status.HTTP_201_CREATED,
summary='Создать профиль компании',
response_model=Optional[CompanyProfileRead],
summary='Create a new company profile',
description='Create a new company profile',
)
async def create_company(
company_data: CompanyProfileCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_auth_user)
user: User = Depends(require_employer)
):
"""Создает новый профиль компании"""
service = CompanyProfilesService(db)
return await service.create_company(company_data, user)
company_profile_service = CompanyProfilesService(db)
return await company_profile_service.create_company(company_data, user)
@company_profiles_router.put(
'/{company_id}/',
response_model=Optional[CompanyProfileRead],
summary='Update a company profile',
description='Update a company profile',
)
async def update_company(
company_id: int,
company_data: CompanyProfileUpdate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_employer),
):
company_profile_service = CompanyProfilesService(db)
return await company_profile_service.update_company(company_id, company_data, user)

View File

@ -3,16 +3,19 @@ from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db
from app.domain.entities.industries import IndustryRead
from app.application.industries_repository import IndustriesRepository
from app.infrastructure.industries_services import IndustriesService
industries_router = APIRouter()
@industries_router.get(
'/',
response_model=List[IndustryRead],
summary='Получить список всех сфер деятельности',
summary='Get all industries',
description='Get all industries',
)
async def get_all_industries(db: AsyncSession = Depends(get_db)):
repo = IndustriesRepository(db)
industries = await repo.get_all()
return [IndustryRead.model_validate(i) for i in industries]
async def get_all_industries(
db: AsyncSession = Depends(get_db),
):
industries_service = IndustriesService(db)
return await industries_service.get_all()

View File

@ -0,0 +1,42 @@
from typing import List
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db
from app.domain.entities.verification_requests import VerificationRequestRead
from app.domain.models import User
from app.infrastructure.company_verification_service import CompanyVerificationRequestsService
from app.infrastructure.dependencies import require_employer
verification_requests_router = APIRouter()
@verification_requests_router.get(
'/{company_id}/',
response_model=List[VerificationRequestRead],
summary=f'Get all verification requests',
description='Get all verification requests',
)
async def get_all_verification_requests(
company_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_employer),
):
verification_requests_service = CompanyVerificationRequestsService(db)
return await verification_requests_service.get_by_company_id(company_id, user)
@verification_requests_router.post(
'/{company_id}/',
response_model=VerificationRequestRead,
summary='Create a new verification request',
description='Create a new verification request',
)
async def create_verification_request(
company_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_employer),
):
verification_requests_service = CompanyVerificationRequestsService(db)
return await verification_requests_service.create_verification_request(company_id, user)

View File

@ -0,0 +1,224 @@
"""0008 изменил тблицу с заявками на верификацию
Revision ID: 08d60de6f70b
Revises: 95c2576d5d3d
Create Date: 2026-04-08 22:31:55.772985
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '08d60de6f70b'
down_revision: Union[str, Sequence[str], None] = '95c2576d5d3d'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('history')
op.drop_constraint(op.f('applicant_contact_recommendations_recommender_id_fkey'), 'applicant_contact_recommendations', type_='foreignkey')
op.drop_constraint(op.f('applicant_contact_recommendations_recipient_id_fkey'), 'applicant_contact_recommendations', type_='foreignkey')
op.create_foreign_key(None, 'applicant_contact_recommendations', 'applicant_profiles', ['recommender_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'applicant_contact_recommendations', 'applicant_profiles', ['recipient_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('applicant_contacts_status_id_fkey'), 'applicant_contacts', type_='foreignkey')
op.drop_constraint(op.f('applicant_contacts_receiver_id_fkey'), 'applicant_contacts', type_='foreignkey')
op.drop_constraint(op.f('applicant_contacts_sender_id_fkey'), 'applicant_contacts', type_='foreignkey')
op.create_foreign_key(None, 'applicant_contacts', 'applicant_profiles', ['receiver_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'applicant_contacts', 'applicant_profiles', ['sender_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'applicant_contacts', 'applicant_contact_statuses', ['status_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('applicant_educations_applicant_id_fkey'), 'applicant_educations', type_='foreignkey')
op.drop_constraint(op.f('applicant_educations_university_id_fkey'), 'applicant_educations', type_='foreignkey')
op.create_foreign_key(None, 'applicant_educations', 'universities', ['university_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'applicant_educations', 'applicant_profiles', ['applicant_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('applicant_experiences_applicant_id_fkey'), 'applicant_experiences', type_='foreignkey')
op.create_foreign_key(None, 'applicant_experiences', 'applicant_profiles', ['applicant_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('applicant_favorite_companies_company_id_fkey'), 'applicant_favorite_companies', type_='foreignkey')
op.drop_constraint(op.f('applicant_favorite_companies_applicant_id_fkey'), 'applicant_favorite_companies', type_='foreignkey')
op.create_foreign_key(None, 'applicant_favorite_companies', 'company_profiles', ['company_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'applicant_favorite_companies', 'applicant_profiles', ['applicant_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('applicant_profiles_user_id_fkey'), 'applicant_profiles', type_='foreignkey')
op.create_foreign_key(None, 'applicant_profiles', 'users', ['user_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('applicant_resume_files_applicant_id_fkey'), 'applicant_resume_files', type_='foreignkey')
op.create_foreign_key(None, 'applicant_resume_files', 'applicant_profiles', ['applicant_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('applicant_resume_links_applicant_id_fkey'), 'applicant_resume_links', type_='foreignkey')
op.create_foreign_key(None, 'applicant_resume_links', 'applicant_profiles', ['applicant_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('applicant_resume_projects_applicant_id_fkey'), 'applicant_resume_projects', type_='foreignkey')
op.create_foreign_key(None, 'applicant_resume_projects', 'applicant_profiles', ['applicant_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('applicant_skills_applicant_id_fkey'), 'applicant_skills', type_='foreignkey')
op.drop_constraint(op.f('applicant_skills_level_id_fkey'), 'applicant_skills', type_='foreignkey')
op.drop_constraint(op.f('applicant_skills_tag_id_fkey'), 'applicant_skills', type_='foreignkey')
op.create_foreign_key(None, 'applicant_skills', 'applicant_skill_tags', ['tag_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'applicant_skills', 'experience_levels', ['level_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'applicant_skills', 'applicant_profiles', ['applicant_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('career_events_location_id_fkey'), 'career_events', type_='foreignkey')
op.drop_constraint(op.f('career_events_company_id_fkey'), 'career_events', type_='foreignkey')
op.drop_constraint(op.f('career_events_moderation_status_id_fkey'), 'career_events', type_='foreignkey')
op.drop_constraint(op.f('career_events_event_type_id_fkey'), 'career_events', type_='foreignkey')
op.create_foreign_key(None, 'career_events', 'location_coordinates', ['location_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'career_events', 'event_types', ['event_type_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'career_events', 'moderation_statuses', ['moderation_status_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'career_events', 'company_profiles', ['company_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('company_profile_socials_company_id_fkey'), 'company_profile_socials', type_='foreignkey')
op.create_foreign_key(None, 'company_profile_socials', 'company_profiles', ['company_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('company_profiles_official_photo_id_fkey'), 'company_profiles', type_='foreignkey')
op.drop_constraint(op.f('company_profiles_logo_id_fkey'), 'company_profiles', type_='foreignkey')
op.drop_constraint(op.f('company_profiles_creator_user_id_fkey'), 'company_profiles', type_='foreignkey')
op.drop_constraint(op.f('company_profiles_industry_id_fkey'), 'company_profiles', type_='foreignkey')
op.create_foreign_key(None, 'company_profiles', 'company_profile_photos', ['official_photo_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'company_profiles', 'industries', ['industry_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'company_profiles', 'company_profile_logos', ['logo_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'company_profiles', 'users', ['creator_user_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('internships_company_id_fkey'), 'internships', type_='foreignkey')
op.drop_constraint(op.f('internships_location_id_fkey'), 'internships', type_='foreignkey')
op.drop_constraint(op.f('internships_experience_level_id_fkey'), 'internships', type_='foreignkey')
op.drop_constraint(op.f('internships_moderation_status_id_fkey'), 'internships', type_='foreignkey')
op.drop_constraint(op.f('internships_work_format_id_fkey'), 'internships', type_='foreignkey')
op.create_foreign_key(None, 'internships', 'moderation_statuses', ['moderation_status_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'internships', 'location_coordinates', ['location_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'internships', 'company_profiles', ['company_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'internships', 'work_formats', ['work_format_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'internships', 'experience_levels', ['experience_level_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('mentorship_programs_location_id_fkey'), 'mentorship_programs', type_='foreignkey')
op.drop_constraint(op.f('mentorship_programs_moderation_status_id_fkey'), 'mentorship_programs', type_='foreignkey')
op.drop_constraint(op.f('mentorship_programs_company_id_fkey'), 'mentorship_programs', type_='foreignkey')
op.create_foreign_key(None, 'mentorship_programs', 'location_coordinates', ['location_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'mentorship_programs', 'company_profiles', ['company_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'mentorship_programs', 'moderation_statuses', ['moderation_status_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('users_role_id_fkey'), 'users', type_='foreignkey')
op.create_foreign_key(None, 'users', 'roles', ['role_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('vacancies_employment_type_id_fkey'), 'vacancies', type_='foreignkey')
op.drop_constraint(op.f('vacancies_work_format_id_fkey'), 'vacancies', type_='foreignkey')
op.drop_constraint(op.f('vacancies_moderation_status_id_fkey'), 'vacancies', type_='foreignkey')
op.drop_constraint(op.f('vacancies_location_id_fkey'), 'vacancies', type_='foreignkey')
op.drop_constraint(op.f('vacancies_experience_level_id_fkey'), 'vacancies', type_='foreignkey')
op.drop_constraint(op.f('vacancies_company_id_fkey'), 'vacancies', type_='foreignkey')
op.create_foreign_key(None, 'vacancies', 'moderation_statuses', ['moderation_status_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'vacancies', 'location_coordinates', ['location_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'vacancies', 'work_formats', ['work_format_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'vacancies', 'employment_types', ['employment_type_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'vacancies', 'experience_levels', ['experience_level_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'vacancies', 'company_profiles', ['company_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('vacancy_applications_applicant_id_fkey'), 'vacancy_applications', type_='foreignkey')
op.drop_constraint(op.f('vacancy_applications_vacancy_id_fkey'), 'vacancy_applications', type_='foreignkey')
op.create_foreign_key(None, 'vacancy_applications', 'applicant_profiles', ['applicant_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'vacancy_applications', 'vacancies', ['vacancy_id'], ['id'], source_schema='public', referent_schema='public')
op.alter_column('verification_requests', 'is_accepted',
existing_type=sa.BOOLEAN(),
nullable=True)
op.drop_constraint(op.f('verification_requests_company_id_fkey'), 'verification_requests', type_='foreignkey')
op.create_foreign_key(None, 'verification_requests', 'company_profiles', ['company_id'], ['id'], source_schema='public', referent_schema='public')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'verification_requests', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('verification_requests_company_id_fkey'), 'verification_requests', 'company_profiles', ['company_id'], ['id'])
op.alter_column('verification_requests', 'is_accepted',
existing_type=sa.BOOLEAN(),
nullable=False)
op.drop_constraint(None, 'vacancy_applications', schema='public', type_='foreignkey')
op.drop_constraint(None, 'vacancy_applications', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('vacancy_applications_vacancy_id_fkey'), 'vacancy_applications', 'vacancies', ['vacancy_id'], ['id'])
op.create_foreign_key(op.f('vacancy_applications_applicant_id_fkey'), 'vacancy_applications', 'applicant_profiles', ['applicant_id'], ['id'])
op.drop_constraint(None, 'vacancies', schema='public', type_='foreignkey')
op.drop_constraint(None, 'vacancies', schema='public', type_='foreignkey')
op.drop_constraint(None, 'vacancies', schema='public', type_='foreignkey')
op.drop_constraint(None, 'vacancies', schema='public', type_='foreignkey')
op.drop_constraint(None, 'vacancies', schema='public', type_='foreignkey')
op.drop_constraint(None, 'vacancies', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('vacancies_company_id_fkey'), 'vacancies', 'company_profiles', ['company_id'], ['id'])
op.create_foreign_key(op.f('vacancies_experience_level_id_fkey'), 'vacancies', 'experience_levels', ['experience_level_id'], ['id'])
op.create_foreign_key(op.f('vacancies_location_id_fkey'), 'vacancies', 'location_coordinates', ['location_id'], ['id'])
op.create_foreign_key(op.f('vacancies_moderation_status_id_fkey'), 'vacancies', 'moderation_statuses', ['moderation_status_id'], ['id'])
op.create_foreign_key(op.f('vacancies_work_format_id_fkey'), 'vacancies', 'work_formats', ['work_format_id'], ['id'])
op.create_foreign_key(op.f('vacancies_employment_type_id_fkey'), 'vacancies', 'employment_types', ['employment_type_id'], ['id'])
op.drop_constraint(None, 'users', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('users_role_id_fkey'), 'users', 'roles', ['role_id'], ['id'])
op.drop_constraint(None, 'mentorship_programs', schema='public', type_='foreignkey')
op.drop_constraint(None, 'mentorship_programs', schema='public', type_='foreignkey')
op.drop_constraint(None, 'mentorship_programs', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('mentorship_programs_company_id_fkey'), 'mentorship_programs', 'company_profiles', ['company_id'], ['id'])
op.create_foreign_key(op.f('mentorship_programs_moderation_status_id_fkey'), 'mentorship_programs', 'moderation_statuses', ['moderation_status_id'], ['id'])
op.create_foreign_key(op.f('mentorship_programs_location_id_fkey'), 'mentorship_programs', 'location_coordinates', ['location_id'], ['id'])
op.drop_constraint(None, 'internships', schema='public', type_='foreignkey')
op.drop_constraint(None, 'internships', schema='public', type_='foreignkey')
op.drop_constraint(None, 'internships', schema='public', type_='foreignkey')
op.drop_constraint(None, 'internships', schema='public', type_='foreignkey')
op.drop_constraint(None, 'internships', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('internships_work_format_id_fkey'), 'internships', 'work_formats', ['work_format_id'], ['id'])
op.create_foreign_key(op.f('internships_moderation_status_id_fkey'), 'internships', 'moderation_statuses', ['moderation_status_id'], ['id'])
op.create_foreign_key(op.f('internships_experience_level_id_fkey'), 'internships', 'experience_levels', ['experience_level_id'], ['id'])
op.create_foreign_key(op.f('internships_location_id_fkey'), 'internships', 'location_coordinates', ['location_id'], ['id'])
op.create_foreign_key(op.f('internships_company_id_fkey'), 'internships', 'company_profiles', ['company_id'], ['id'])
op.drop_constraint(None, 'company_profiles', schema='public', type_='foreignkey')
op.drop_constraint(None, 'company_profiles', schema='public', type_='foreignkey')
op.drop_constraint(None, 'company_profiles', schema='public', type_='foreignkey')
op.drop_constraint(None, 'company_profiles', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('company_profiles_industry_id_fkey'), 'company_profiles', 'industries', ['industry_id'], ['id'])
op.create_foreign_key(op.f('company_profiles_creator_user_id_fkey'), 'company_profiles', 'users', ['creator_user_id'], ['id'])
op.create_foreign_key(op.f('company_profiles_logo_id_fkey'), 'company_profiles', 'company_profile_logos', ['logo_id'], ['id'])
op.create_foreign_key(op.f('company_profiles_official_photo_id_fkey'), 'company_profiles', 'company_profile_photos', ['official_photo_id'], ['id'])
op.drop_constraint(None, 'company_profile_socials', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('company_profile_socials_company_id_fkey'), 'company_profile_socials', 'company_profiles', ['company_id'], ['id'])
op.drop_constraint(None, 'career_events', schema='public', type_='foreignkey')
op.drop_constraint(None, 'career_events', schema='public', type_='foreignkey')
op.drop_constraint(None, 'career_events', schema='public', type_='foreignkey')
op.drop_constraint(None, 'career_events', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('career_events_event_type_id_fkey'), 'career_events', 'event_types', ['event_type_id'], ['id'])
op.create_foreign_key(op.f('career_events_moderation_status_id_fkey'), 'career_events', 'moderation_statuses', ['moderation_status_id'], ['id'])
op.create_foreign_key(op.f('career_events_company_id_fkey'), 'career_events', 'company_profiles', ['company_id'], ['id'])
op.create_foreign_key(op.f('career_events_location_id_fkey'), 'career_events', 'location_coordinates', ['location_id'], ['id'])
op.drop_constraint(None, 'applicant_skills', schema='public', type_='foreignkey')
op.drop_constraint(None, 'applicant_skills', schema='public', type_='foreignkey')
op.drop_constraint(None, 'applicant_skills', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('applicant_skills_tag_id_fkey'), 'applicant_skills', 'applicant_skill_tags', ['tag_id'], ['id'])
op.create_foreign_key(op.f('applicant_skills_level_id_fkey'), 'applicant_skills', 'experience_levels', ['level_id'], ['id'])
op.create_foreign_key(op.f('applicant_skills_applicant_id_fkey'), 'applicant_skills', 'applicant_profiles', ['applicant_id'], ['id'])
op.drop_constraint(None, 'applicant_resume_projects', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('applicant_resume_projects_applicant_id_fkey'), 'applicant_resume_projects', 'applicant_profiles', ['applicant_id'], ['id'])
op.drop_constraint(None, 'applicant_resume_links', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('applicant_resume_links_applicant_id_fkey'), 'applicant_resume_links', 'applicant_profiles', ['applicant_id'], ['id'])
op.drop_constraint(None, 'applicant_resume_files', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('applicant_resume_files_applicant_id_fkey'), 'applicant_resume_files', 'applicant_profiles', ['applicant_id'], ['id'])
op.drop_constraint(None, 'applicant_profiles', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('applicant_profiles_user_id_fkey'), 'applicant_profiles', 'users', ['user_id'], ['id'])
op.drop_constraint(None, 'applicant_favorite_companies', schema='public', type_='foreignkey')
op.drop_constraint(None, 'applicant_favorite_companies', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('applicant_favorite_companies_applicant_id_fkey'), 'applicant_favorite_companies', 'applicant_profiles', ['applicant_id'], ['id'])
op.create_foreign_key(op.f('applicant_favorite_companies_company_id_fkey'), 'applicant_favorite_companies', 'company_profiles', ['company_id'], ['id'])
op.drop_constraint(None, 'applicant_experiences', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('applicant_experiences_applicant_id_fkey'), 'applicant_experiences', 'applicant_profiles', ['applicant_id'], ['id'])
op.drop_constraint(None, 'applicant_educations', schema='public', type_='foreignkey')
op.drop_constraint(None, 'applicant_educations', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('applicant_educations_university_id_fkey'), 'applicant_educations', 'universities', ['university_id'], ['id'])
op.create_foreign_key(op.f('applicant_educations_applicant_id_fkey'), 'applicant_educations', 'applicant_profiles', ['applicant_id'], ['id'])
op.drop_constraint(None, 'applicant_contacts', schema='public', type_='foreignkey')
op.drop_constraint(None, 'applicant_contacts', schema='public', type_='foreignkey')
op.drop_constraint(None, 'applicant_contacts', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('applicant_contacts_sender_id_fkey'), 'applicant_contacts', 'applicant_profiles', ['sender_id'], ['id'])
op.create_foreign_key(op.f('applicant_contacts_receiver_id_fkey'), 'applicant_contacts', 'applicant_profiles', ['receiver_id'], ['id'])
op.create_foreign_key(op.f('applicant_contacts_status_id_fkey'), 'applicant_contacts', 'applicant_contact_statuses', ['status_id'], ['id'])
op.drop_constraint(None, 'applicant_contact_recommendations', schema='public', type_='foreignkey')
op.drop_constraint(None, 'applicant_contact_recommendations', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('applicant_contact_recommendations_recipient_id_fkey'), 'applicant_contact_recommendations', 'applicant_profiles', ['recipient_id'], ['id'])
op.create_foreign_key(op.f('applicant_contact_recommendations_recommender_id_fkey'), 'applicant_contact_recommendations', 'applicant_profiles', ['recommender_id'], ['id'])
op.create_table('history',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('tstamp', postgresql.TIMESTAMP(), server_default=sa.text('now()'), autoincrement=False, nullable=True),
sa.Column('schemaname', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('tabname', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('operation', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('who', sa.TEXT(), server_default=sa.text('CURRENT_USER'), autoincrement=False, nullable=True),
sa.Column('new_val', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True),
sa.Column('old_val', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True),
sa.Column('item_id', sa.BIGINT(), autoincrement=False, nullable=True)
)
# ### end Alembic commands ###

View File

@ -5,6 +5,8 @@ from app.domain.entities.industries import IndustryRead
from app.domain.entities.company_profile_socials import CompanyProfileSocialCreate, CompanyProfileSocialRead
from app.domain.entities.company_profile_logos import CompanyProfileLogoRead
from app.domain.entities.company_profile_photos import CompanyProfilePhotoRead
from app.domain.entities.verification_requests import VerificationRequestRead
class CompanyProfileCreate(BaseModel):
title: str = Field(max_length=250)
@ -19,6 +21,7 @@ class CompanyProfileCreate(BaseModel):
industry_id: int
socials: Optional[List[CompanyProfileSocialCreate]] = []
class CompanyProfileUpdate(BaseModel):
title: Optional[str] = Field(default=None, max_length=250)
description: Optional[str] = None
@ -31,6 +34,7 @@ class CompanyProfileUpdate(BaseModel):
official_photo_id: Optional[int] = None
industry_id: Optional[int] = None
class CompanyProfileRead(BaseModel):
id: int
title: str
@ -49,6 +53,7 @@ class CompanyProfileRead(BaseModel):
official_photo: Optional[CompanyProfilePhotoRead] = None
industry: IndustryRead
socials: List[CompanyProfileSocialRead] = []
verification_requests: List[VerificationRequestRead] = []
class Config:
from_attributes = True

View File

@ -0,0 +1,16 @@
from typing import Optional
from pydantic import BaseModel
class CompanyVerificationRequestCreate(BaseModel):
company_id: int
class VerificationRequestRead(BaseModel):
id: int
is_accepted: Optional[bool] = None
company_id: int
class Config:
from_attributes = True

View File

@ -7,7 +7,7 @@ from app.domain.models.base import RootTable
class VerificationRequest(RootTable):
__tablename__ = 'verification_requests'
is_accepted: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
is_accepted: Mapped[bool] = mapped_column(Boolean, nullable=True, default=None)
company_id: Mapped[int] = mapped_column(ForeignKey('company_profiles.id'), nullable=False)

View File

@ -0,0 +1,103 @@
import os
import aiofiles
from fastapi import UploadFile, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import FileResponse
from app.application.company_profile_logos_repository import CompanyProfileLogosRepository
from app.application.company_profiles_repository import CompanyProfilesRepository
from app.core.constants import UserRoles
from app.domain.entities.company_profile_logos import CompanyProfileLogoRead
from app.domain.models import User, CompanyProfileLogo
from app.infrastructure.files_service import FilesService
class CompanyProfileLogosService:
def __init__(self, db: AsyncSession):
self.company_profile_logos_repository = CompanyProfileLogosRepository(db)
self.company_profiles_repository = CompanyProfilesRepository(db)
self.files_service = FilesService()
async def get_file_by_id(self, file_id: int) -> FileResponse:
company_profile_photo = await self.company_profile_logos_repository.get_by_id(file_id)
if not company_profile_photo:
raise HTTPException(404, "Файл с таким ID не найден")
return FileResponse(
company_profile_photo.path,
media_type=self.files_service.get_media_type(company_profile_photo.filename),
filename=os.path.basename(company_profile_photo.filename),
)
async def upload_file(self, company_profile_id: int, file: UploadFile,
current_user: User) -> CompanyProfileLogoRead:
company_profile = await self.company_profiles_repository.get_by_id(company_profile_id)
if company_profile is None:
raise HTTPException(404, "Компания не найдена")
if company_profile.creator_user_id != current_user.id and not (
current_user.role.title == UserRoles.MODERATOR and current_user.is_admin
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='Доступ запрещен',
)
excising_photo = await self.company_profile_logos_repository.get_by_id(company_profile.logo_id)
if excising_photo:
await self.delete_file(excising_photo.id, current_user)
file_path = await self.files_service.save_file(file, f'uploads/company_profile_logos/{company_profile.id}')
company_profile_logo_model = CompanyProfileLogo(
filename=file.filename,
path=file_path,
)
company_profile_logo_model = await self.company_profile_logos_repository.create(company_profile_logo_model)
company_profile.logo_id = company_profile_logo_model.id
await self.company_profiles_repository.update(company_profile)
return CompanyProfileLogoRead.model_validate(company_profile_logo_model)
async def delete_file(self, file_id: int, current_user: User) -> CompanyProfileLogoRead:
company_profile = await self.company_profiles_repository.get_by_company_logo_id(file_id)
if company_profile is None and not (
current_user.role.title == UserRoles.MODERATOR and current_user.is_admin
):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail='Данное фото не привязано к компании',
)
if company_profile.creator_user_id != current_user.id and not (
current_user.role.title == UserRoles.MODERATOR and current_user.is_admin
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='Доступ запрещен',
)
company_profile_photo = await self.company_profile_logos_repository.get_by_id(file_id)
if company_profile_photo is None:
raise HTTPException(404, "Файл не найден")
if not os.path.exists(company_profile_photo.path):
raise HTTPException(404, "Файл не найден на диске")
if os.path.exists(company_profile_photo.path):
os.remove(company_profile_photo.path)
company_profile.logo_id = None
await self.company_profiles_repository.update(company_profile)
company_profile_photo = await self.company_profile_logos_repository.delete(company_profile_photo)
return CompanyProfileLogoRead.model_validate(company_profile_photo)

View File

@ -0,0 +1,103 @@
import os
import aiofiles
from fastapi import UploadFile, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import FileResponse
from app.application.company_profile_photos_repository import CompanyProfilePhotosRepository
from app.application.company_profiles_repository import CompanyProfilesRepository
from app.core.constants import UserRoles
from app.domain.entities.company_profile_photos import CompanyProfilePhotoRead
from app.domain.models import User, CompanyProfilePhoto
from app.infrastructure.files_service import FilesService
class CompanyProfilePhotosService:
def __init__(self, db: AsyncSession):
self.company_profile_photos_repository = CompanyProfilePhotosRepository(db)
self.company_profiles_repository = CompanyProfilesRepository(db)
self.files_service = FilesService()
async def get_file_by_id(self, file_id: int) -> FileResponse:
company_profile_photo = await self.company_profile_photos_repository.get_by_id(file_id)
if not company_profile_photo:
raise HTTPException(404, "Файл с таким ID не найден")
return FileResponse(
company_profile_photo.path,
media_type=self.files_service.get_media_type(company_profile_photo.filename),
filename=os.path.basename(company_profile_photo.filename),
)
async def upload_file(self, company_profile_id: int, file: UploadFile,
current_user: User) -> CompanyProfilePhotoRead:
company_profile = await self.company_profiles_repository.get_by_id(company_profile_id)
if company_profile is None:
raise HTTPException(404, "Компания не найдена")
if company_profile.creator_user_id != current_user.id and not (
current_user.role.title == UserRoles.MODERATOR and current_user.is_admin
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='Доступ запрещен',
)
excising_photo = await self.company_profile_photos_repository.get_by_id(company_profile.official_photo_id)
if excising_photo:
await self.delete_file(excising_photo.id, current_user)
file_path = await self.files_service.save_file(file, f'uploads/company_profile_photos/{company_profile.id}')
company_profile_photo_model = CompanyProfilePhoto(
filename=file.filename,
path=file_path,
)
company_profile_photo_model = await self.company_profile_photos_repository.create(company_profile_photo_model)
company_profile.official_photo_id = company_profile_photo_model.id
await self.company_profiles_repository.update(company_profile)
return CompanyProfilePhotoRead.model_validate(company_profile_photo_model)
async def delete_file(self, file_id: int, current_user: User) -> CompanyProfilePhotoRead:
company_profile = await self.company_profiles_repository.get_by_company_official_photo_id(file_id)
if company_profile is None and not (
current_user.role.title == UserRoles.MODERATOR and current_user.is_admin
):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail='Данное фото не привязано к компании',
)
if company_profile.creator_user_id != current_user.id and not (
current_user.role.title == UserRoles.MODERATOR and current_user.is_admin
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='Доступ запрещен',
)
company_profile_photo = await self.company_profile_photos_repository.get_by_id(file_id)
if company_profile_photo is None:
raise HTTPException(404, "Файл не найден")
if not os.path.exists(company_profile_photo.path):
raise HTTPException(404, "Файл не найден на диске")
if os.path.exists(company_profile_photo.path):
os.remove(company_profile_photo.path)
company_profile.official_photo_id = None
await self.company_profiles_repository.update(company_profile)
company_profile_photo = await self.company_profile_photos_repository.delete(company_profile_photo)
return CompanyProfilePhotoRead.model_validate(company_profile_photo)

View File

@ -0,0 +1,79 @@
from typing import List
from fastapi import HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.application.company_profile_socials_repository import CompanyProfileSocialsRepository
from app.application.company_profiles_repository import CompanyProfilesRepository
from app.core.constants import UserRoles
from app.domain.entities.company_profile_socials import CompanyProfileSocialRead, CompanyProfileSocialCreate
from app.domain.models import User, CompanyProfileSocial
class CompanyProfileSocialsService:
def __init__(self, db: AsyncSession):
self.company_profile_socials_repository = CompanyProfileSocialsRepository(db)
self.company_profiles_repository = CompanyProfilesRepository(db)
async def get_by_company_profile_id(
self,
company_profile_id: int,
) -> List[CompanyProfileSocialRead]:
company_profile = await self.company_profiles_repository.get_by_id(company_profile_id)
if not company_profile:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail='Компания с таким ID не найдена'
)
company_profile_socials = await self.company_profile_socials_repository.get_by_company_profile_id(
company_profile_id
)
response = []
for company_profile_social in company_profile_socials:
response.append(CompanyProfileSocialRead.model_validate(company_profile_social))
return response
async def replace_company_profile_socials(
self,
company_profile_socials: List[CompanyProfileSocialCreate],
company_profile_id: int,
current_user: User,
) -> List[CompanyProfileSocialRead]:
company_profile = await self.company_profiles_repository.get_by_id(company_profile_id)
if not company_profile:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Соискатель с таким ID не найден')
if company_profile.creator_user_id != current_user.id and not (
current_user.role.title == UserRoles.MODERATOR and current_user.is_admin
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='Доступ запрещен',
)
old_company_profile_socials = await self.company_profile_socials_repository.get_by_company_profile_id(
company_profile_id
)
await self.company_profile_socials_repository.delete_list(old_company_profile_socials)
company_profile_social_models = []
for company_profile_social in company_profile_socials:
company_profile_social_models.append(CompanyProfileSocial(
title=company_profile_social.title,
link=company_profile_social.link,
company_id=company_profile_id,
))
company_profile_social_models = await self.company_profile_socials_repository.create_list(
company_profile_social_models
)
response = []
for company_profile_social_model in company_profile_social_models:
response.append(CompanyProfileSocialRead.model_validate(company_profile_social_model))
return response

View File

@ -6,59 +6,42 @@ from sqlalchemy.orm import joinedload
from app.application.company_profiles_repository import CompanyProfilesRepository
from app.application.industries_repository import IndustriesRepository
from app.core.constants import UserRoles
from app.domain.models import CompanyProfile, CompanyProfileSocial, User
from app.domain.entities.company_profiles import CompanyProfileCreate, CompanyProfileRead
from app.domain.entities.company_profiles import CompanyProfileCreate, CompanyProfileRead, CompanyProfileUpdate
class CompanyProfilesService:
def __init__(self, db: AsyncSession):
self.db = db
self.repo = CompanyProfilesRepository(db)
self.industry_repo = IndustriesRepository(db)
self.company_profiles_repository = CompanyProfilesRepository(db)
self.industries_repository = IndustriesRepository(db)
async def get_company_by_creator_id(self, user_id: int) -> Optional[CompanyProfileRead]:
"""
Получить профиль компании по ID создателя со всеми связями.
"""
query = (
select(CompanyProfile)
.filter_by(creator_user_id=user_id)
.options(
joinedload(CompanyProfile.logo),
joinedload(CompanyProfile.official_photo),
joinedload(CompanyProfile.industry),
joinedload(CompanyProfile.socials)
)
)
result = await self.db.execute(query)
profile = result.scalars().first()
company_profile = await self.company_profiles_repository.get_company_by_creator_id(user_id)
if not profile:
return None
if not company_profile:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="У вас нет созданной компании"
)
return CompanyProfileRead.model_validate(profile)
return CompanyProfileRead.model_validate(company_profile)
async def create_company(self, data: CompanyProfileCreate, user: User) -> CompanyProfileRead:
"""
Создать новый профиль компании.
"""
# Проверка: нет ли уже профиля у этого пользователя
existing_profile = await self.get_company_by_creator_id(user.id)
existing_profile = await self.company_profiles_repository.get_company_by_creator_id(user.id)
if existing_profile:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="У вас уже создан профиль компании. Используйте обновление."
)
# Проверка существования индустрии
industry = await self.industry_repo.get_by_id(data.industry_id)
industry = await self.industries_repository.get_by_id(data.industry_id)
if not industry:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Указанная индустрия не найдена"
)
# Создание объекта модели
new_company = CompanyProfile(
title=data.title,
description=data.description,
@ -70,23 +53,50 @@ class CompanyProfilesService:
official_photo_id=data.official_photo_id,
industry_id=data.industry_id,
creator_user_id=user.id,
is_verified=False
)
# Сохранение в БД через репозиторий
created_company = await self.repo.create(new_company)
created_company = await self.company_profiles_repository.create(new_company)
# Добавление социальных сетей, если они переданы
if data.socials:
social_models = [
CompanyProfileSocial(
title=s.title,
link=s.link,
company_id=created_company.id
) for s in data.socials
]
await self.repo.add_socials(social_models)
result = await self.company_profiles_repository.get_by_id(created_company.id)
return CompanyProfileRead.model_validate(result)
async def update_company(self, company_id: int, data: CompanyProfileUpdate, current_user: User) -> CompanyProfileRead:
existing_profile = await self.company_profiles_repository.get_by_id(company_id)
if not existing_profile:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Компания с таким ID не найдена."
)
industry = await self.industries_repository.get_by_id(data.industry_id)
if not industry:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Указанная индустрия не найдена"
)
if existing_profile.creator_user_id != current_user.id and not (
current_user.role.title == UserRoles.MODERATOR and current_user.is_admin
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='Доступ запрещен',
)
existing_profile.title = data.title
existing_profile.description = data.description
existing_profile.website_url = data.website_url
existing_profile.inn = data.inn
existing_profile.corporate_email = str(data.corporate_email) if data.corporate_email else None
existing_profile.video_url = data.video_url
existing_profile.logo_id = data.logo_id
existing_profile.official_photo_id = data.official_photo_id
existing_profile.industry_id = data.industry_id
existing_profile.creator_user_id = current_user.id
existing_profile = await self.company_profiles_repository.update(existing_profile)
result = await self.company_profiles_repository.get_by_id(existing_profile.id)
# Возвращаем полный объект профиля (с подгруженными связями)
result = await self.repo.get_by_id(created_company.id)
return CompanyProfileRead.model_validate(result)

View File

@ -0,0 +1,80 @@
from typing import List
from fastapi import HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.application.company_profiles_repository import CompanyProfilesRepository
from app.application.verification_requests_repository import VerificationRequestSRepository
from app.core.constants import UserRoles
from app.domain.entities.verification_requests import VerificationRequestRead
from app.domain.models import User, VerificationRequest
class CompanyVerificationRequestsService:
def __init__(self, db: AsyncSession):
self.verification_requests_repository = VerificationRequestSRepository(db)
self.company_profiles_repository = CompanyProfilesRepository(db)
async def get_by_company_id(self, company_id: int, current_user: User) -> List[VerificationRequestRead]:
company_profile = await self.company_profiles_repository.get_by_id(company_id)
if not company_profile:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Компания с таким ID не найдена."
)
if company_profile.creator_user_id != current_user.id and not (
current_user.role.title == UserRoles.MODERATOR and current_user.is_admin
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='Доступ запрещен',
)
requests = await self.verification_requests_repository.get_by_company_profile_id(company_id)
response = []
for request in requests:
response.append(VerificationRequestRead.model_validate(request))
return response
async def create_verification_request(self, company_id: int, current_user: User) -> VerificationRequestRead:
company_profile = await self.company_profiles_repository.get_by_id(company_id)
if not company_profile:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Компания с таким ID не найдена."
)
if company_profile.creator_user_id != current_user.id and not (
current_user.role.title == UserRoles.MODERATOR and current_user.is_admin
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='Доступ запрещен',
)
if company_profile.is_verified:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Компания с таким ID уже верифицирована."
)
old_requests = await self.verification_requests_repository.get_by_company_profile_id(company_id)
for old_request in old_requests:
if old_request.is_accepted is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="У компании уже есть активная заявка на верификацию"
)
new_verification_request_model = VerificationRequest(
company_id=company_id,
)
new_verification_request_model = await self.verification_requests_repository.create(
new_verification_request_model
)
return VerificationRequestRead.model_validate(new_verification_request_model)

View File

@ -1,41 +1,42 @@
import os
import uuid
import aiofiles
from app.application.files_repository import FilesRepository
from fastapi import UploadFile
from werkzeug.utils import secure_filename
class FilesService:
def __init__(self, db):
self.repo = FilesRepository(db)
# Базовый путь относительно корня проекта
self.base_dir = "База данных/media"
async def save_file(self, file: UploadFile, upload_dir) -> str:
os.makedirs(upload_dir, exist_ok=True)
filename = self.generate_filename(file)
file_path = os.path.join(upload_dir, filename)
async def _save_to_disk(self, upload_file, sub_folder: str):
# Генерируем уникальное имя, чтобы не перезаписать файлы
unique_name = f"{uuid.uuid4()}_{upload_file.filename}"
relative_folder = os.path.join(sub_folder)
full_folder_path = os.path.join(self.base_dir, relative_folder)
async with aiofiles.open(file_path, 'wb') as out_file:
content = await file.read()
await out_file.write(content)
return file_path
os.makedirs(full_folder_path, exist_ok=True)
@staticmethod
def generate_filename(file: UploadFile) -> str:
return secure_filename(f"{uuid.uuid4()}_{file.filename}")
file_path = os.path.join(full_folder_path, unique_name)
# URL для раздачи статикой
web_url = f"/media/{sub_folder}/{unique_name}"
@staticmethod
def get_media_type(filename: str) -> str:
extension = filename.split('.')[-1].lower()
if extension in ['jpeg', 'jpg', 'png']:
return f"image/{extension}"
if extension == 'pdf':
return "application/pdf"
if extension in ['zip']:
return "application/zip"
if extension in ['doc', 'docx']:
return "application/msword"
if extension in ['xls', 'xlsx']:
return "application/vnd.ms-excel"
if extension in ['ppt', 'pptx']:
return "application/vnd.ms-powerpoint"
if extension in ['txt']:
return "text/plain"
content = await upload_file.read()
async with aiofiles.open(file_path, mode='wb') as f:
await f.write(content)
return {
"filename": upload_file.filename,
"path": file_path,
"url": web_url
}
async def upload_logo(self, file):
info = await self._save_to_disk(file, "company_logos")
return await self.repo.create_logo_record(**info)
async def upload_photo(self, file):
info = await self._save_to_disk(file, "company_photos")
return await self.repo.create_photo_record(**info)
return "application/octet-stream"

View File

@ -0,0 +1,22 @@
from typing import List
from sqlalchemy.ext.asyncio import AsyncSession
from app.application.industries_repository import IndustriesRepository
from app.domain.entities.industries import IndustryRead
class IndustriesService:
def __init__(self, db: AsyncSession):
self.industries_repository = IndustriesRepository(db)
async def get_all(self) -> List[IndustryRead]:
industries = await self.industries_repository.get_all()
response = []
for industry in industries:
response.append(IndustryRead.model_validate(
industry
))
return response

View File

@ -1,5 +1,4 @@
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import RedirectResponse
@ -8,16 +7,20 @@ from app.controllers.applicant_profiles_router import applicant_profiles_router
from app.controllers.applicant_skill_tags_router import applicant_skill_tags_router
from app.controllers.applicant_skills_router import applicant_skills_router
from app.controllers.auth_router import auth_router
from app.controllers.company_profile_logos_router import company_profile_logos_router
from app.controllers.company_profile_photos_router import company_profile_photos_router
from app.controllers.company_profile_socials_router import company_profile_socials_router
from app.controllers.company_profiles_router import company_profiles_router
from app.controllers.dictionaries_router import router as dictionaries_router
from app.controllers.experince_levels_router import experience_levels_router
from app.controllers.files_router import files_router
from app.controllers.industries_router import industries_router
from app.controllers.internships_router import internships_router
from app.controllers.files_router import files_router
from app.controllers.experince_levels_router import experience_levels_router
from app.controllers.roles_router import roles_router
from app.controllers.universities_router import universities_router
from app.controllers.users_router import users_router
from app.controllers.vacancies_router import vacancies_router
from app.controllers.verification_requests_router import verification_requests_router
from app.settings import Settings
@ -39,8 +42,11 @@ def start_app():
api_app.include_router(applicant_skill_tags_router, prefix=f'{settings.prefix}/applicant_skill_tags', tags=['applicant_skill_tags'])
api_app.include_router(applicant_skills_router, prefix=f'{settings.prefix}/applicant_skills', tags=['applicant_skills'])
api_app.include_router(auth_router, prefix=f'{settings.prefix}/auth', tags=['auth'])
api_app.include_router(company_profile_logos_router, prefix=f'{settings.prefix}/company_profile_logos', tags=['company_profile_logos'])
api_app.include_router(company_profile_photos_router, prefix=f'{settings.prefix}/company_profile_photos', tags=['company_profile_photos'])
api_app.include_router(company_profile_socials_router, prefix=f'{settings.prefix}/company_profile_socials', tags=['company_profile_socials'])
api_app.include_router(company_profiles_router, prefix=f'{settings.prefix}/company_profiles', tags=['company_profiles'])
api_app.include_router(industries_router, prefix=f'{settings.prefix}/company_profiles/industries', tags=['company_profiles'])
api_app.include_router(industries_router, prefix=f'{settings.prefix}/industries', tags=['company_profiles'])
api_app.include_router(internships_router, prefix=f'{settings.prefix}/internships', tags=["internships"])
api_app.include_router(files_router, prefix=f'{settings.prefix}/company_profiles/files', tags=['company_profiles'])
api_app.include_router(dictionaries_router, prefix=f'{settings.prefix}/vacancies/dictionaries', tags=['vacancies'])
@ -49,6 +55,7 @@ def start_app():
api_app.include_router(universities_router, prefix=f'{settings.prefix}/universities', tags=['universities'])
api_app.include_router(users_router, prefix=f'{settings.prefix}/users', tags=['users'])
api_app.include_router(vacancies_router, prefix=f'{settings.prefix}/vacancies', tags=['vacancies'])
api_app.include_router(verification_requests_router, prefix=f'{settings.prefix}/verification_requests', tags=['verification_requests'])
return api_app

View File

@ -1,7 +1,6 @@
import {fetchBaseQuery} from '@reduxjs/toolkit/query/react';
import {logout} from '../Redux/Slices/authSlice.js';
import CONFIG from "../Core/сonfig.js";
import {notification} from "antd";
export const baseQuery = fetchBaseQuery({
baseUrl: CONFIG.BASE_URL,

View File

@ -1,16 +1,16 @@
import { createApi } from '@reduxjs/toolkit/query/react';
import { baseQueryWithAuth } from "./baseQuery.js";
import {createApi} from '@reduxjs/toolkit/query/react';
import {baseQueryWithAuth} from "./baseQuery.js";
export const companyApi = createApi({
reducerPath: 'companyApi',
baseQuery: baseQueryWithAuth, // Просто ссылка на функцию
tagTypes: ['Company'],
baseQuery: baseQueryWithAuth,
tagTypes: ['Company', 'CompanyProfileLogos', 'CompanyProfilePhotos'],
endpoints: (builder) => ({
getProfile: builder.query({
query: () => '/company_profiles/me',
getCompanyProfile: builder.query({
query: () => '/company_profiles/me/',
providesTags: ['Company'],
}),
createOrUpdateCompany: builder.mutation({
createCompanyProfile: builder.mutation({
query: (data) => ({
url: '/company_profiles/',
method: 'POST',
@ -18,7 +18,69 @@ export const companyApi = createApi({
}),
invalidatesTags: ['Company'],
}),
updateCompanyProfile: builder.mutation({
query: ({companyProfileId, data}) => ({
url: `/company_profiles/${companyProfileId}/`,
method: 'PUT',
body: data,
}),
invalidatesTags: ['Company'],
}),
uploadCompanyProfileLogo: builder.mutation({
query: ({companyProfileId, fileData}) => {
if (!(fileData instanceof File)) {
throw new Error('Invalid file object');
}
const formData = new FormData();
formData.append('file', fileData);
return {
url: `/company_profile_logos/files/${companyProfileId}/upload/`,
method: 'POST',
formData: true,
body: formData,
};
},
invalidatesTags: ["CompanyProfileLogos", "Company"],
}),
deleteCompanyProfileLogo: builder.mutation({
query: (fileId) => ({
url: `/company_profile_logos/files/${fileId}/`,
method: "DELETE",
}),
invalidatesTags: ["CompanyProfileLogos", "Company"],
}),
uploadCompanyProfilePhoto: builder.mutation({
query: ({companyProfileId, fileData}) => {
if (!(fileData instanceof File)) {
throw new Error('Invalid file object');
}
const formData = new FormData();
formData.append('file', fileData);
return {
url: `/company_profile_photos/files/${companyProfileId}/upload/`,
method: 'POST',
formData: true,
body: formData,
};
},
invalidatesTags: ["CompanyProfilePhotos", "Company"],
}),
deleteCompanyProfilePhoto: builder.mutation({
query: (fileId) => ({
url: `/company_profile_photos/files/${fileId}/`,
method: "DELETE",
}),
invalidatesTags: ["CompanyProfilePhotos", "Company"],
}),
}),
});
export const { useGetProfileQuery, useCreateOrUpdateCompanyMutation } = companyApi;
export const {
useGetCompanyProfileQuery,
useCreateCompanyProfileMutation,
useUpdateCompanyProfileMutation,
useUploadCompanyProfileLogoMutation,
useDeleteCompanyProfileLogoMutation,
useUploadCompanyProfilePhotoMutation,
useDeleteCompanyProfilePhotoMutation,
} = companyApi;

View File

@ -0,0 +1,28 @@
import {createApi} from "@reduxjs/toolkit/query/react";
import {baseQueryWithAuth} from "./baseQuery.js";
export const companyProfileSocialsApi = createApi({
reducerPath: "companyProfileSocialsApi",
baseQuery: baseQueryWithAuth,
tagTypes: ["CompanyProfileSocials"],
endpoints: (builder) => ({
getCompanyProfileSocialsByCompanyProfileId: builder.query({
query: (companyProfileId) => `/company_profile_socials/${companyProfileId}/`,
providesTags: ["CompanyProfileSocials"],
}),
replaceCompanyProfileSocials: builder.mutation({
query: ({companyProfileId, companyProfileSocials}) => ({
url: `/company_profile_socials/${companyProfileId}/`,
method: "POST",
body: companyProfileSocials,
}),
invalidatesTags: ["CompanyProfileSocials"],
}),
}),
});
export const {
useGetCompanyProfileSocialsByCompanyProfileIdQuery,
useReplaceCompanyProfileSocialsMutation
} = companyProfileSocialsApi;

View File

@ -4,14 +4,11 @@ import { baseQueryWithAuth } from "./baseQuery.js";
export const industriesApi = createApi({
reducerPath: 'industriesApi',
baseQuery: baseQueryWithAuth,
tagTypes: ['industries'],
endpoints: (builder) => ({
getAllIndustries: builder.query({
query: () => '/company_profiles/industries/',
transformResponse: (response) =>
response.map(item => ({
label: item.title,
value: item.id
})),
query: () => '/industries/',
providesTags: ['industries'],
}),
}),
});

View File

@ -0,0 +1,27 @@
import {createApi} from "@reduxjs/toolkit/query/react";
import {baseQueryWithAuth} from "./baseQuery.js";
export const verificationRequestsApi = createApi({
reducerPath: "verificationRequestsApi",
baseQuery: baseQueryWithAuth,
tagTypes: ["verificationRequests"],
endpoints: (builder) => ({
getVerificationRequestsByCompanyProfileId: builder.query({
query: (companyId) => `/verification_requests/${companyId}/`,
providesTags: ["verificationRequests"],
}),
createVerificationRequest: builder.mutation({
query: (companyId) => ({
url: `/verification_requests/${companyId}/`,
method: "POST",
}),
injectTags: ["verificationRequests"],
}),
}),
});
export const {
useGetVerificationRequestsByCompanyProfileIdQuery,
useCreateVerificationRequestMutation,
} = verificationRequestsApi;

View File

@ -73,6 +73,7 @@ const AppLayout = () => {
onClick={handleMenuClick}
/>
{user?.role?.title === ROLES.APPLICANT && (
<Tooltip title="Избранное">
<Button type="text" onClick={handleFavoritesClick} style={{paddingInline: 10}}>
<Badge count={favoritesCount} overflowCount={99} size="small">
@ -80,6 +81,7 @@ const AppLayout = () => {
</Badge>
</Button>
</Tooltip>
)}
{user ? (
<Space

View File

@ -1,76 +1,47 @@
import { useEffect, useMemo } from 'react';
import { Form, notification } from 'antd';
import {useEffect, useMemo} from 'react';
import {Form, notification} from 'antd';
import useEditor from "../../../../../../../Hook/useEditor.js";
import { useSelector } from "react-redux";
import {useSelector} from "react-redux";
import dayjs from "dayjs";
import { useGetAllUniversitiesQuery } from "../../../../../../../Api/universitiesApi.js";
import {useGetAllUniversitiesQuery} from "../../../../../../../Api/universitiesApi.js";
import {
useGetApplicantEducationsByApplicantIdQuery,
useReplaceApplicantEducationsMutation
} from "../../../../../../../Api/applicantEducationsApi.js";
import { useUpdateUserMutation } from "../../../../../../../Api/usersApi.js";
import { useUpdateApplicantProfileMutation } from "../../../../../../../Api/applicantProfilesApi.js";
import {useUpdateUserMutation} from "../../../../../../../Api/usersApi.js";
import {useUpdateApplicantProfileMutation} from "../../../../../../../Api/applicantProfilesApi.js";
import {
useGetApplicantSkillsByApplicantIdQuery,
useReplaceApplicantSkillsMutation
} from "../../../../../../../Api/applicantSkillsApi.js";
import { useGetAllApplicantSkillTagsQuery } from "../../../../../../../Api/applicantSkillTagsApi.js";
import { useGetAllExperienceLevelsQuery } from "../../../../../../../Api/experienceLevelsApi.js";
import {useGetAllApplicantSkillTagsQuery} from "../../../../../../../Api/applicantSkillTagsApi.js";
import {useGetAllExperienceLevelsQuery} from "../../../../../../../Api/experienceLevelsApi.js";
const getStorageKey = (userId) => userId ? `profile_draft_user_${userId}` : null;
const useProfileTab = () => {
const [form] = Form.useForm();
const { editorRef, joditConfig, getContent, setContent } = useEditor();
const { userData } = useSelector((state) => state.auth);
const {editorRef, joditConfig, getContent, setContent} = useEditor();
const {userData} = useSelector((state) => state.auth);
const { data: universities = [] } = useGetAllUniversitiesQuery();
const { data: skillTags = [] } = useGetAllApplicantSkillTagsQuery();
const { data: experienceLevels = [] } = useGetAllExperienceLevelsQuery();
const {data: universities = []} = useGetAllUniversitiesQuery();
const {data: skillTags = []} = useGetAllApplicantSkillTagsQuery();
const {data: experienceLevels = []} = useGetAllExperienceLevelsQuery();
const userId = userData?.id;
const applicantId = userData?.applicant_profile?.id;
const currentStorageKey = useMemo(() => getStorageKey(userId), [userId]);
const { data: educationsData = [] } = useGetApplicantEducationsByApplicantIdQuery(applicantId, { skip: !applicantId });
const { data: skillsData = [] } = useGetApplicantSkillsByApplicantIdQuery(applicantId, { skip: !applicantId });
const [updateUser, { isLoading: updatingUser }] = useUpdateUserMutation();
const [replaceEducations, { isLoading: updatingEducations }] = useReplaceApplicantEducationsMutation();
const [replaceSkills, { isLoading: updatingSkills }] = useReplaceApplicantSkillsMutation();
const [updateApplicantProfile, { isLoading: updatingApplicantProfile }] = useUpdateApplicantProfileMutation();
const {data: educationsData = []} = useGetApplicantEducationsByApplicantIdQuery(applicantId, {skip: !applicantId});
const {data: skillsData = []} = useGetApplicantSkillsByApplicantIdQuery(applicantId, {skip: !applicantId});
const [updateUser, {isLoading: updatingUser}] = useUpdateUserMutation();
const [replaceEducations, {isLoading: updatingEducations}] = useReplaceApplicantEducationsMutation();
const [replaceSkills, {isLoading: updatingSkills}] = useReplaceApplicantSkillsMutation();
const [updateApplicantProfile, {isLoading: updatingApplicantProfile}] = useUpdateApplicantProfileMutation();
useEffect(() => {
if (!currentStorageKey) return;
const saved = localStorage.getItem(currentStorageKey);
if (saved) {
try {
const draft = JSON.parse(saved);
const restoredValues = { ...draft };
if (restoredValues.birthdate) {
restoredValues.birthdate = dayjs(restoredValues.birthdate);
}
form.setFieldsValue(restoredValues);
if (restoredValues.resume_html) {
setContent(restoredValues.resume_html);
}
console.log("Данные восстановлены из локального черновика пользователя:", userId);
} catch (e) {
console.error("Ошибка парсинга локальных данных", e);
}
}
}, [currentStorageKey, form, setContent, userId]);
useEffect(() => {
const hasDraft = currentStorageKey ? localStorage.getItem(currentStorageKey) : null;
if (userData && !hasDraft) {
const values = { ...userData, resume_url: userData?.applicant_profile?.resume_url };
if (userData) {
const values = {...userData, resume_url: userData?.applicant_profile?.resume_url};
if (values.birthdate) {
values.birthdate = dayjs(values.birthdate);
@ -95,7 +66,7 @@ const useProfileTab = () => {
setContent(userData.applicant_profile.resume_html);
}
}
}, [userData, educationsData, skillsData, form, setContent, currentStorageKey]);
}, [userData, educationsData, skillsData, form, setContent]);
const handleSave = async (values) => {
const resumeHtml = getContent();
@ -120,48 +91,33 @@ const useProfileTab = () => {
try {
await Promise.all([
updateUser({ userId, ...userPayload }).unwrap(),
replaceEducations({ applicantId, applicantEducations: values.educations || [] }).unwrap(),
updateApplicantProfile({ applicantId, ...applicantProfilePayload }).unwrap(),
replaceSkills({ applicantId, applicantSkills: skillsPayload }).unwrap(),
updateUser({userId, ...userPayload}).unwrap(),
replaceEducations({applicantId, applicantEducations: values.educations || []}).unwrap(),
updateApplicantProfile({applicantId, ...applicantProfilePayload}).unwrap(),
replaceSkills({applicantId, applicantSkills: skillsPayload}).unwrap(),
]);
if (currentStorageKey) {
localStorage.removeItem(currentStorageKey);
}
notification.success({
message: 'Успешно',
description: 'Профиль обновлен',
});
} catch (err) {
if (currentStorageKey) {
const backup = {
...values,
resume_html: resumeHtml,
birthdate: values.birthdate ? values.birthdate.toISOString() : null
};
localStorage.setItem(currentStorageKey, JSON.stringify(backup));
}
} catch {
notification.success({
message: 'Успешно',
description: 'Изменения сохранены',
});
console.warn("Данные сохранены:", userId, );
}
};
const universitiesOptions = useMemo(() =>
universities.map(i => ({ value: i.id, label: i.title })), [universities]
universities.map(i => ({value: i.id, label: i.title})), [universities]
);
const applicantSkillTagsOptions = useMemo(() =>
skillTags.map(i => ({ value: i.id, label: i.title })), [skillTags]
skillTags.map(i => ({value: i.id, label: i.title})), [skillTags]
);
const experienceLevelsOptions = useMemo(() =>
experienceLevels.map(i => ({ value: i.id, label: i.title })), [experienceLevels]
experienceLevels.map(i => ({value: i.id, label: i.title})), [experienceLevels]
);
return {

View File

@ -1,192 +1,296 @@
import React from 'react'
import {
Button, Col, Divider, Form, Input, Row,
Select, Skeleton, Space, Typography, Upload,
Alert, Avatar, Badge,
Button, Col, Divider, Flex, Form, Image, Input, Row,
Select, Skeleton, Space, Spin, Tag, Typography, Upload,
} from 'antd'
import { DeleteOutlined, PlusOutlined, UploadOutlined } from '@ant-design/icons'
import {
DeleteOutlined,
FileImageOutlined,
InfoOutlined,
PlusOutlined,
UploadOutlined,
UserOutlined
} from '@ant-design/icons'
import useCompanyTab from './useCompanyTab'
import LoadingIndicator from "../../../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import CONFIG from "../../../../../../../Core/сonfig.js";
const { Title, Text } = Typography
const CompanyTab = () => {
const {
form, verForm, isLoading, isSaving, handleSave,
dictionaries, verificationStatus, submitVerification,
companyTitle,
form,
isLoading,
handleSave,
industriesItems,
companyData,
moderationStatus,
handleUploadCompanyProfileLogo,
handleDeleteCompanyProfileLogo,
handleUploadCompanyProfilePhoto,
handleDeleteCompanyProfilePhoto,
isSuspendForLogoShowing,
isSuspendForPhotoShowing,
verificationRequestsData,
handleCreateVerificationRequest,
} = useCompanyTab()
if (isLoading) return <Skeleton active paragraph={{ rows: 10 }} />
return (
<Space direction="vertical" size={16} style={{ width: '100%' }}>
<Spin indicator={<LoadingIndicator/>} spinning={isLoading}>
<Divider orientation="left">Верификация компании</Divider>
<div style={{
padding: 16, borderRadius: 10,
border: `1px solid ${verificationStatus === 'approved' ? '#b7eb8f' : verificationStatus === 'pending' ? '#ffe58f' : '#d9d9d9'}`,
background: verificationStatus === 'approved' ? '#f6ffed' : verificationStatus === 'pending' ? '#fffbe6' : '#fafafa',
}}>
<Title level={5} style={{ marginTop: 0, marginBottom: 12 }}>Статус верификации компании</Title>
<p style={{ marginBottom: 16 }}>
<strong>Текущий статус:</strong>{' '}
<Text type={verificationStatus === 'approved' ? 'success' : 'warning'}>
{verificationStatus === 'approved' ? 'Верифицирована ✓' :
verificationStatus === 'pending' ? 'На рассмотрении' :
verificationStatus === 'rejected' ? 'Отклонена — подайте повторно' :
'Не верифицирована'}
</Text>
</p>
{verificationStatus !== 'approved' && (
<Form layout="vertical" form={verForm} onFinish={submitVerification}>
<Form.Item label="Название компании" name="company_name" rules={[{ required: true, message: 'Введите название' }]}>
<Input placeholder="Как в профиле" />
</Form.Item>
<Row gutter={16}>
<Col xs={24} md={12}>
<Form.Item name="corporate_email" label="Корпоративная почта" rules={[{ required: true, type: 'email', message: 'Введите email' }]}>
<Input placeholder="hr@company.ru" />
</Form.Item>
</Col>
</Row>
<Form.Item name="links" label="Ссылки (сайт, соцсети)">
<Input.TextArea rows={2} placeholder="https://..." />
</Form.Item>
<Button type="primary" htmlType="submit" disabled={verificationStatus === 'pending'}>
Отправить на проверку
</Button>
</Form>
{companyData === null && (
<Alert
type="info"
title={"У вас еще нет компании. Создайте ее, чтобы добавлять вакансии и другие возможности!"}
/>
)}
</div>
{companyData !== null && !companyData?.is_verified && !verificationRequestsData.length && (
<Alert
type="info"
title={"У вас создана компания. Отправьте запрос на модерацию, чтобы вы могли создавать вакансии и другие возможности!"}
/>
)}
{companyData !== null && (
<Space orientation={"vertical"}>
<Space
orientation={"horizontal"}
style={{
marginTop: "10px"
}}
>
<Typography.Text type="secondary">
Статус верификации:
</Typography.Text>
<Tag
color={companyData?.is_verified ? "green" : "red"}
>
<Badge
status={companyData?.is_verified ? "success" : "warning"}
text={moderationStatus.status}
/>
</Tag>
</Space>
{moderationStatus.showSendRequestButton && (
<Button
type="primary"
onClick={handleCreateVerificationRequest}
loading={isLoading}
>
Отправить запрос на верификацию компании
</Button>
)}
</Space>
)}
<Space orientation="vertical" size={16} style={{width: '100%'}}>
<Form layout="vertical" form={form} onFinish={handleSave}>
{/* Исправлено: orientation -> titlePlacement */}
<Divider orientation="left" titlePlacement="left">Основная информация</Divider>
<Row gutter={24}>
<Col xs={24} md={16}>
<Form.Item label="Наименование" name="name" rules={[{ required: true, message: 'Введите название' }]}>
<Input placeholder="ООО «Ромашка»" size="large" />
<Form.Item
label="Наименование"
name="title"
rules={[{required: true, message: 'Введите название'}]}
>
<Input placeholder="ООО «Ромашка»" size="large"/>
</Form.Item>
</Col>
<Col xs={24} md={8}>
<Form.Item label="ИНН" name="inn" rules={[{ required: true, message: 'Введите ИНН' }]}>
<Input placeholder="7707083893" size="large" />
<Form.Item
label="ИНН"
name="inn"
rules={[{required: true, message: 'Введите ИНН'}]}
>
<Input placeholder="7707083893" size="large"/>
</Form.Item>
</Col>
</Row>
<Form.Item label="Краткое описание" name="about">
<Input.TextArea rows={4} placeholder="Расскажите о компании..." />
<Form.Item
label="Краткое описание"
name="description"
>
<Input.TextArea rows={4} placeholder="Расскажите о компании..."/>
</Form.Item>
<Row gutter={24}>
<Col xs={24} md={12}>
<Form.Item label="Сфера деятельности" name="area" rules={[{ required: true, message: 'Выберите сферу деятельности' }]}>
<Form.Item
label="Сфера деятельности"
name="industry_id"
rules={[{required: true, message: 'Выберите сферу деятельности'}]}
>
<Select
showSearch
options={dictionaries}
showSearch={{
filterOption: (input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase()),
optionFilterProp: "label"
}}
options={industriesItems}
placeholder="Выберите сферу"
size="large"
optionFilterProp="label"
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
/>
</Form.Item>
</Col>
<Col xs={24} md={12}>
<Form.Item label="Корпоративный email" name="corporate_email" rules={[{ type: 'email' }]}>
<Input placeholder="hr@company.ru" size="large" />
<Form.Item label="Корпоративный email" name="corporate_email" rules={[{type: 'email'}]}>
<Input placeholder="hr@company.ru" size="large"/>
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col xs={24} md={12}>
<Form.Item label="Сайт" name="site">
<Input placeholder="https://example.com" size="large" />
<Form.Item label="Сайт" name="website_url">
<Input placeholder="https://example.com" size="large"/>
</Form.Item>
</Col>
<Col xs={24} md={12}>
<Form.Item label="Видеопрезентация (ссылка)" name="video_url">
<Input placeholder="https://youtube.com/..." size="large" />
<Input placeholder="https://youtube.com/..." size="large"/>
</Form.Item>
</Col>
</Row>
<Divider orientation="left">Социальные сети</Divider>
<Form.List name="socials">
{(fields, { add, remove }) => (
{(fields, {add, remove}) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Row key={key} gutter={24} align="bottom" style={{ marginBottom: 12 }}>
{fields.map(({key, name, ...restField}) => (
<Row key={key} gutter={24} align="bottom" style={{marginBottom: 12}}>
<Col xs={10} md={8}>
<Form.Item {...restField} name={[name, 'title']}
label={name === 0 ? 'Название сети' : ''} style={{ marginBottom: 0 }}>
<Input placeholder="ВКонтакте" size="large" />
<Form.Item
{...restField}
name={[name, 'title']}
label={name === 0 ? 'Название сети' : ''}
style={{marginBottom: 0}}
rules={[{required: true, message: "Введите название соцсети"}]}
>
<Input placeholder="ВКонтакте" size="large"/>
</Form.Item>
</Col>
<Col xs={11} md={14}>
<Form.Item {...restField} name={[name, 'link']}
label={name === 0 ? 'Ссылка' : ''} style={{ marginBottom: 0 }}>
<Input placeholder="https://vk.com/..." size="large" />
<Form.Item
{...restField}
name={[name, 'link']}
label={name === 0 ? 'Ссылка' : ''}
style={{marginBottom: 0}}
rules={[{required: true, message: "Вставьте ссылку"}]}
>
<Input placeholder="https://vk.com/..." size="large"/>
</Form.Item>
</Col>
<Col xs={3} md={2}>
<Button type="text" danger onClick={() => remove(name)}
icon={<DeleteOutlined />} style={{ height: 40 }} />
icon={<DeleteOutlined/>} style={{height: 40}}/>
</Col>
</Row>
))}
<Form.Item>
<Button type="dashed" onClick={() => add()} icon={<PlusOutlined />} block size="large">
<Button type="dashed" onClick={() => add()} icon={<PlusOutlined/>} block
size="large">
Добавить соцсеть
</Button>
</Form.Item>
</>
)}
</Form.List>
{companyData !== null && companyData?.id !== undefined && (
<>
<Divider orientation="left">Медиа-файлы</Divider>
<Row gutter={24}>
<Col xs={24} md={12}>
<Form.Item name="logo_id" hidden><Input /></Form.Item>
<Form.Item label="Логотип" name="logo_file" valuePropName="fileList"
getValueFromEvent={e => Array.isArray(e) ? e : e?.fileList}>
<Upload action="http://localhost:5000/api/v1/company_profiles/files/upload-logo"
headers={{ Authorization: `Bearer ${localStorage.getItem('access_token')}` }}
onChange={(info) => {
if (info.file.status === 'done') {
form.setFieldValue('logo_id', info.file.response?.id);
}
<Space orientation="vertical">
{companyData?.logo_id && !isSuspendForLogoShowing ? (
<Image
src={`${CONFIG.BASE_URL}/company_profile_logos/file/${companyData?.logo_id}/`}
width={250}
/>
) : (
<Flex
justify={'center'}
>
<Avatar size={100} icon={<UserOutlined/>}
style={{backgroundColor: "#1890ff"}}/>
</Flex>
)}
<Upload
showUploadList={false}
accept=".jpg,.jpeg,.png"
beforeUpload={async (file) => {
await handleUploadCompanyProfileLogo(companyData?.id, file);
return false;
}}
listType="picture" maxCount={1} accept=".jpg,.jpeg,.png">
<Button icon={<UploadOutlined />} size="large" block>Загрузить логотип</Button>
>
<Button icon={<UploadOutlined/>} size="large"
block>{companyData?.logo_id ? "Загрузить новый логотип" : "Загрузить логотип"}</Button>
</Upload>
</Form.Item>
{companyData?.logo_id && (
<Button
onClick={() => handleDeleteCompanyProfileLogo(companyData.logo_id)}
type={"primary"}
danger
block
>
Удалить логотип
</Button>
)}
</Space>
</Col>
<Col xs={24} md={12}>
<Form.Item name="official_photo_id" hidden><Input /></Form.Item>
<Form.Item label="Фото офиса" name="photo_file" valuePropName="fileList"
getValueFromEvent={e => Array.isArray(e) ? e : e?.fileList}>
<Upload action="http://localhost:5000/api/v1/company_profiles/files/upload-photo"
headers={{ Authorization: `Bearer ${localStorage.getItem('access_token')}` }}
onChange={(info) => {
if (info.file.status === 'done') {
form.setFieldValue('official_photo_id', info.file.response?.id);
}
<Space orientation="vertical">
{companyData?.official_photo_id && !isSuspendForPhotoShowing ? (
<Image
src={`${CONFIG.BASE_URL}/company_profile_photos/file/${companyData?.official_photo_id}/`}
width={250}
/>
) : (
<Flex
justify={'center'}
>
<Avatar size={100} icon={<FileImageOutlined/>}
style={{backgroundColor: "#1890ff"}}/>
</Flex>
)}
<Upload
showUploadList={false}
accept=".jpg,.jpeg,.png"
beforeUpload={async (file) => {
await handleUploadCompanyProfilePhoto(companyData?.id, file);
return false;
}}
listType="picture" maxCount={1} accept=".jpg,.jpeg,.png">
<Button icon={<UploadOutlined />} size="large" block>Загрузить фото</Button>
>
<Button icon={<UploadOutlined/>} size="large"
block>{companyData?.official_photo_id ? "Загрузить новое фото профиля" : "Загрузить фото профиля"}</Button>
</Upload>
</Form.Item>
{companyData?.official_photo_id && (
<Button
onClick={() => handleDeleteCompanyProfilePhoto(companyData.official_photo_id)}
type={"primary"}
danger
block
>
Удалить изображение профиля
</Button>
)}
</Space>
</Col>
</Row>
</>
)}
<Button type="primary" htmlType="submit" loading={isSaving} size="large" style={{ minWidth: 200, marginTop: 24, borderRadius: 8 }}>
<Button type="primary" htmlType="submit" loading={isLoading} size="large"
style={{minWidth: 200, marginTop: 24, borderRadius: 8}}>
Сохранить изменения
</Button>
</Form>
</Space>
</Spin>
)
}

View File

@ -1,73 +1,155 @@
import { useEffect, useMemo } from 'react';
import { Form, notification } from 'antd';
import { useSelector } from 'react-redux';
import { useGetProfileQuery, useCreateOrUpdateCompanyMutation } from "../../../../../../../Api/companyApi";
import { useGetAllIndustriesQuery } from "../../../../../../../Api/industriesApi";
import {useEffect, useMemo, useState} from 'react';
import {Form, notification} from 'antd';
import {
submitEmployerVerification,
getEmployerVerificationStatus,
} from "../../../../../../../local/tramplinStore.js";
useGetCompanyProfileQuery,
useCreateCompanyProfileMutation,
useUpdateCompanyProfileMutation, useUploadCompanyProfileLogoMutation, useDeleteCompanyProfileLogoMutation,
useUploadCompanyProfilePhotoMutation, useDeleteCompanyProfilePhotoMutation
} from "../../../../../../../Api/companyApi";
import {useGetAllIndustriesQuery} from "../../../../../../../Api/industriesApi";
import {
useGetCompanyProfileSocialsByCompanyProfileIdQuery, useReplaceCompanyProfileSocialsMutation
} from "../../../../../../../Api/companyProfileSocialsApi.js";
import {
useCreateVerificationRequestMutation,
useGetVerificationRequestsByCompanyProfileIdQuery
} from "../../../../../../../Api/verificationRequestsApi.js";
const useCompanyTab = () => {
const [form] = Form.useForm();
const [verForm] = Form.useForm();
const { userData } = useSelector((s) => s.auth);
const [isSuspendForLogoShowing, setIsSuspendForLogoShowing] = useState(false);
const [isSuspendForPhotoShowing, setIsSuspendForPhotoShowing] = useState(false);
const { data: industries = [], isLoading: isDictLoading } = useGetAllIndustriesQuery();
const { data: companyData, isLoading: isFetching, refetch } = useGetProfileQuery();
const [saveCompany, { isLoading: isSaving }] = useCreateOrUpdateCompanyMutation();
const {
data: industries = [],
isLoading: isIndustriesLoading,
} = useGetAllIndustriesQuery(undefined, {
pullingInterval: 10000,
});
const {
data: companyData = null,
isLoading: isMyCompanyLoading,
} = useGetCompanyProfileQuery();
const [
createCompanyProfile,
{isLoading: isCreating}
] = useCreateCompanyProfileMutation();
const [
updateCompanyProfile,
{isLoading: isUpdating}
] = useUpdateCompanyProfileMutation();
const dictionaries = industries;
const {
data: socialsData = [],
isLoading: isSocialDataLoading,
} = useGetCompanyProfileSocialsByCompanyProfileIdQuery(
companyData?.id,
{
skip: !companyData?.id,
}
);
const {
data: verificationRequestsData = [],
isLoading: isVerificationRequestsDataLoading,
} = useGetVerificationRequestsByCompanyProfileIdQuery(
companyData?.id,
{
skip: !companyData?.id,
pollingInterval: 10000,
}
);
const [
createVerificationRequest,
{isLoading: isCreatingVerificationRequest}
] = useCreateVerificationRequestMutation();
const [
uploadCompanyProfileLogo,
{isLoading: isUploadingCompanyProfileLogo}
] = useUploadCompanyProfileLogoMutation();
const [
deleteCompanyProfileLogo,
{isLoading: isDeletingCompanyProfileLogo}
] = useDeleteCompanyProfileLogoMutation();
const [
uploadCompanyProfilePhoto,
{isLoading: isUploadingCompanyProfilePhoto}
] = useUploadCompanyProfilePhotoMutation();
const [
deleteCompanyProfilePhoto,
{isLoading: isDeletingCompanyProfilePhoto}
] = useDeleteCompanyProfilePhotoMutation();
const [
replaceCompanyProfileSocials,
{isLoading: isReplacingCompanyProfileSocials}
] = useReplaceCompanyProfileSocialsMutation();
const getCompanyModerationStatus = () => {
if (companyData?.is_verified) {
return {
status: "Компания верифицирована",
showSendRequestButton: false,
};
} else if (!verificationRequestsData.length) {
return {
status: "Необходимо отправить заявку на верификацию",
showSendRequestButton: true,
};
} else {
const isAllRejected = verificationRequestsData.filter((verification_request) => (
!verification_request.is_accepted && verification_request.is_accepted !== null
));
if (isAllRejected.length) {
return {
status: "Заявка на модерацию отклонена",
showSendRequestButton: true,
};
} else {
return {
status: "Ожидание модерации компании",
showSendRequestButton: false,
};
}
}
};
const moderationStatus = useMemo(getCompanyModerationStatus, [companyData, verificationRequestsData])
useEffect(() => {
if (companyData) {
form.setFieldsValue({
name: companyData.title,
const values = {
title: companyData.title,
inn: companyData.inn,
about: companyData.description,
area: companyData.industry_id ? Number(companyData.industry_id) : undefined,
description: companyData.description,
industry_id: companyData.industry_id,
corporate_email: companyData.corporate_email,
site: companyData.website_url,
website_url: companyData.website_url,
video_url: companyData.video_url,
socials: companyData.socials || [],
logo_id: companyData.logo?.id,
official_photo_id: companyData.official_photo?.id,
logo_file: companyData.logo ? [{
uid: '-1',
name: companyData.logo.filename,
status: 'done',
url: `http://localhost:5000${companyData.logo.url}`,
}] : [],
photo_file: companyData.official_photo ? [{
uid: '-2',
name: companyData.official_photo.filename,
status: 'done',
url: `http://localhost:5000${companyData.official_photo.url}`,
}] : [],
});
}
}, [companyData, form, industries]);
const handleSave = async (values) => {
try {
const payload = {
title: values.name,
description: values.about,
inn: values.inn,
website_url: values.site,
corporate_email: values.corporate_email,
video_url: values.video_url,
industry_id: Number(values.area),
socials: (values.socials || []).map(s => ({ title: s.title, link: s.link })),
logo_id: values.logo_id,
official_photo_id: values.official_photo_id,
};
await saveCompany(payload).unwrap();
notification.success({ message: 'Успешно', description: 'Профиль компании обновлен' });
refetch();
values.socials = (socialsData || []).map((social) => ({
title: social.title,
link: social.link,
}));
form.setFieldsValue(values);
}
}, [companyData, form, industries, socialsData]);
const handleCreateVerificationRequest = async () => {
try {
await createVerificationRequest(companyData?.id);
notification.success({
message: 'Успешно',
description: 'Заявка на верификацию отправлена'
});
} catch (error) {
notification.error({
message: 'Ошибка',
@ -76,37 +158,210 @@ const useCompanyTab = () => {
}
};
const verificationStatus = userData?.id != null ? getEmployerVerificationStatus(userData.id) : 'none';
const submitVerification = async (values) => {
const handleDeleteCompanyProfileLogo = async (companyId) => {
try {
submitEmployerVerification({
userId: userData.id,
company: companyData?.title || values.company_name,
inn: values.inn || companyData?.inn,
corporateEmail: values.corporate_email,
links: values.links,
});
await deleteCompanyProfileLogo(companyId).unwrap();
notification.success({
message: 'Заявка отправлена',
description: 'Куратор проверит данные и подтвердит компанию.',
message: 'Успешно',
description: 'Фото профиля удалено'
});
// TODO найти способ обновления логотипа
setTimeout(() => {
setIsSuspendForLogoShowing(!isSuspendForLogoShowing);
}, 3000);
} catch (error) {
notification.error({
message: 'Ошибка',
description: error.data?.detail?.[0]?.msg || error.data?.detail || 'Не удалось сохранить данные',
});
verForm.resetFields();
} catch (e) {
console.error(e);
}
};
const handleUploadCompanyProfileLogo = async (companyProfileId, file) => {
try {
await uploadCompanyProfileLogo({
companyProfileId,
fileData: file,
}).unwrap();
notification.success({
message: 'Успешно',
description: 'Фото профиля загружено'
});
// TODO найти способ обновления логотипа
setIsSuspendForLogoShowing(true);
setTimeout(() => {
setIsSuspendForLogoShowing(false);
}, 3000);
} catch (error) {
console.error(error);
const errorMessage = error.data?.detail
? JSON.stringify(error.data.detail, null, 2)
: JSON.stringify(error.data || error.message || "Неизвестная ошибка", null, 2);
notification.error({
title: "Ошибка загрузки файла",
description: `Не удалось загрузить файл ${file.name}: ${errorMessage}`,
placement: "topRight",
});
}
};
const handleUploadCompanyProfilePhoto = async (companyProfileId, file) => {
try {
await uploadCompanyProfilePhoto({
companyProfileId,
fileData: file,
}).unwrap();
notification.success({
message: 'Успешно',
description: 'Фото профиля загружено'
});
// TODO найти способ обновления фото профиля
setIsSuspendForPhotoShowing(true);
setTimeout(() => {
setIsSuspendForPhotoShowing(false);
}, 3000);
} catch (error) {
console.error(error);
const errorMessage = error.data?.detail
? JSON.stringify(error.data.detail, null, 2)
: JSON.stringify(error.data || error.message || "Неизвестная ошибка", null, 2);
notification.error({
title: "Ошибка загрузки файла",
description: `Не удалось загрузить файл ${file.name}: ${errorMessage}`,
placement: "topRight",
});
}
};
const handleDeleteCompanyProfilePhoto = async (companyId) => {
try {
await deleteCompanyProfilePhoto(companyId).unwrap();
notification.success({
message: 'Успешно',
description: 'Фото профиля удалено'
});
// TODO найти способ обновления логотипа
setTimeout(() => {
setIsSuspendForPhotoShowing(!isSuspendForPhotoShowing);
}, 3000);
} catch (error) {
notification.error({
message: 'Ошибка',
description: error.data?.detail?.[0]?.msg || error.data?.detail || 'Не удалось сохранить данные',
});
}
};
const handleSave = async (values) => {
try {
const companyPayload = {
title: values.title,
description: values.description,
inn: values.inn,
website_url: values.website_url,
corporate_email: values.corporate_email,
video_url: values.video_url,
industry_id: values.industry_id,
};
if (companyData) {
await handleUpdateCompanyProfile(companyPayload);
const socialsPayload = (values.socials || []).map((social) => ({
title: social.title,
link: social.link,
company_id: companyData.id,
}));
await Promise.all([
handleReplaceCompanySocials(socialsPayload, companyData.id),
]);
} else {
const resp = await handleCreateCompanyProfile(companyPayload);
const socialsPayload = (values.socials || []).map((social) => ({
title: social.title,
link: social.link,
company_id: resp?.id,
}));
await Promise.all([
handleReplaceCompanySocials(socialsPayload, resp?.id),
]);
}
} catch (error) {
notification.error({
message: 'Ошибка',
description: error.data?.detail?.[0]?.msg || error.data?.detail || 'Не удалось сохранить данные',
});
}
};
const handleUpdateCompanyProfile = async (companyPayload) => {
try {
await updateCompanyProfile({companyProfileId: companyData.id, data: companyPayload});
notification.success({
message: 'Успешно',
description: 'Профиль компании обновлен'
});
} catch (error) {
notification.error({
message: 'Ошибка',
description: error.data?.detail?.[0]?.msg || error.data?.detail || 'Не удалось сохранить данные',
});
}
};
const handleCreateCompanyProfile = async (companyPayload) => {
try {
const resp = await createCompanyProfile(companyPayload).unwrap();
notification.success({
message: 'Успешно',
description: 'Профиль компании создан'
});
return resp;
} catch (error) {
notification.error({
message: 'Ошибка',
description: error.data?.detail?.[0]?.msg || error.data?.detail || 'Не удалось сохранить данные',
});
}
};
const handleReplaceCompanySocials = async (companyProfileSocials, companyProfileId) => {
try {
await replaceCompanyProfileSocials({companyProfileId, companyProfileSocials}).unwrap();
} catch (error) {
notification.error({
message: 'Ошибка',
description: error.data?.detail?.[0]?.msg || error.data?.detail || 'Не удалось сохранить социальные сети',
});
}
};
const submitVerification = async (values) => {
// TODO
};
const industriesItems = industries.map((industry) => ({
value: industry.id,
label: industry.title,
}));
return {
form,
verForm,
isLoading: isDictLoading || isFetching,
isSaving,
isLoading: isIndustriesLoading || isCreating || isMyCompanyLoading || isReplacingCompanyProfileSocials || isSocialDataLoading || isUpdating || isUploadingCompanyProfileLogo || isDeletingCompanyProfileLogo || isVerificationRequestsDataLoading || isCreatingVerificationRequest || isUploadingCompanyProfilePhoto || isDeletingCompanyProfilePhoto,
handleSave,
dictionaries,
verificationStatus,
submitVerification,
companyTitle: companyData?.title,
industriesItems,
companyData,
moderationStatus,
handleUploadCompanyProfileLogo,
handleDeleteCompanyProfileLogo,
handleUploadCompanyProfilePhoto,
handleDeleteCompanyProfilePhoto,
isSuspendForLogoShowing,
isSuspendForPhotoShowing,
verificationRequestsData,
handleCreateVerificationRequest,
};
};

View File

@ -1,9 +1,9 @@
import React, { useState, useEffect } from 'react'
import React, {useState, useEffect} from 'react'
import {
Button, Empty, Flex, Popconfirm, Select, Space, Table, Tag, Typography, notification,
Button, Empty, Flex, Popconfirm, Select, Space, Table, Tag, Typography, notification, Spin, Result,
} from 'antd'
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'
import { useSelector } from 'react-redux'
import {DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined} from '@ant-design/icons'
import {useSelector} from 'react-redux'
import {
getOpportunities,
@ -14,9 +14,11 @@ import {
} from '../../../../../../../local/tramplinStore.js'
import OpportunityModal from '../OpportunityModal/OpportunityModal.jsx'
import { salaryText, TYPE_COLORS } from '../../../../../HomePage/useHomePage.js'
import {salaryText, TYPE_COLORS} from '../../../../../HomePage/useHomePage.js'
import useMyOpportunitiesTab from "./useMyOpportunitiesTab.js";
import LoadingIndicator from "../../../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
const { Text } = Typography
const {Text} = Typography
const TYPE_LABEL = {
vacancy: 'Вакансия',
@ -27,14 +29,19 @@ const TYPE_LABEL = {
}
const STATUS_LABELS = {
approved: { color: 'success', text: 'Опубликовано' },
pending: { color: 'processing', text: 'На модерации' },
rejected: { color: 'error', text: 'Отклонено' },
draft: { color: 'default', text: 'Черновик' },
approved: {color: 'success', text: 'Опубликовано'},
pending: {color: 'processing', text: 'На модерации'},
rejected: {color: 'error', text: 'Отклонено'},
draft: {color: 'default', text: 'Черновик'},
}
export default function MyOpportunitiesTab() {
const { userData } = useSelector(s => s.auth)
const {
companyData,
isLoading,
} = useMyOpportunitiesTab();
const {userData} = useSelector(s => s.auth)
const [opportunities, setOpportunities] = useState([])
const [modalOpen, setModalOpen] = useState(false)
const [editItem, setEditItem] = useState(null)
@ -66,7 +73,7 @@ export default function MyOpportunitiesTab() {
const handleSubmit = (values) => {
if (editItem) {
patchOpportunity(editItem.id, values)
notification.success({ message: 'Изменения сохранены' })
notification.success({message: 'Изменения сохранены'})
} else {
upsertExtraOpportunity({
...values,
@ -76,7 +83,7 @@ export default function MyOpportunitiesTab() {
moderationStatus: 'pending',
publishedAt: new Date().toISOString().slice(0, 10),
})
notification.success({ message: 'Отправлено на модерацию' })
notification.success({message: 'Отправлено на модерацию'})
}
setModalOpen(false)
setEditItem(null)
@ -88,11 +95,11 @@ export default function MyOpportunitiesTab() {
localStorage.setItem('tramplin_opp_extras_v1', JSON.stringify(filteredExtras))
const patches = JSON.parse(localStorage.getItem('tramplin_opp_patches_v1') || '{}')
patches[id] = { ...patches[id], moderationStatus: 'rejected' }
patches[id] = {...patches[id], moderationStatus: 'rejected'}
localStorage.setItem('tramplin_opp_patches_v1', JSON.stringify(patches))
bumpStore()
notification.success({ message: 'Удалено' })
notification.success({message: 'Удалено'})
}
const columns = [
@ -101,8 +108,8 @@ export default function MyOpportunitiesTab() {
key: 'title',
render: (_, r) => (
<div>
<Text strong style={{ fontSize: 13 }}>{r.title}</Text><br />
<Tag color={TYPE_COLORS[r.type]} style={{ fontSize: 11, margin: '2px 0 0' }}>
<Text strong style={{fontSize: 13}}>{r.title}</Text><br/>
<Tag color={TYPE_COLORS[r.type]} style={{fontSize: 11, margin: '2px 0 0'}}>
{TYPE_LABEL[r.type] || r.type}
</Tag>
</div>
@ -114,7 +121,7 @@ export default function MyOpportunitiesTab() {
key: 'city',
width: 110,
render: (city, r) => (
<Text type="secondary" style={{ fontSize: 12 }}>
<Text type="secondary" style={{fontSize: 12}}>
{city}{r.format === 'remote' ? ' (удал.)' : ''}
</Text>
),
@ -123,7 +130,7 @@ export default function MyOpportunitiesTab() {
title: 'Зарплата',
key: 'salary',
width: 130,
render: (_, r) => <Text style={{ fontSize: 12 }}>{salaryText(r) || '—'}</Text>,
render: (_, r) => <Text style={{fontSize: 12}}>{salaryText(r) || '—'}</Text>,
},
{
title: 'Статус',
@ -131,7 +138,7 @@ export default function MyOpportunitiesTab() {
key: 'status',
width: 130,
render: (s) => {
const st = STATUS_LABELS[s] || { color: 'default', text: s || '—' }
const st = STATUS_LABELS[s] || {color: 'default', text: s || '—'}
return <Tag color={st.color}>{st.text}</Tag>
},
},
@ -140,7 +147,7 @@ export default function MyOpportunitiesTab() {
dataIndex: 'publishedAt',
key: 'date',
width: 100,
render: d => <Text type="secondary" style={{ fontSize: 12 }}>{d || '—'}</Text>,
render: d => <Text type="secondary" style={{fontSize: 12}}>{d || '—'}</Text>,
},
{
title: '',
@ -150,8 +157,11 @@ export default function MyOpportunitiesTab() {
<Space size={4}>
<Button
size="small"
icon={<EditOutlined />}
onClick={() => { setEditItem(r); setModalOpen(true) }}
icon={<EditOutlined/>}
onClick={() => {
setEditItem(r);
setModalOpen(true)
}}
/>
<Popconfirm
title="Удалить эту запись?"
@ -159,34 +169,47 @@ export default function MyOpportunitiesTab() {
okText="Да"
cancelText="Нет"
>
<Button size="small" danger icon={<DeleteOutlined />} />
<Button size="small" danger icon={<DeleteOutlined/>}/>
</Popconfirm>
</Space>
),
},
]
if (!companyData.is_verified) {
return (
<Result
title={"Не доступно"}
subTitle={"Ваша компания еще не прошла модерацию, дождитесь результата модерации или отправьте запрос на модерацию во вкладке компания"}
/>
);
}
return (
<Spin spinning={isLoading} indicator={<LoadingIndicator/>}>
<div>
<Flex justify="space-between" align="center" style={{ marginBottom: 16 }} wrap="wrap" gap={8}>
<Flex justify="space-between" align="center" style={{marginBottom: 16}} wrap="wrap" gap={8}>
<Space>
<Select
value={statusFilter}
onChange={setStatusFilter}
style={{ width: 160 }}
style={{width: 160}}
options={[
{ value: 'all', label: 'Все' },
{ value: 'approved', label: 'Опубликованные' },
{ value: 'pending', label: 'На модерации' },
{ value: 'rejected', label: 'Отклонённые' },
{value: 'all', label: 'Все'},
{value: 'approved', label: 'Опубликованные'},
{value: 'pending', label: 'На модерации'},
{value: 'rejected', label: 'Отклонённые'},
]}
/>
<Button icon={<ReloadOutlined />} onClick={load}>Обновить</Button>
<Button icon={<ReloadOutlined/>} onClick={load}>Обновить</Button>
</Space>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => { setEditItem(null); setModalOpen(true) }}
icon={<PlusOutlined/>}
onClick={() => {
setEditItem(null);
setModalOpen(true)
}}
disabled={!isVerified}
>
Создать возможность
@ -206,24 +229,28 @@ export default function MyOpportunitiesTab() {
)}
{filtered.length === 0
? <Empty description="У вас пока нет активных записей." />
? <Empty description="У вас пока нет активных записей."/>
: <Table
dataSource={filtered}
columns={columns}
rowKey="id"
size="small"
pagination={{ pageSize: 10 }}
pagination={{pageSize: 10}}
/>
}
<OpportunityModal
open={modalOpen}
onClose={() => { setModalOpen(false); setEditItem(null) }}
onClose={() => {
setModalOpen(false);
setEditItem(null)
}}
onSubmit={handleSubmit}
initial={editItem}
companyName={companyName}
notificationApi={notification}
/>
</div>
</Spin>
)
}

View File

@ -1,32 +1,15 @@
import { useMemo, useState, useEffect } from 'react'
import { useSelector } from 'react-redux'
import { getEmployerOpportunities } from '../../../../../../../local/tramplinStore.js'
import {useGetCompanyProfileQuery} from "../../../../../../../Api/companyApi.js";
const useMyOpportunitiesTab = () => {
const { userData } = useSelector((s) => s.auth)
const [v, setV] = useState(0)
const [statusFilter, setStatusFilter] = useState('all')
const {
data: companyData = null,
isLoading: isMyCompanyLoading,
} = useGetCompanyProfileQuery();
useEffect(() => {
const fn = () => setV((x) => x + 1)
window.addEventListener('tramplin-store-changed', fn)
return () => window.removeEventListener('tramplin-store-changed', fn)
}, [])
const opportunities = useMemo(() => {
const list = getEmployerOpportunities(userData?.id)
const mapped = list.map((o) => ({
id: o.id,
title: o.title,
kind: o.kind || (o.type === 'mentor' ? 'mentorship' : o.type),
status: o.status || 'active',
moderationStatus: o.moderationStatus || 'approved',
}))
if (statusFilter === 'all') return mapped
return mapped.filter((r) => r.status === statusFilter)
}, [userData?.id, v, statusFilter])
return { opportunities, isLoading: false, statusFilter, setStatusFilter }
}
return {
companyData,
isLoading: isMyCompanyLoading,
}
};
export default useMyOpportunitiesTab

View File

@ -1,4 +1,4 @@
import { useGetProfileQuery } from '../../../../../../../Api/companyApi.js'
import { useGetCompanyProfileQuery } from '../../../../../../../Api/companyApi.js'
import {
useGetWorkFormatsQuery,
useGetExperienceLevelsQuery,
@ -31,7 +31,7 @@ function mapExpTitleToSlug(title) {
}
const useOpportunityModal = (form, mode, kind, editingId, onClose, notificationApi) => {
const { data: companyProfile } = useGetProfileQuery()
const { data: companyProfile } = useGetCompanyProfileQuery()
const { data: workFormats = [] } = useGetWorkFormatsQuery()
const { data: experienceLevels = [] } = useGetExperienceLevelsQuery()
const { userData } = useSelector((s) => s.auth)

View File

@ -1,20 +1,21 @@
import React, { useEffect, useState } from 'react'
import { Avatar, Empty, Flex, Select, Space, Table, Tag, Typography, notification } from 'antd'
import { UserOutlined } from '@ant-design/icons'
import { useSelector } from 'react-redux'
import React, {useEffect, useState} from 'react'
import {Avatar, Empty, Flex, Select, Space, Table, Tag, Typography, notification, Spin, Result} from 'antd'
import {UserOutlined} from '@ant-design/icons'
import {useSelector} from 'react-redux'
import {
getApplicationsForEmployer,
updateApplication,
getOpportunities
} from '../../../../../../../local/tramplinStore.js'
import useResponsesTab from "./useResponsesTab.js";
import LoadingIndicator from "../../../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
const { Text } = Typography
const {Text} = Typography
const STATUS_OPTIONS = [
{ value: 'Новый', label: 'Новый' },
{ value: 'accepted', label: 'Принят' },
{ value: 'rejected', label: 'Отклонён' },
{ value: 'reserve', label: 'В резерве' },
{value: 'Новый', label: 'Новый'},
{value: 'accepted', label: 'Принят'},
{value: 'rejected', label: 'Отклонён'},
{value: 'reserve', label: 'В резерве'},
]
const STATUS_COLORS = {
@ -26,7 +27,12 @@ const STATUS_COLORS = {
}
export default function ResponsesTab() {
const { userData } = useSelector(s => s.auth)
const {
companyData,
isLoading,
} = useResponsesTab();
const {userData} = useSelector(s => s.auth)
const [applications, setApplications] = useState([])
const [myOpps, setMyOpps] = useState([])
const [filterOpp, setFilterOpp] = useState('all')
@ -44,8 +50,10 @@ export default function ResponsesTab() {
const myApps = allApps.filter(a => String(a.ownerUserId) === String(userData?.id))
setApplications(myApps)
}
useEffect(() => { load() }, [userData, companyName])
}
useEffect(() => {
load()
}, [userData, companyName])
useEffect(() => {
const fn = () => load()
@ -64,10 +72,10 @@ export default function ResponsesTab() {
title: 'Соискатель', key: 'applicant',
render: (_, r) => (
<Flex align="center" gap={8}>
<Avatar icon={<UserOutlined />} size={32} />
<Avatar icon={<UserOutlined/>} size={32}/>
<div>
<Text strong style={{ fontSize: 13 }}>{r.applicantName || 'Соискатель'}</Text><br />
<Text type="secondary" style={{ fontSize: 11 }}>
<Text strong style={{fontSize: 13}}>{r.applicantName || 'Соискатель'}</Text><br/>
<Text type="secondary" style={{fontSize: 11}}>
{r.applicantEmail || `ID пользователя: ${r.applicantUserId}`}
</Text>
</div>
@ -78,20 +86,20 @@ export default function ResponsesTab() {
title: 'Вакансия', key: 'opp',
render: (_, r) => (
<div>
<Text style={{ fontSize: 13 }}>{r.opportunityTitle}</Text>
<Text style={{fontSize: 13}}>{r.opportunityTitle}</Text>
</div>
),
},
{
title: 'Сопр. письмо', dataIndex: 'coverLetter', key: 'letter', width: 200,
render: text => (
<Text style={{ fontSize: 12 }} ellipsis={{ tooltip: text }}>{text || '—'}</Text>
<Text style={{fontSize: 12}} ellipsis={{tooltip: text}}>{text || '—'}</Text>
),
},
{
title: 'Дата', dataIndex: 'date', key: 'date', width: 100,
render: d => (
<Text type="secondary" style={{ fontSize: 12 }}>
<Text type="secondary" style={{fontSize: 12}}>
{d || '—'}
</Text>
),
@ -102,15 +110,15 @@ export default function ResponsesTab() {
<Select
value={r.status || 'Новый'}
size="small"
style={{ width: 150 }}
style={{width: 150}}
options={STATUS_OPTIONS}
onChange={val => {
updateApplication(r.id, { status: val })
notification.success({ message: 'Статус обновлён' })
updateApplication(r.id, {status: val})
notification.success({message: 'Статус обновлён'})
}}
optionRender={opt => (
<Space>
<Tag color={STATUS_COLORS[opt.value] || 'default'} style={{ marginRight: 0 }}>
<Tag color={STATUS_COLORS[opt.value] || 'default'} style={{marginRight: 0}}>
{opt.label}
</Tag>
</Space>
@ -120,38 +128,49 @@ export default function ResponsesTab() {
},
]
if (!companyData.is_verified) {
return (
<Result
title={"Не доступно"}
subTitle={"Ваша компания еще не прошла модерацию, дождитесь результата модерации или отправьте запрос на модерацию во вкладке компания"}
/>
);
}
return (
<Spin spinning={isLoading} indicator={<LoadingIndicator/>}>
<div>
<Flex gap={12} style={{ marginBottom: 16 }} wrap="wrap">
<Flex gap={12} style={{marginBottom: 16}} wrap="wrap">
<Select
value={filterOpp}
onChange={setFilterOpp}
style={{ width: 220 }}
style={{width: 220}}
placeholder="Фильтр по вакансии"
options={[
{ value: 'all', label: 'Все вакансии' },
...myOpps.map(o => ({ value: String(o.id), label: o.title }))
{value: 'all', label: 'Все вакансии'},
...myOpps.map(o => ({value: String(o.id), label: o.title}))
]}
/>
<Select
value={filterStatus}
onChange={setFilterStatus}
style={{ width: 180 }}
style={{width: 180}}
placeholder="Фильтр по статусу"
options={[{ value: 'all', label: 'Все статусы' }, ...STATUS_OPTIONS]}
options={[{value: 'all', label: 'Все статусы'}, ...STATUS_OPTIONS]}
/>
</Flex>
{filtered.length === 0
? <Empty description="Откликов на ваши предложения пока нет" />
? <Empty description="Откликов на ваши предложения пока нет"/>
: <Table
dataSource={filtered}
columns={columns}
rowKey="id"
size="small"
pagination={{ pageSize: 10 }}
pagination={{pageSize: 10}}
/>
}
</div>
</Spin>
)
}

View File

@ -1,29 +1,15 @@
import { notification } from 'antd'
import { useState } from 'react'
import { updateApplication } from '../../../../../../../local/tramplinStore.js'
import {useGetCompanyProfileQuery} from "../../../../../../../Api/companyApi.js";
function useResponsesTab(onRefresh) {
const [loading, setLoading] = useState(false)
const useResponsesTab = () => {
const {
data: companyData = null,
isLoading: isMyCompanyLoading,
} = useGetCompanyProfileQuery();
const handleStatusChange = async (id, newStatus) => {
setLoading(true)
try {
updateApplication(id, { status: newStatus })
notification.success({ message: 'Статус обновлён' })
if (onRefresh) onRefresh()
} catch (error) {
notification.error({ message: 'Не удалось обновить статус' })
} finally {
setLoading(false)
return {
companyData,
isLoading: isMyCompanyLoading,
}
}
const handleNoteBlur = async (id, note) => {
updateApplication(id, { note })
notification.success({ message: 'Заметка сохранена' })
}
return { handleStatusChange, handleNoteBlur, loading }
}
export default useResponsesTab

View File

@ -2,7 +2,7 @@ import { useState, useCallback, useMemo, useEffect } from 'react'
import { Form, notification } from 'antd'
import dayjs from 'dayjs'
import { useSelector } from 'react-redux'
import { useGetProfileQuery } from '../../../../../Api/companyApi.js'
import { useGetCompanyProfileQuery } from '../../../../../Api/companyApi.js'
import { getOpportunityById } from '../../../../../local/tramplinStore.js'
import { getApplicationsForEmployer } from '../../../../../local/tramplinStore.js'
@ -10,7 +10,7 @@ export const useEmployerCabinet = () => {
const [api, contextHolder] = notification.useNotification()
const [form] = Form.useForm()
const { userData } = useSelector((s) => s.auth)
const { data: companyProfile } = useGetProfileQuery()
const { data: companyProfile } = useGetCompanyProfileQuery()
const [storeV, setStoreV] = useState(0)
useEffect(() => {

View File

@ -1,4 +1,4 @@
import { configureStore } from "@reduxjs/toolkit";
import {configureStore} from "@reduxjs/toolkit";
import authReducer from "./Slices/authSlice.js";
import userReducer from "./Slices/usersSlice.js";
import {authApi} from "../Api/authApi.js";
@ -10,12 +10,13 @@ import {applicantProfilesApi} from "../Api/applicantProfilesApi.js";
import {applicantSkillsApi} from "../Api/applicantSkillsApi.js";
import {applicantSkillTagsApi} from "../Api/applicantSkillTagsApi.js";
import {experienceLevelsApi} from "../Api/experienceLevelsApi.js";
// Добавляем новые API
import { companyApi } from "../Api/companyApi.js";
import { industriesApi } from "../Api/industriesApi.js";
import {vacanciesApi } from "../Api/vacanciesApi.js";
import {dictionariesApi } from "../Api/dictionariesApi.js";
import { internshipsApi } from '../Api/internshipsApi'; // путь к твоему новому API
import {companyApi} from "../Api/companyApi.js";
import {industriesApi} from "../Api/industriesApi.js";
import {vacanciesApi} from "../Api/vacanciesApi.js";
import {dictionariesApi} from "../Api/dictionariesApi.js";
import {internshipsApi} from '../Api/internshipsApi';
import {companyProfileSocialsApi} from "../Api/companyProfileSocialsApi.js";
import {verificationRequestsApi} from "../Api/verificationRequestsApi.js";
export const store = configureStore({
reducer: {
@ -26,8 +27,11 @@ export const store = configureStore({
[usersApi.reducerPath]: usersApi.reducer,
[rolesApi.reducerPath]: rolesApi.reducer,
[universitiesApi.reducerPath]: universitiesApi.reducer,
[applicantEducationsApi.reducerPath]: applicantEducationsApi.reducer,
[applicantProfilesApi.reducerPath]: applicantProfilesApi.reducer,
[applicantSkillsApi.reducerPath]: applicantSkillsApi.reducer,
@ -36,12 +40,19 @@ export const store = configureStore({
[experienceLevelsApi.reducerPath]: experienceLevelsApi.reducer,
// Регистрируем новые редьюсеры
[companyApi.reducerPath]: companyApi.reducer,
[industriesApi.reducerPath]: industriesApi.reducer,
[vacanciesApi.reducerPath]: vacanciesApi.reducer,
[dictionariesApi.reducerPath]: dictionariesApi.reducer,
[internshipsApi.reducerPath]: internshipsApi.reducer, // ДОБАВИТЬ ЭТО
[internshipsApi.reducerPath]: internshipsApi.reducer,
[companyProfileSocialsApi.reducerPath]: companyProfileSocialsApi.reducer,
[verificationRequestsApi.reducerPath]: verificationRequestsApi.reducer,
},
middleware: (getDefaultMiddleware) => (
getDefaultMiddleware().concat(
@ -51,15 +62,16 @@ export const store = configureStore({
universitiesApi.middleware,
applicantEducationsApi.middleware,
applicantProfilesApi.middleware,
// Регистрируем новый middleware
companyApi.middleware,
industriesApi.middleware,
vacanciesApi.middleware,
dictionariesApi.middleware,
internshipsApi.middleware, // ДОБАВИТЬ ЭТО
internshipsApi.middleware,
applicantSkillsApi.middleware,
applicantSkillTagsApi.middleware,
experienceLevelsApi.middleware,
companyProfileSocialsApi.middleware,
verificationRequestsApi.middleware,
)
),
});