backend-lehrer (11 files): - llm_gateway/routes/schools.py (867 → 5), recording_api.py (848 → 6) - messenger_api.py (840 → 5), print_generator.py (824 → 5) - unit_analytics_api.py (751 → 5), classroom/routes/context.py (726 → 4) - llm_gateway/routes/edu_search_seeds.py (710 → 4) klausur-service (12 files): - ocr_labeling_api.py (845 → 4), metrics_db.py (833 → 4) - legal_corpus_api.py (790 → 4), page_crop.py (758 → 3) - mail/ai_service.py (747 → 4), github_crawler.py (767 → 3) - trocr_service.py (730 → 4), full_compliance_pipeline.py (723 → 4) - dsfa_rag_api.py (715 → 4), ocr_pipeline_auto.py (705 → 4) website (6 pages): - audit-checklist (867 → 8), content (806 → 6) - screen-flow (790 → 4), scraper (789 → 5) - zeugnisse (776 → 5), modules (745 → 4) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
234 lines
7.8 KiB
Python
234 lines
7.8 KiB
Python
"""
|
|
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,
|
|
)
|