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 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 @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), current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=Interview(session=session) data=await service.get_interview(interview_id=interview_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("/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), current_user: dict = Depends(require_permission(PermissionTag.CANDIDATES_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=ActivityLog(session=session) data=await service.get_activity(activity_id=activity_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("/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))