1067 lines
38 KiB
Python
1067 lines
38 KiB
Python
from fastapi import APIRouter,Depends,Query
|
|
from fastapi.responses import FileResponse,JSONResponse
|
|
from fastapi import HTTPException
|
|
from db_setup import get_session
|
|
from job.candidate.views import CandidateScoring,FileRead,CandidateView
|
|
from job.interviews.views import Interview
|
|
from job.notes.views import Note
|
|
from job.activity.views import ActivityLog
|
|
from job.feedback.views import FeedbackView
|
|
from job.pipeline.views import Pipeline
|
|
from job.history.views import HistoryRecorder
|
|
from job.assignment.views import Assignment
|
|
from job.cost.views import HiringCost
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from users.permissions import PermissionTag, require_permission
|
|
from job.job_post.views import JobPost,JobPostCreate
|
|
from job_assist.execute_agent import run_field_assist
|
|
import logging
|
|
from users.views import User
|
|
from job.job_post.models import SocialPlatform
|
|
from fastapi import UploadFile, File, Form
|
|
from dotenv import load_dotenv
|
|
from datetime import datetime, time, timezone
|
|
from pydantic import BaseModel
|
|
from uuid import UUID
|
|
from typing import Literal, Optional
|
|
import uuid
|
|
load_dotenv()
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class CandidateUpdate(BaseModel):
|
|
favorite: bool | None = None
|
|
rating: float | None = None
|
|
|
|
|
|
class InterviewCreate(BaseModel):
|
|
inbox_id: int
|
|
interview_date: datetime | None = None
|
|
interview_time: datetime | None = None
|
|
interview_type: str | None = None
|
|
interview_status: str | None = None
|
|
|
|
|
|
class InterviewUpdate(BaseModel):
|
|
interview_date: datetime | None = None
|
|
interview_time: datetime | None = None
|
|
interview_type: str | None = None
|
|
interview_status: str | None = None
|
|
inbox_id: int | None = None
|
|
|
|
|
|
class NoteCreate(BaseModel):
|
|
user_id: UUID
|
|
note: str
|
|
|
|
|
|
class NoteUpdate(BaseModel):
|
|
note: str | None = None
|
|
|
|
|
|
class ActivityCreate(BaseModel):
|
|
message_id: UUID | None = None
|
|
user_id: UUID | None = None
|
|
inbox_id: int | None = None
|
|
activity_type: str | None = None
|
|
activity_status: str | None = None
|
|
description: str | None = None
|
|
activity_date: datetime | None = None
|
|
activity_time: datetime | None = None
|
|
|
|
|
|
class FeedbackCreate(BaseModel):
|
|
inbox_id: int | None = None
|
|
review: str | None = None
|
|
financial_status: str | None = None
|
|
score: float | None = None
|
|
note: str | None = None
|
|
reviewed_by: UUID | None = None
|
|
|
|
|
|
class FeedbackUpdate(BaseModel):
|
|
review: str | None = None
|
|
financial_status: str | None = None
|
|
score: float | None = None
|
|
note: str | None = None
|
|
inbox_id: int | None = None
|
|
reviewed_by: UUID | None = None
|
|
|
|
|
|
class StageChange(BaseModel):
|
|
inbox_id: int | None = None
|
|
manual_upload_id: UUID | None = None
|
|
to_stage: str
|
|
change_reason: str | None = None
|
|
|
|
|
|
class JobAssignmentCreate(BaseModel):
|
|
job_post_id: UUID
|
|
user_id: UUID
|
|
assignment_role: str | None = None
|
|
|
|
|
|
class ApplicationAssignmentCreate(BaseModel):
|
|
inbox_id: int
|
|
user_id: UUID
|
|
assignment_role: str | None = None
|
|
|
|
|
|
class HiringCostCreate(BaseModel):
|
|
cost_type: str
|
|
amount: float
|
|
job_post_id: UUID | None = None
|
|
currency: str | None = None
|
|
description: str | None = None
|
|
incurred_at: datetime | None = None
|
|
|
|
|
|
class JobUpdate(BaseModel):
|
|
title: str | None = None
|
|
department: str | None = None
|
|
location: str | None = None
|
|
employment_type: str | None = None
|
|
vacancies: int | None = None
|
|
salary: str | None = None
|
|
salary_min: float | None = None
|
|
salary_max: float | None = None
|
|
experience_min: int | None = None
|
|
experience_max: int | None = None
|
|
description: str | None = None
|
|
|
|
|
|
class JobStatusUpdate(BaseModel):
|
|
requisition_status: str
|
|
|
|
|
|
class FeedbackTemplateCreate(BaseModel):
|
|
name: str
|
|
department: str | None = None
|
|
criteria: list[str] | None = None
|
|
is_active: bool | None = None
|
|
|
|
|
|
class FeedbackTemplateUpdate(BaseModel):
|
|
name: str | None = None
|
|
department: str | None = None
|
|
criteria: list[str] | None = None
|
|
is_active: bool | None = None
|
|
|
|
|
|
@router.get("/jobs/alias")
|
|
async def get_job_alias(session: AsyncSession = Depends(get_session)):
|
|
try:
|
|
alias_lst=await SocialPlatform.list_aliases(session)
|
|
return JSONResponse(content={"data":alias_lst,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
@router.post("/candidate/create/candidate")
|
|
async def create_manual_candidate(
|
|
file: UploadFile = File(...),
|
|
candidate_email: str | None = Form(None),
|
|
candidate_name: str | None = Form(None),
|
|
candidate_phone: str | None = Form(None),
|
|
job_post_id: str | None = Form(None),
|
|
current_company: str | None = Form(None),
|
|
current_position: str | None = Form(None),
|
|
platform: str | None = Form(None),
|
|
experience: str | None = Form(None),
|
|
status: str | None = Form(None),
|
|
referral_by: str | None = Form(None),
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
saved_path=None
|
|
try:
|
|
file_content = await file.read()
|
|
logger.info(f"Received file: {file.filename} ({len(file_content)} bytes)")
|
|
reader=FileRead(session=session,filename=file.filename,file=file_content)
|
|
# Parse first: an unreadable PDF is a 400, and doing it before the write
|
|
# keeps a file that can never back a row off the disk entirely.
|
|
parsed=await reader.injest_manual_upload()
|
|
saved=await reader.save_manual_upload()
|
|
saved_path=saved.get("file_path")
|
|
service=CandidateView(session=session)
|
|
data=await service.create_candidate(
|
|
candidate_email=candidate_email,
|
|
candidate_name=candidate_name,
|
|
candidate_phone=candidate_phone,
|
|
job_post_id=job_post_id,
|
|
current_company=current_company,
|
|
current_position=current_position,
|
|
platform=platform,
|
|
experience=experience,
|
|
status=status,
|
|
referral_by=referral_by,
|
|
file_name=saved.get("file_name"),
|
|
file_path=saved_path,
|
|
full_text=parsed.get("text") or "",
|
|
current_user=current_user.get("id"),
|
|
)
|
|
return JSONResponse(content={"data":data,"status_code":200})
|
|
except HTTPException:
|
|
# create_candidate rejects a blank email with a 422 AFTER the file has
|
|
# landed, so without this every such attempt would leave an orphan PDF.
|
|
FileRead.discard_upload(saved_path)
|
|
raise
|
|
except Exception as e:
|
|
FileRead.discard_upload(saved_path)
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
@router.get("/candidate/fetch/users")
|
|
async def fetch_users(
|
|
role_id:int=Query(8),
|
|
top:int=Query(10),
|
|
skip:int=Query(0),
|
|
search:str=Query(None),
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=User(session=session)
|
|
data=await service.get_users(role_id=role_id,top=top,skip=skip)
|
|
return JSONResponse(content={"data":data,"status_code":200})
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
@router.post("/candidate/cv_upload")
|
|
async def cv_upload(
|
|
file: UploadFile = File(...),
|
|
candidate_email: str | None = Form(None),
|
|
candidate_name: str | None = Form(None),
|
|
candidate_phone: str | None = Form(None),
|
|
job_post_id: str | None = Form(None),
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
file_content = await file.read()
|
|
logger.info(f"Received file: {file.filename} ({len(file_content)} bytes)")
|
|
service=FileRead(session=session,filename=file.filename,file=file_content)
|
|
data=await service.ingest_upload(
|
|
candidate_email=candidate_email,candidate_name=candidate_name,
|
|
current_user=current_user,
|
|
)
|
|
return JSONResponse(content={"data":data,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.post("/candidate/inbox-match")
|
|
async def candidate_inbox_match(
|
|
inbox_message_id: str = Query(...),
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=FileRead(session=session)
|
|
data=await service.match_inbox_cv(inbox_message_id,current_user=current_user)
|
|
return JSONResponse(content={"data":data,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.post("/job/post-job")
|
|
async def post_job(
|
|
payload: JobPostCreate,
|
|
current_user: dict = Depends(require_permission(PermissionTag.JOB_BOARD_CREATE)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=JobPost(session=session)
|
|
data=payload.model_dump()
|
|
if data['mode']=="customScheduled" and data.get('scheduler_date'):
|
|
data['due_at']=datetime.combine(
|
|
data['scheduler_date'],
|
|
data['scheduler_time'] or time(0, 0, 0),
|
|
tzinfo=timezone.utc,
|
|
).strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
|
result=await service.post_job(data,current_user)
|
|
return JSONResponse(content={"data":result,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
class JobAssistRequest(BaseModel):
|
|
field: Literal[
|
|
"title", "department", "location", "salary",
|
|
"requirements", "optional_skills", "description",
|
|
]
|
|
action: Literal["fix", "suggest"]
|
|
text: str = ""
|
|
context: dict = {}
|
|
|
|
|
|
@router.post("/job/assist-field")
|
|
async def assist_job_field(
|
|
payload: JobAssistRequest,
|
|
current_user: dict = Depends(require_permission(
|
|
PermissionTag.JOB_BOARD_CREATE, PermissionTag.JOBS_EDIT, require_all=False,
|
|
)),
|
|
):
|
|
"""AI assist for one job-form field: fix the recruiter's text or suggest content.
|
|
|
|
No DB session — the form state travels in the payload. RuntimeError from the
|
|
agent is a provider problem and must surface as a generic 503; its cause can
|
|
carry prompt content that never belongs in a response body.
|
|
"""
|
|
try:
|
|
suggestion = await run_field_assist(
|
|
field=payload.field,
|
|
action=payload.action,
|
|
text=payload.text,
|
|
context=payload.context,
|
|
)
|
|
return JSONResponse(content={"data": {"suggestion": suggestion}, "status_code": 200})
|
|
except HTTPException:
|
|
raise
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=422, detail=str(e))
|
|
except Exception:
|
|
raise HTTPException(status_code=503, detail="AI assist is unavailable right now. Try again shortly.")
|
|
|
|
|
|
@router.get("/job/buffer/channels")
|
|
async def buffer_channels(
|
|
current_user: dict = Depends(require_permission(PermissionTag.JOB_BOARD_VIEW)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=JobPost(session=session)
|
|
data=await service.list_channels()
|
|
return JSONResponse(content={"data":data,"total":len(data),"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
class InboxScoreRequest(BaseModel):
|
|
job_id: str
|
|
message_ids: list[str] # inbox_messages PK uuids, not Graph message ids
|
|
|
|
|
|
@router.post("/candidate/score")
|
|
async def score_candidates(
|
|
job_id: str = Form(...),
|
|
files: list[UploadFile] = File(...),
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Score uploaded CV PDFs against a job post; persists and returns the leaderboard."""
|
|
try:
|
|
pairs=[(f.filename,await f.read()) for f in files]
|
|
service=CandidateScoring(session=session)
|
|
data=await service.score_uploads(job_id,pairs,current_user)
|
|
return JSONResponse(content={"data":data,"total":len(data),"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.post("/candidate/score_inbox")
|
|
async def score_inbox_candidates(
|
|
payload: InboxScoreRequest,
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Score the decoded attachments of inbox messages against a job post."""
|
|
try:
|
|
service=CandidateScoring(session=session)
|
|
data=await service.score_inbox(payload.job_id,payload.message_ids,current_user)
|
|
return JSONResponse(content={"data":data,"total":len(data),"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.get("/candidate/scored/fetch")
|
|
async def fetch_scored_candidates(
|
|
job_id: str = Query(None),
|
|
limit: int = Query(10, ge=1, le=100),
|
|
offset: int = Query(0, ge=0),
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Persisted scored candidates, newest first. Without job_id returns the whole
|
|
pool across jobs. `total` is the full result-set size, not the page length."""
|
|
try:
|
|
service=CandidateScoring(session=session)
|
|
data,total=await service.fetch_candidates(job_id,limit=limit,offset=offset)
|
|
return JSONResponse(content={"data":data,"total":total,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.get("/job/fetch")
|
|
async def fetch_job_posts(
|
|
search: str | None = Query(None),
|
|
top: int | None = Query(10, ge=1, le=100),
|
|
skip: int = Query(0, ge=0),
|
|
ids: str | None = Query(None),
|
|
active_only: bool = Query(True),
|
|
# Either job-board or candidate viewers may list jobs — recruiters scoring
|
|
# CVs need a job to score against (CV Import picker).
|
|
current_user: dict = Depends(
|
|
require_permission(
|
|
PermissionTag.JOB_BOARD_VIEW, PermissionTag.CANDIDATES_VIEW, require_all=False
|
|
)
|
|
),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=JobPost(session=session)
|
|
id_list=[x.strip() for x in (ids or "").split(",") if x.strip()] or None
|
|
data,total=await service.fetch_job_posts(
|
|
search=search,
|
|
top=top,
|
|
skip=skip,
|
|
ids=id_list,
|
|
active_only=active_only,
|
|
)
|
|
return JSONResponse(content={"data":data,"total":total,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.get("/jobs/fetch")
|
|
async def fetch_jobs(
|
|
search: str | None = Query(None),
|
|
department: str | None = Query(None),
|
|
requisition_status: str | None = Query(None),
|
|
employment_type: str | None = Query(None),
|
|
# le=500 (not 100): the Jobs board loads a full client-side page for facets;
|
|
# a 200 ceiling used to 422 the SPA and render an empty requisition list.
|
|
top: int | None = Query(10, ge=1, le=500),
|
|
skip: int = Query(0, ge=0),
|
|
# Defaults False, unlike /job/fetch: a requisition list must show CLOSED
|
|
# requisitions, and those carry is_active = false. Soft-deleted rows are still
|
|
# excluded by the include_deleted branch in fetch_job_posts.
|
|
active_only: bool = Query(False),
|
|
current_user: dict = Depends(require_permission(PermissionTag.JOBS_VIEW)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=JobPost(session=session)
|
|
data,total=await service.fetch_jobs(
|
|
search=search,department=department,requisition_status=requisition_status,
|
|
employment_type=employment_type,top=top,skip=skip,active_only=active_only,
|
|
)
|
|
return JSONResponse(content={"data":data,"total":total,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.get("/candidate/fetch_by_id")
|
|
async def fetch_candidate_by_id(
|
|
candidate_id: str = Query(...),
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=CandidateScoring(session=session)
|
|
data=await service.fetch_candidate_by_id(candidate_id)
|
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.get("/candidate/fetch")
|
|
async def fetch_candidate(
|
|
user_id:str=Query(None),
|
|
limit:int=Query(10,ge=1,le=100),
|
|
offset:int=Query(0,ge=0),
|
|
search:str=Query(None),
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=CandidateView(session=session)
|
|
data=await service.get_candidate(user_id=user_id,limit=limit,offset=offset,search=search)
|
|
|
|
|
|
total=await service.count_candidates(user_id=user_id,search=search) if isinstance(data,list) else 1
|
|
return JSONResponse(content={"data":data,"total":total,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.patch("/candidate/update")
|
|
async def update_candidate(
|
|
user_id:str=Query(...),
|
|
payload:CandidateUpdate=...,
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=CandidateView(session=session)
|
|
data=await service.update_candidate(user_id,payload.model_dump(exclude_unset=True),current_user)
|
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.get("/candidate/history/fetch")
|
|
async def fetch_candidate_history(
|
|
user_id:str=Query(...),
|
|
limit:int=Query(200,ge=1,le=500),
|
|
offset:int=Query(0,ge=0),
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=HistoryRecorder(session=session)
|
|
data,total=await service.list_for_user(user_id,limit=limit,offset=offset)
|
|
return JSONResponse(content={"data":data,"total":total,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.get("/interview/fetch")
|
|
async def fetch_interview(
|
|
interview_id:str=Query(None),
|
|
inbox_id:int=Query(None),
|
|
from_date:datetime=Query(None),
|
|
to_date:datetime=Query(None),
|
|
status:str=Query(None),
|
|
top:int=Query(None),
|
|
skip:int=Query(0,ge=0),
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=Interview(session=session)
|
|
if not interview_id and inbox_id is None and (from_date is not None or to_date is not None or status is not None or top is not None):
|
|
data,total=await service.get_interviews_range(
|
|
from_date=from_date,to_date=to_date,status=status,top=top,skip=skip,
|
|
)
|
|
return JSONResponse(content={"data":data,"total":total,"status_code":200})
|
|
data=await service.get_interview(
|
|
interview_id=interview_id,inbox_id=inbox_id,
|
|
from_date=from_date,to_date=to_date,status=status,top=top,skip=skip,
|
|
)
|
|
if isinstance(data,tuple):
|
|
data,total=data
|
|
else:
|
|
total=1 if isinstance(data,dict) else len(data)
|
|
return JSONResponse(content={"data":data,"total":total,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.post("/interview/create")
|
|
async def create_interview(
|
|
payload:InterviewCreate,
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=Interview(session=session)
|
|
data=await service.create_interview(payload.model_dump(exclude_unset=True),current_user)
|
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.patch("/interview/update")
|
|
async def update_interview(
|
|
interview_id:str=Query(...),
|
|
payload:InterviewUpdate=...,
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=Interview(session=session)
|
|
data=await service.update_interview(interview_id,payload.model_dump(exclude_unset=True),current_user)
|
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.get("/notes/fetch")
|
|
async def fetch_notes(
|
|
note_id:str=Query(None),
|
|
user_id:str=Query(None),
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=Note(session=session)
|
|
data=await service.get_note(note_id=note_id,user_id=user_id)
|
|
total=1 if isinstance(data,dict) else len(data)
|
|
return JSONResponse(content={"data":data,"total":total,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.post("/notes/create")
|
|
async def create_note(
|
|
payload:NoteCreate,
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=Note(session=session)
|
|
data=await service.create_note(payload.model_dump(exclude_unset=True),current_user)
|
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.patch("/notes/update")
|
|
async def update_note(
|
|
note_id:str=Query(...),
|
|
payload:NoteUpdate=...,
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=Note(session=session)
|
|
data=await service.update_note(note_id,payload.model_dump(exclude_unset=True),current_user)
|
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.get("/activity/fetch")
|
|
async def fetch_activity(
|
|
activity_id:str=Query(None),
|
|
inbox_id:int=Query(None),
|
|
top:int=Query(None),
|
|
skip:int=Query(0,ge=0),
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=ActivityLog(session=session)
|
|
if not activity_id and inbox_id is None and top is not None:
|
|
data,total=await service.get_activity_feed(top=top,skip=skip)
|
|
return JSONResponse(content={"data":data,"total":total,"status_code":200})
|
|
data=await service.get_activity(activity_id=activity_id,inbox_id=inbox_id,top=top,skip=skip)
|
|
if isinstance(data,tuple):
|
|
data,total=data
|
|
else:
|
|
total=1 if isinstance(data,dict) else len(data)
|
|
return JSONResponse(content={"data":data,"total":total,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.post("/activity/create")
|
|
async def create_activity(
|
|
payload:ActivityCreate,
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=ActivityLog(session=session)
|
|
data=await service.create_activity(payload.model_dump(exclude_unset=True))
|
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.get("/feedback/fetch")
|
|
async def fetch_feedback(
|
|
feedback_id:str=Query(None),
|
|
inbox_id:int=Query(None),
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=FeedbackView(session=session)
|
|
data=await service.get_feedback(feedback_id=feedback_id,inbox_id=inbox_id)
|
|
total=1 if isinstance(data,dict) else len(data)
|
|
return JSONResponse(content={"data":data,"total":total,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.post("/feedback/create")
|
|
async def create_feedback(
|
|
payload:FeedbackCreate,
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=FeedbackView(session=session)
|
|
data=await service.create_feedback(payload.model_dump(exclude_unset=True),current_user)
|
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.patch("/feedback/update")
|
|
async def update_feedback(
|
|
feedback_id:str=Query(...),
|
|
payload:FeedbackUpdate=...,
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=FeedbackView(session=session)
|
|
data=await service.update_feedback(feedback_id,payload.model_dump(exclude_unset=True),current_user)
|
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.patch("/candidate/stage")
|
|
async def change_candidate_stage(
|
|
payload:StageChange,
|
|
current_user: dict = Depends(require_permission(PermissionTag.PIPELINE_EDIT)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=Pipeline(session=session)
|
|
data=await service.change_stage(
|
|
payload.to_stage,current_user,inbox_id=payload.inbox_id,
|
|
manual_upload_id=payload.manual_upload_id,change_reason=payload.change_reason,
|
|
)
|
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
@router.get("/pipeline/candidates/fetch")
|
|
async def fetch_pipeline_candidates(
|
|
job_post_id:Optional[uuid.UUID]=Query(None),
|
|
limit:int=Query(10,ge=1,le=1000),
|
|
offset:int=Query(0,ge=0),
|
|
current_user: dict = Depends(require_permission(PermissionTag.PIPELINE_VIEW)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=Pipeline(session=session)
|
|
result=await service.get_all(job_post_id=job_post_id,limit=limit,offset=offset)
|
|
return JSONResponse(content={**result,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.get("/pipeline/candidate/score/fetch")
|
|
async def fetch_pipeline_candidate_score(
|
|
user_id:uuid.UUID=Query(...),
|
|
job_post_id:uuid.UUID=Query(None),
|
|
current_user: dict = Depends(require_permission(PermissionTag.PIPELINE_VIEW)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=Pipeline(session=session)
|
|
data=await service.get_pipeline_candidates(user_id=user_id,job_post_id=job_post_id)
|
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.get("/pipeline/transitions/fetch")
|
|
async def fetch_pipeline_transitions(
|
|
transition_id:str=Query(None),
|
|
inbox_id:int=Query(None),
|
|
manual_upload_id:UUID=Query(None),
|
|
current_user: dict = Depends(require_permission(PermissionTag.PIPELINE_VIEW)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=Pipeline(session=session)
|
|
data=await service.get_transitions(inbox_id=inbox_id,transition_id=transition_id,manual_upload_id=manual_upload_id)
|
|
total=1 if isinstance(data,dict) else len(data)
|
|
return JSONResponse(content={"data":data,"total":total,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.get("/job/assignments/fetch")
|
|
async def fetch_job_assignments(
|
|
job_post_id:str=Query(...),
|
|
current_user: dict = Depends(require_permission(PermissionTag.JOBS_VIEW)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=Assignment(session=session)
|
|
data=await service.list_job_assignments(job_post_id)
|
|
return JSONResponse(content={"data":data,"total":len(data),"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.post("/job/assignments/create")
|
|
async def create_job_assignment(
|
|
payload:JobAssignmentCreate,
|
|
current_user: dict = Depends(require_permission(PermissionTag.JOBS_EDIT)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=Assignment(session=session)
|
|
data=await service.create_job_assignment(payload.model_dump(exclude_unset=True),current_user)
|
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.get("/candidate/assignments/fetch")
|
|
async def fetch_application_assignments(
|
|
inbox_id:int=Query(...),
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=Assignment(session=session)
|
|
data=await service.list_application_assignments(inbox_id)
|
|
return JSONResponse(content={"data":data,"total":len(data),"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.post("/candidate/assignments/create")
|
|
async def create_application_assignment(
|
|
payload:ApplicationAssignmentCreate,
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=Assignment(session=session)
|
|
data=await service.create_application_assignment(payload.model_dump(exclude_unset=True),current_user)
|
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.get("/job/costs/fetch")
|
|
async def fetch_hiring_costs(
|
|
job_post_id:str=Query(None),
|
|
from_date:datetime=Query(None),
|
|
to_date:datetime=Query(None),
|
|
top:int=Query(None),
|
|
skip:int=Query(0,ge=0),
|
|
current_user: dict = Depends(require_permission(PermissionTag.JOBS_VIEW)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=HiringCost(session=session)
|
|
data,total=await service.list_costs(
|
|
job_post_id=job_post_id,from_date=from_date,to_date=to_date,top=top,skip=skip,
|
|
)
|
|
return JSONResponse(content={"data":data,"total":total,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.post("/job/costs/create")
|
|
async def create_hiring_cost(
|
|
payload:HiringCostCreate,
|
|
current_user: dict = Depends(require_permission(PermissionTag.JOBS_EDIT)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=HiringCost(session=session)
|
|
data=await service.create_cost(payload.model_dump(exclude_unset=True),current_user)
|
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.patch("/jobs/update")
|
|
async def update_job(
|
|
payload:JobUpdate,
|
|
job_post_id:str=Query(...),
|
|
current_user: dict = Depends(require_permission(PermissionTag.JOBS_EDIT)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=JobPost(session=session)
|
|
data=await service.update_job(job_post_id,payload.model_dump(exclude_unset=True),current_user)
|
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.delete("/jobs/delete")
|
|
async def delete_job(
|
|
job_post_id:str=Query(...),
|
|
current_user: dict = Depends(require_permission(PermissionTag.JOBS_DELETE)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=JobPost(session=session)
|
|
data=await service.delete_job(job_post_id,current_user)
|
|
return JSONResponse(content={"data":data,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.patch("/jobs/status")
|
|
async def set_job_status(
|
|
payload:JobStatusUpdate,
|
|
job_post_id:str=Query(...),
|
|
current_user: dict = Depends(require_permission(PermissionTag.JOBS_EDIT)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=JobPost(session=session)
|
|
data=await service.set_job_status(job_post_id,payload.model_dump(exclude_unset=True),current_user)
|
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.get("/feedback/templates/fetch")
|
|
async def fetch_feedback_templates(
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=FeedbackView(session=session)
|
|
data,total=await service.get_templates()
|
|
return JSONResponse(content={"data":data,"total":total,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.post("/feedback/templates/create")
|
|
async def create_feedback_template(
|
|
payload:FeedbackTemplateCreate,
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_CREATE)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=FeedbackView(session=session)
|
|
data=await service.create_template(payload.model_dump(exclude_unset=True),current_user)
|
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.patch("/feedback/templates/update")
|
|
async def update_feedback_template(
|
|
payload:FeedbackTemplateUpdate,
|
|
template_id:str=Query(...),
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_EDIT)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=FeedbackView(session=session)
|
|
data=await service.update_template(template_id,payload.model_dump(exclude_unset=True))
|
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.delete("/feedback/templates/delete")
|
|
async def delete_feedback_template(
|
|
template_id:str=Query(...),
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_DELETE)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=FeedbackView(session=session)
|
|
data=await service.delete_template(template_id)
|
|
return JSONResponse(content={"data":data,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
|
|
|
|
@router.get("/documents/download")
|
|
async def download_document(
|
|
inbox_id:int|None=Query(None),
|
|
manual_upload_candidate_id:str|None=Query(None),
|
|
index:int=Query(0,ge=0),
|
|
current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)),
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
try:
|
|
service=CandidateView(session=session)
|
|
path,filename=await service.download_document(inbox_id,manual_upload_candidate_id,index)
|
|
return FileResponse(
|
|
path=str(path),
|
|
filename=filename,
|
|
media_type="application/octet-stream",
|
|
content_disposition_type="attachment",
|
|
)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|