""" Interviews API endpoints """ from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy.orm import Session, joinedload from sqlalchemy import or_, and_ from models import get_db, Interview, User, Candidate, Job from api.auth import get_current_active_user, require_recruiter, require_interviewer from api.schemas import ( InterviewCreate, InterviewUpdate, InterviewResponse, BaseResponse ) from datetime import datetime router = APIRouter(prefix="/interviews", tags=["Interviews"]) @router.get("/", response_model=List[InterviewResponse]) async def get_interviews( skip: int = Query(0, ge=0), limit: int = Query(10, ge=1, le=10000), status: Optional[str] = Query(None), interview_type: Optional[str] = Query(None), candidate_id: Optional[int] = Query(None), job_id: Optional[int] = Query(None), db: Session = Depends(get_db), current_user: User = Depends(get_current_active_user) ): """ Get all interviews with optional filtering """ query = db.query(Interview).options( joinedload(Interview.candidate), joinedload(Interview.job), joinedload(Interview.interviewer_user) ) # Apply filters if status: query = query.filter(Interview.status == status.lower()) if interview_type: query = query.filter(Interview.interview_type == interview_type.lower()) if candidate_id: query = query.filter(Interview.candidate_id == candidate_id) if job_id: query = query.filter(Interview.job_id == job_id) # Order by scheduled date query = query.order_by(Interview.scheduled_at.desc()) interviews = query.offset(skip).limit(limit).all() return interviews @router.get("/stats") async def get_interview_stats( db: Session = Depends(get_db), current_user: User = Depends(get_current_active_user) ): """ Get interview statistics """ total_interviews = db.query(Interview).count() scheduled = db.query(Interview).filter(Interview.status == "scheduled").count() completed = db.query(Interview).filter(Interview.status == "completed").count() ai_interviews = db.query(Interview).filter(Interview.interview_type == "ai_interview").count() live_interviews = db.query(Interview).filter(Interview.interview_type == "live_interview").count() return { "total_interviews": total_interviews, "scheduled": scheduled, "completed": completed, "ai_interviews": ai_interviews, "live_interviews": live_interviews } @router.get("/scheduled-today") async def get_scheduled_today( db: Session = Depends(get_db), current_user: User = Depends(get_current_active_user) ): """ Get interviews scheduled for today """ from datetime import date today = date.today() interviews = db.query(Interview).filter( and_( Interview.status == "scheduled", Interview.scheduled_at >= today, Interview.scheduled_at < today.replace(day=today.day + 1) ) ).order_by(Interview.scheduled_at).all() return [interview.to_dict() for interview in interviews] @router.get("/{interview_id}", response_model=InterviewResponse) async def get_interview( interview_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_active_user) ): """ Get a specific interview by ID """ interview = db.query(Interview).options( joinedload(Interview.candidate), joinedload(Interview.job), joinedload(Interview.interviewer_user) ).filter(Interview.id == interview_id).first() if not interview: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Interview not found" ) return interview @router.post("/", response_model=InterviewResponse) async def create_interview( interview_data: InterviewCreate, db: Session = Depends(get_db), current_user: User = Depends(require_recruiter) ): """ Schedule a new interview """ # Validate candidate exists candidate = db.query(Candidate).filter(Candidate.id == interview_data.candidate_id).first() if not candidate: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Candidate not found" ) # Validate job exists job = db.query(Job).filter(Job.id == interview_data.job_id).first() if not job: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Job not found" ) # Validate interviewer exists (if provided) if interview_data.interviewer_id: interviewer = db.query(User).filter(User.id == interview_data.interviewer_id).first() if not interviewer: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Interviewer not found" ) # Create interview interview_dict = interview_data.dict() interview_dict["status"] = "scheduled" # For AI interviews, ensure no interviewer is assigned if interview_data.interview_type == "ai_interview": interview_dict["interviewer_id"] = None interview = Interview(**interview_dict) db.add(interview) db.commit() db.refresh(interview) # Update candidate status from models.utils import update_candidate_status update_candidate_status(db, interview_data.candidate_id, "interview_scheduled") # Send email notifications (don't block on failure) try: from modules.notification_service import send_interview_notifications notification_result = send_interview_notifications(db, interview, 'created') if notification_result['errors']: # Log errors but don't fail the request print(f"Email notification errors: {notification_result['errors']}") except ImportError as e: print(f"Failed to import notification service: {str(e)}") except Exception as e: # Log error but don't block interview creation print(f"Failed to send interview notifications: {str(e)}") import traceback traceback.print_exc() return interview @router.put("/{interview_id}", response_model=InterviewResponse) async def update_interview( interview_id: int, interview_data: InterviewUpdate, db: Session = Depends(get_db), current_user: User = Depends(require_recruiter) ): """ Update an interview """ interview = db.query(Interview).filter(Interview.id == interview_id).first() if not interview: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Interview not found" ) # Track if scheduled_at changed for rescheduling notifications scheduled_at_changed = False if interview_data.scheduled_at and interview_data.scheduled_at != interview.scheduled_at: scheduled_at_changed = True # Validate status transition if status is being updated update_data = interview_data.dict(exclude_unset=True) if 'status' in update_data and update_data['status'] != interview.status: from models.utils import validate_interview_status_transition is_valid, error_message = validate_interview_status_transition( interview.status, update_data['status'] ) if not is_valid: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=error_message ) # Record timestamps for status changes new_status = update_data['status'].lower() if new_status == 'completed' and not interview.completed_at: interview.completed_at = datetime.utcnow() elif new_status == 'cancelled' and not interview.cancelled_at: interview.cancelled_at = datetime.utcnow() # Update fields for field, value in update_data.items(): if hasattr(interview, field): setattr(interview, field, value) # Update the updated_at timestamp interview.updated_at = datetime.utcnow() db.commit() db.refresh(interview) # Send rescheduling notifications if date/time changed if scheduled_at_changed: try: from modules.notification_service import send_interview_notifications notification_result = send_interview_notifications(db, interview, 'updated') if notification_result['errors']: print(f"Email notification errors: {notification_result['errors']}") except Exception as e: print(f"Failed to send rescheduling notifications: {str(e)}") return interview @router.delete("/{interview_id}", response_model=BaseResponse) async def delete_interview( interview_id: int, cancellation_reason: Optional[str] = None, db: Session = Depends(get_db), current_user: User = Depends(require_recruiter) ): """ Cancel/delete an interview """ interview = db.query(Interview).filter(Interview.id == interview_id).first() if not interview: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Interview not found" ) # Set status to cancelled and record timestamp interview.status = "cancelled" interview.cancelled_at = datetime.utcnow() interview.cancellation_reason = cancellation_reason or "Cancelled by recruiter" db.commit() # Send cancellation notifications try: from modules.notification_service import send_interview_notifications notification_result = send_interview_notifications(db, interview, 'cancelled') if notification_result['errors']: print(f"Email notification errors: {notification_result['errors']}") except Exception as e: print(f"Failed to send cancellation notifications: {str(e)}") return {"success": True, "message": "Interview cancelled successfully"} @router.post("/{interview_id}/start", response_model=InterviewResponse) async def start_interview( interview_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_interviewer) ): """ Start an interview """ interview = db.query(Interview).filter(Interview.id == interview_id).first() if not interview: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Interview not found" ) # Validate status transition from models.utils import validate_interview_status_transition is_valid, error_message = validate_interview_status_transition( interview.status, 'in_progress' ) if not is_valid: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=error_message ) interview.status = "in_progress" interview.started_at = datetime.utcnow() db.commit() db.refresh(interview) return interview @router.post("/{interview_id}/complete", response_model=InterviewResponse) async def complete_interview( interview_id: int, feedback: Optional[str] = None, rating: Optional[int] = None, db: Session = Depends(get_db), current_user: User = Depends(require_interviewer) ): """ Complete an interview """ interview = db.query(Interview).filter(Interview.id == interview_id).first() if not interview: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Interview not found" ) # Validate status transition from models.utils import validate_interview_status_transition is_valid, error_message = validate_interview_status_transition( interview.status, 'completed' ) if not is_valid: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=error_message ) interview.status = "completed" interview.completed_at = datetime.utcnow() if feedback: interview.interviewer_feedback = feedback if rating: interview.interviewer_rating = rating db.commit() db.refresh(interview) # Update candidate status from models.utils import update_candidate_status update_candidate_status(db, interview.candidate_id, "interview_completed") return interview @router.post("/{interview_id}/score") async def update_interview_scores( interview_id: int, scores: dict, db: Session = Depends(get_db), current_user: User = Depends(require_interviewer) ): """ Update interview scores (for AI interviews) """ interview = db.query(Interview).filter(Interview.id == interview_id).first() if not interview: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Interview not found" ) # Update scores if "overall_score" in scores: interview.overall_score = scores["overall_score"] if "technical_score" in scores: interview.technical_score = scores["technical_score"] if "communication_score" in scores: interview.communication_score = scores["communication_score"] if "problem_solving_score" in scores: interview.problem_solving_score = scores["problem_solving_score"] if "cultural_fit_score" in scores: interview.cultural_fit_score = scores["cultural_fit_score"] if "clarity_score" in scores: interview.clarity_score = scores["clarity_score"] if "relevance_score" in scores: interview.relevance_score = scores["relevance_score"] if "conciseness_score" in scores: interview.conciseness_score = scores["conciseness_score"] db.commit() # Also update candidate overall score candidate = db.query(Candidate).filter(Candidate.id == interview.candidate_id).first() if candidate and interview.overall_score: candidate.overall_score = interview.overall_score candidate.technical_score = interview.technical_score candidate.communication_score = interview.communication_score candidate.cultural_fit_score = interview.cultural_fit_score db.commit() return {"success": True, "message": "Scores updated successfully"}