from fastapi import APIRouter,Depends,Query from fastapi.responses import JSONResponse from fastapi import HTTPException from db_setup import get_session from job.candidate.views import 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.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 import logging from job.job_post.plugins import PlatformAlias 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 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 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 @router.get("/jobs/alias") async def get_job_alias(): try: alias_lst=[k.name for k in PlatformAlias] 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), 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, 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.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, ) 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) 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": 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)) @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)) @router.get("/job/fetch") async def fetch_job_posts( search: str | None = Query(None), top: int | None = Query(None), skip: int = Query(0, ge=0), ids: str | None = Query(None), active_only: bool = Query(True), current_user: dict = Depends(require_permission(PermissionTag.JOB_BOARD_VIEW)), 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("/candidate/fetch") async def fetch_candidate( user_id:str=Query(None), limit:int=Query(10), offset:int=Query(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)) 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("/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)) 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)) 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)) 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)) 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.inbox_id,payload.to_stage,current_user,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/transitions/fetch") async def fetch_pipeline_transitions( transition_id:str=Query(None), inbox_id:int=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) 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))