""" Schools API - Staff Routes. CRUD and search endpoints for school staff members. """ import logging from typing import Optional from fastapi import APIRouter, HTTPException, Query from .schools_db import get_db_pool from .schools_models import ( SchoolStaffBase, SchoolStaffResponse, SchoolStaffListResponse, ) logger = logging.getLogger(__name__) router = APIRouter(tags=["schools"]) # ============================================================================= # School Staff Endpoints # ============================================================================= @router.get("/{school_id}/staff", response_model=SchoolStaffListResponse) async def get_school_staff(school_id: str): """Get staff members for a school.""" pool = await get_db_pool() async with pool.acquire() as conn: rows = await conn.fetch(""" SELECT ss.id, ss.school_id, ss.first_name, ss.last_name, ss.full_name, ss.title, ss.position, ss.position_type, ss.subjects, ss.email, ss.phone, ss.profile_url, ss.photo_url, ss.is_active, ss.created_at, s.name as school_name FROM school_staff ss JOIN schools s ON ss.school_id = s.id WHERE ss.school_id = $1 AND ss.is_active = TRUE ORDER BY CASE ss.position_type WHEN 'principal' THEN 1 WHEN 'vice_principal' THEN 2 WHEN 'secretary' THEN 3 ELSE 4 END, ss.last_name """, school_id) staff = [ SchoolStaffResponse( id=str(row["id"]), school_id=str(row["school_id"]), school_name=row["school_name"], first_name=row["first_name"], last_name=row["last_name"], full_name=row["full_name"], title=row["title"], position=row["position"], position_type=row["position_type"], subjects=row["subjects"], email=row["email"], phone=row["phone"], profile_url=row["profile_url"], photo_url=row["photo_url"], is_active=row["is_active"], created_at=row["created_at"], ) for row in rows ] return SchoolStaffListResponse( staff=staff, total=len(staff), ) @router.post("/{school_id}/staff", response_model=SchoolStaffResponse) async def create_school_staff(school_id: str, staff: SchoolStaffBase): """Add a staff member to a school.""" pool = await get_db_pool() async with pool.acquire() as conn: # Verify school exists school = await conn.fetchrow("SELECT name FROM schools WHERE id = $1", school_id) if not school: raise HTTPException(status_code=404, detail="School not found") # Create full name full_name = staff.full_name if not full_name: parts = [] if staff.title: parts.append(staff.title) if staff.first_name: parts.append(staff.first_name) parts.append(staff.last_name) full_name = " ".join(parts) row = await conn.fetchrow(""" INSERT INTO school_staff ( school_id, first_name, last_name, full_name, title, position, position_type, subjects, email, phone ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at """, school_id, staff.first_name, staff.last_name, full_name, staff.title, staff.position, staff.position_type, staff.subjects, staff.email, staff.phone, ) return SchoolStaffResponse( id=str(row["id"]), school_id=school_id, school_name=school["name"], first_name=staff.first_name, last_name=staff.last_name, full_name=full_name, title=staff.title, position=staff.position, position_type=staff.position_type, subjects=staff.subjects, email=staff.email, phone=staff.phone, is_active=True, created_at=row["created_at"], ) # ============================================================================= # Search Endpoints # ============================================================================= @router.get("/search/staff", response_model=SchoolStaffListResponse) async def search_school_staff( q: Optional[str] = Query(None, description="Search query"), state: Optional[str] = Query(None, description="Filter by state"), position_type: Optional[str] = Query(None, description="Filter by position type"), has_email: Optional[bool] = Query(None, description="Only staff with email"), page: int = Query(1, ge=1), page_size: int = Query(50, ge=1, le=200), ): """Search school staff across all schools.""" pool = await get_db_pool() async with pool.acquire() as conn: conditions = ["ss.is_active = TRUE", "s.is_active = TRUE"] params = [] param_idx = 1 if q: conditions.append(f""" (LOWER(ss.full_name) LIKE LOWER(${param_idx}) OR LOWER(ss.last_name) LIKE LOWER(${param_idx}) OR LOWER(s.name) LIKE LOWER(${param_idx})) """) params.append(f"%{q}%") param_idx += 1 if state: conditions.append(f"s.state = ${param_idx}") params.append(state.upper()) param_idx += 1 if position_type: conditions.append(f"ss.position_type = ${param_idx}") params.append(position_type) param_idx += 1 if has_email is not None and has_email: conditions.append("ss.email IS NOT NULL") where_clause = " AND ".join(conditions) # Count total total = await conn.fetchval(f""" SELECT COUNT(*) FROM school_staff ss JOIN schools s ON ss.school_id = s.id WHERE {where_clause} """, *params) # Fetch staff offset = (page - 1) * page_size rows = await conn.fetch(f""" SELECT ss.id, ss.school_id, ss.first_name, ss.last_name, ss.full_name, ss.title, ss.position, ss.position_type, ss.subjects, ss.email, ss.phone, ss.profile_url, ss.photo_url, ss.is_active, ss.created_at, s.name as school_name FROM school_staff ss JOIN schools s ON ss.school_id = s.id WHERE {where_clause} ORDER BY ss.last_name, ss.first_name LIMIT ${param_idx} OFFSET ${param_idx + 1} """, *params, page_size, offset) staff = [ SchoolStaffResponse( id=str(row["id"]), school_id=str(row["school_id"]), school_name=row["school_name"], first_name=row["first_name"], last_name=row["last_name"], full_name=row["full_name"], title=row["title"], position=row["position"], position_type=row["position_type"], subjects=row["subjects"], email=row["email"], phone=row["phone"], profile_url=row["profile_url"], photo_url=row["photo_url"], is_active=row["is_active"], created_at=row["created_at"], ) for row in rows ] return SchoolStaffListResponse( staff=staff, total=total, )