
- Complete FastAPI backend with SQLite database
- AI-powered resume parsing and job matching using OpenAI
- JWT authentication with role-based access control
- Resume upload, job management, and matching endpoints
- Recruiter dashboard with candidate ranking
- Analytics and skill gap analysis features
- Comprehensive API documentation with OpenAPI
- Alembic database migrations
- File upload support for PDF, DOCX, and TXT resumes
- CORS enabled for frontend integration
🤖 Generated with BackendIM
Co-Authored-By: Claude <noreply@anthropic.com>
154 lines
5.0 KiB
Python
154 lines
5.0 KiB
Python
from fastapi import APIRouter, Depends, Query, HTTPException
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import func, desc
|
|
from app.core.deps import get_db, get_current_recruiter
|
|
from app.models.user import User
|
|
from app.models.job import Job
|
|
from app.models.match import Match
|
|
from app.models.resume import Resume
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/candidates/{job_id}")
|
|
def get_candidates_for_job(
|
|
job_id: int,
|
|
min_score: float = Query(0, ge=0, le=100),
|
|
limit: int = Query(50, ge=1, le=100),
|
|
current_user: User = Depends(get_current_recruiter),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Get ranked candidates for a specific job"""
|
|
# Verify job belongs to recruiter
|
|
job = db.query(Job).filter(
|
|
Job.id == job_id,
|
|
Job.recruiter_id == current_user.id
|
|
).first()
|
|
|
|
if not job:
|
|
raise HTTPException(status_code=404, detail="Job not found")
|
|
|
|
# Get matches for this job, ordered by score
|
|
matches = db.query(Match).join(Resume).join(User).filter(
|
|
Match.job_id == job_id,
|
|
Match.match_score >= min_score
|
|
).order_by(desc(Match.match_score)).limit(limit).all()
|
|
|
|
# Format response with candidate info
|
|
candidates = []
|
|
for match in matches:
|
|
resume = db.query(Resume).filter(Resume.id == match.resume_id).first()
|
|
user = db.query(User).filter(User.id == match.user_id).first()
|
|
|
|
candidates.append({
|
|
"match_id": match.id,
|
|
"candidate_name": user.full_name,
|
|
"candidate_email": user.email,
|
|
"resume_title": resume.title,
|
|
"match_score": match.match_score,
|
|
"skill_match_score": match.skill_match_score,
|
|
"experience_match_score": match.experience_match_score,
|
|
"education_match_score": match.education_match_score,
|
|
"skills": resume.skills,
|
|
"experience_years": resume.experience_years,
|
|
"education_level": resume.education_level,
|
|
"created_at": match.created_at
|
|
})
|
|
|
|
return {
|
|
"job_title": job.title,
|
|
"job_company": job.company,
|
|
"total_candidates": len(candidates),
|
|
"candidates": candidates
|
|
}
|
|
|
|
|
|
@router.get("/jobs/stats")
|
|
def get_job_stats(
|
|
current_user: User = Depends(get_current_recruiter),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Get statistics for recruiter's jobs"""
|
|
# Get job stats
|
|
job_stats = db.query(
|
|
Job.id,
|
|
Job.title,
|
|
Job.company,
|
|
Job.created_at,
|
|
func.count(Match.id).label("total_matches"),
|
|
func.avg(Match.match_score).label("avg_match_score"),
|
|
func.max(Match.match_score).label("best_match_score")
|
|
).outerjoin(Match).filter(
|
|
Job.recruiter_id == current_user.id
|
|
).group_by(Job.id).all()
|
|
|
|
return [
|
|
{
|
|
"job_id": stat.id,
|
|
"job_title": stat.title,
|
|
"company": stat.company,
|
|
"created_at": stat.created_at,
|
|
"total_matches": stat.total_matches or 0,
|
|
"avg_match_score": round(stat.avg_match_score or 0, 2),
|
|
"best_match_score": stat.best_match_score or 0
|
|
}
|
|
for stat in job_stats
|
|
]
|
|
|
|
|
|
@router.get("/overview")
|
|
def get_dashboard_overview(
|
|
current_user: User = Depends(get_current_recruiter),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Get overview statistics for recruiter dashboard"""
|
|
# Total jobs
|
|
total_jobs = db.query(Job).filter(Job.recruiter_id == current_user.id).count()
|
|
|
|
# Active jobs
|
|
active_jobs = db.query(Job).filter(
|
|
Job.recruiter_id == current_user.id,
|
|
Job.is_active
|
|
).count()
|
|
|
|
# Total matches across all jobs
|
|
total_matches = db.query(Match).join(Job).filter(
|
|
Job.recruiter_id == current_user.id
|
|
).count()
|
|
|
|
# High-quality matches (score >= 80)
|
|
high_quality_matches = db.query(Match).join(Job).filter(
|
|
Job.recruiter_id == current_user.id,
|
|
Match.match_score >= 80
|
|
).count()
|
|
|
|
# Recent matches (last 7 days)
|
|
from datetime import datetime, timedelta
|
|
recent_matches = db.query(Match).join(Job).filter(
|
|
Job.recruiter_id == current_user.id,
|
|
Match.created_at >= datetime.utcnow() - timedelta(days=7)
|
|
).count()
|
|
|
|
# Top performing job
|
|
top_job = db.query(
|
|
Job.title,
|
|
func.count(Match.id).label("match_count"),
|
|
func.avg(Match.match_score).label("avg_score")
|
|
).outerjoin(Match).filter(
|
|
Job.recruiter_id == current_user.id
|
|
).group_by(Job.id).order_by(
|
|
desc(func.count(Match.id))
|
|
).first()
|
|
|
|
return {
|
|
"total_jobs": total_jobs,
|
|
"active_jobs": active_jobs,
|
|
"total_matches": total_matches,
|
|
"high_quality_matches": high_quality_matches,
|
|
"recent_matches": recent_matches,
|
|
"top_performing_job": {
|
|
"title": top_job.title if top_job else None,
|
|
"match_count": top_job.match_count if top_job else 0,
|
|
"avg_score": round(top_job.avg_score or 0, 2) if top_job else 0
|
|
}
|
|
} |