Compare commits
3 Commits
c0b6aff7af
...
46ba0601e4
| Author | SHA1 | Date | |
|---|---|---|---|
| 46ba0601e4 | |||
| 8bfb85fc4b | |||
| 3a9c9e2e7c |
30
api/app/application/company_profile_logos_repository.py
Normal file
30
api/app/application/company_profile_logos_repository.py
Normal 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
|
||||
30
api/app/application/company_profile_photos_repository.py
Normal file
30
api/app/application/company_profile_photos_repository.py
Normal 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
|
||||
39
api/app/application/company_profile_socials_repository.py
Normal file
39
api/app/application/company_profile_socials_repository.py
Normal 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
|
||||
@ -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
|
||||
|
||||
38
api/app/application/verification_requests_repository.py
Normal file
38
api/app/application/verification_requests_repository.py
Normal 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
|
||||
54
api/app/controllers/company_profile_logos_router.py
Normal file
54
api/app/controllers/company_profile_logos_router.py
Normal 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)
|
||||
54
api/app/controllers/company_profile_photos_router.py
Normal file
54
api/app/controllers/company_profile_photos_router.py
Normal 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)
|
||||
43
api/app/controllers/company_profile_socials_router.py
Normal file
43
api/app/controllers/company_profile_socials_router.py
Normal 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)
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
42
api/app/controllers/verification_requests_router.py
Normal file
42
api/app/controllers/verification_requests_router.py
Normal 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)
|
||||
@ -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 ###
|
||||
@ -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
|
||||
16
api/app/domain/entities/verification_requests.py
Normal file
16
api/app/domain/entities/verification_requests.py
Normal 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
|
||||
@ -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)
|
||||
|
||||
|
||||
103
api/app/infrastructure/company_profile_logos_router.py
Normal file
103
api/app/infrastructure/company_profile_logos_router.py
Normal 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)
|
||||
103
api/app/infrastructure/company_profile_photos_router.py
Normal file
103
api/app/infrastructure/company_profile_photos_router.py
Normal 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)
|
||||
79
api/app/infrastructure/company_profile_socials_service.py
Normal file
79
api/app/infrastructure/company_profile_socials_service.py
Normal 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
|
||||
@ -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)
|
||||
80
api/app/infrastructure/company_verification_service.py
Normal file
80
api/app/infrastructure/company_verification_service.py
Normal 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)
|
||||
@ -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"
|
||||
22
api/app/infrastructure/industries_services.py
Normal file
22
api/app/infrastructure/industries_services.py
Normal 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
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -3,14 +3,14 @@ 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;
|
||||
28
web/src/Api/companyProfileSocialsApi.js
Normal file
28
web/src/Api/companyProfileSocialsApi.js
Normal 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;
|
||||
@ -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'],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
27
web/src/Api/verificationRequestsApi.js
Normal file
27
web/src/Api/verificationRequestsApi.js
Normal 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;
|
||||
@ -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
|
||||
|
||||
@ -18,7 +18,6 @@ import {
|
||||
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();
|
||||
@ -31,7 +30,6 @@ const useProfileTab = () => {
|
||||
|
||||
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});
|
||||
@ -41,35 +39,8 @@ const useProfileTab = () => {
|
||||
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) {
|
||||
if (userData) {
|
||||
const values = {...userData, resume_url: userData?.applicant_profile?.resume_url};
|
||||
|
||||
if (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();
|
||||
@ -126,31 +97,16 @@ const useProfileTab = () => {
|
||||
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, );
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -1,95 +1,139 @@
|
||||
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: 'Введите название' }]}>
|
||||
<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: 'Введите ИНН' }]}>
|
||||
<Form.Item
|
||||
label="ИНН"
|
||||
name="inn"
|
||||
rules={[{required: true, message: 'Введите ИНН'}]}
|
||||
>
|
||||
<Input placeholder="7707083893" size="large"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item label="Краткое описание" name="about">
|
||||
<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>
|
||||
@ -102,7 +146,7 @@ const CompanyTab = () => {
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col xs={24} md={12}>
|
||||
<Form.Item label="Сайт" name="site">
|
||||
<Form.Item label="Сайт" name="website_url">
|
||||
<Input placeholder="https://example.com" size="large"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@ -120,14 +164,24 @@ const CompanyTab = () => {
|
||||
{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 }}>
|
||||
<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 }}>
|
||||
<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>
|
||||
@ -138,55 +192,105 @@ const CompanyTab = () => {
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,73 +1,155 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
import {Form, notification} from 'antd';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useGetProfileQuery, useCreateOrUpdateCompanyMutation } from "../../../../../../../Api/companyApi";
|
||||
import {
|
||||
useGetCompanyProfileQuery,
|
||||
useCreateCompanyProfileMutation,
|
||||
useUpdateCompanyProfileMutation, useUploadCompanyProfileLogoMutation, useDeleteCompanyProfileLogoMutation,
|
||||
useUploadCompanyProfilePhotoMutation, useDeleteCompanyProfilePhotoMutation
|
||||
} from "../../../../../../../Api/companyApi";
|
||||
import {useGetAllIndustriesQuery} from "../../../../../../../Api/industriesApi";
|
||||
import {
|
||||
submitEmployerVerification,
|
||||
getEmployerVerificationStatus,
|
||||
} from "../../../../../../../local/tramplinStore.js";
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
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'
|
||||
@ -15,6 +15,8 @@ import {
|
||||
|
||||
import OpportunityModal from '../OpportunityModal/OpportunityModal.jsx'
|
||||
import {salaryText, TYPE_COLORS} from '../../../../../HomePage/useHomePage.js'
|
||||
import useMyOpportunitiesTab from "./useMyOpportunitiesTab.js";
|
||||
import LoadingIndicator from "../../../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
|
||||
|
||||
const {Text} = Typography
|
||||
|
||||
@ -34,6 +36,11 @@ const STATUS_LABELS = {
|
||||
}
|
||||
|
||||
export default function MyOpportunitiesTab() {
|
||||
const {
|
||||
companyData,
|
||||
isLoading,
|
||||
} = useMyOpportunitiesTab();
|
||||
|
||||
const {userData} = useSelector(s => s.auth)
|
||||
const [opportunities, setOpportunities] = useState([])
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
@ -151,7 +158,10 @@ export default function MyOpportunitiesTab() {
|
||||
<Button
|
||||
size="small"
|
||||
icon={<EditOutlined/>}
|
||||
onClick={() => { setEditItem(r); setModalOpen(true) }}
|
||||
onClick={() => {
|
||||
setEditItem(r);
|
||||
setModalOpen(true)
|
||||
}}
|
||||
/>
|
||||
<Popconfirm
|
||||
title="Удалить эту запись?"
|
||||
@ -166,7 +176,17 @@ export default function MyOpportunitiesTab() {
|
||||
},
|
||||
]
|
||||
|
||||
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}>
|
||||
<Space>
|
||||
@ -186,7 +206,10 @@ export default function MyOpportunitiesTab() {
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined/>}
|
||||
onClick={() => { setEditItem(null); setModalOpen(true) }}
|
||||
onClick={() => {
|
||||
setEditItem(null);
|
||||
setModalOpen(true)
|
||||
}}
|
||||
disabled={!isVerified}
|
||||
>
|
||||
Создать возможность
|
||||
@ -218,12 +241,16 @@ export default function MyOpportunitiesTab() {
|
||||
|
||||
<OpportunityModal
|
||||
open={modalOpen}
|
||||
onClose={() => { setModalOpen(false); setEditItem(null) }}
|
||||
onClose={() => {
|
||||
setModalOpen(false);
|
||||
setEditItem(null)
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
initial={editItem}
|
||||
companyName={companyName}
|
||||
notificationApi={notification}
|
||||
/>
|
||||
</div>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import { Avatar, Empty, Flex, Select, Space, Table, Tag, Typography, notification } from 'antd'
|
||||
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
|
||||
|
||||
@ -26,6 +27,11 @@ const STATUS_COLORS = {
|
||||
}
|
||||
|
||||
export default function ResponsesTab() {
|
||||
const {
|
||||
companyData,
|
||||
isLoading,
|
||||
} = useResponsesTab();
|
||||
|
||||
const {userData} = useSelector(s => s.auth)
|
||||
const [applications, setApplications] = useState([])
|
||||
const [myOpps, setMyOpps] = useState([])
|
||||
@ -45,7 +51,9 @@ export default function ResponsesTab() {
|
||||
|
||||
setApplications(myApps)
|
||||
}
|
||||
useEffect(() => { load() }, [userData, companyName])
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [userData, companyName])
|
||||
|
||||
useEffect(() => {
|
||||
const fn = () => load()
|
||||
@ -120,7 +128,17 @@ 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">
|
||||
<Select
|
||||
@ -153,5 +171,6 @@ export default function ResponsesTab() {
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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 {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,
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user