📄 admin.py 9,906 bytes Wednesday 16:02 📋 Raw

"""
Admin Management API Endpoints
GET /api/v1/admin/roster - Full roster with parent emails
GET /api/v1/admin/coaches - All coaches with case counts
GET /api/v1/admin/stats - Extended stats by sport and grade
"""
from typing import List, Optional, Dict
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException, Query, status, Request
from sqlalchemy.orm import Session
from sqlalchemy import and_
from pydantic import BaseModel

from app.database import get_db
from app.models import Athlete, Case, Event, School, User, Milestone
from app.auth import get_current_user, require_role

router = APIRouter(tags=["admin"])

============== SCHEMAS ==============

class AdminRosterAthlete(BaseModel):
"""Athlete row in admin roster"""
id: str
first_name: str
last_name: str
grade: int
sports: List[str]
team: Optional[str] = None
current_status: str
parent_emails: List[str] = []
active_case_count: int = 0

class AdminRosterResponse(BaseModel):
"""Admin roster response"""
athletes: List[AdminRosterAthlete]
total: int

class AdminCoachSummary(BaseModel):
"""Coach summary in admin view"""
id: str
first_name: str
last_name: str
email: str
assigned_sports: List[str]
assigned_teams: List[str]
active_cases_count: int = 0

class AdminCoachesResponse(BaseModel):
"""Admin coaches response"""
coaches: List[AdminCoachSummary]

class SportStats(BaseModel):
"""Stats by sport"""
name: str
athletes: int
active_cases: int
coaches: int
ats: int

class GradeStats(BaseModel):
"""Stats by grade"""
grade: int
athletes: int
cases: int

class AdminStatsResponse(BaseModel):
"""Admin extended stats"""
total_athletes: int
active_cases: int
out_today: int
modified: int
by_sport: List[SportStats]
by_grade: List[GradeStats]

============== ADMIN ROSTER ENDPOINT ==============

@router.get("/roster", response_model=AdminRosterResponse)
async def get_admin_roster(
request: Request,
school_id: str = Query(..., description="School identifier"),
current_user: User = Depends(require_role(["admin", "at", "ad"])),
db: Session = Depends(get_db),
):
"""
Get full roster with management data for admin/AT

Returns athletes with parent emails resolved from users table
"""
if current_user.school_id != school_id:
    raise HTTPException(
        status_code=status.HTTP_403_FORBIDDEN,
        detail="Access denied for this school"
    )

school = db.query(School).filter(School.id == school_id).first()
if not school:
    raise HTTPException(
        status_code=status.HTTP_404_NOT_FOUND,
        detail=f"School '{school_id}' not found"
    )

# Get all athletes
athletes = db.query(Athlete).filter(
    Athlete.school_id == school_id
).order_by(Athlete.last_name, Athlete.first_name).all()

# Get all users for email resolution
all_users = db.query(User).filter(User.school_id == school_id).all()
user_map = {u.id: u.email for u in all_users}

# Build active case counts
active_case_counts = {}
active_cases = db.query(Case).filter(
    and_(Case.school_id == school_id, Case.status == "active")
).all()
for c in active_cases:
    active_case_counts[c.athlete_id] = active_case_counts.get(c.athlete_id, 0) + 1

roster_athletes = []
for a in athletes:
    # Resolve parent emails
    parent_emails = []
    for pid in (a.parent_ids or []):
        if pid in user_map:
            parent_emails.append(user_map[pid])

    roster_athletes.append(AdminRosterAthlete(
        id=a.id,
        first_name=a.first_name,
        last_name=a.last_name,
        grade=a.grade,
        sports=a.sports or [],
        team=a.team,
        current_status=a.current_status,
        parent_emails=parent_emails,
        active_case_count=active_case_counts.get(a.id, 0)
    ))

return AdminRosterResponse(
    athletes=roster_athletes,
    total=len(roster_athletes)
)

============== ADMIN COACHES ENDPOINT ==============

@router.get("/coaches", response_model=AdminCoachesResponse)
async def get_admin_coaches(
request: Request,
school_id: str = Query(..., description="School identifier"),
current_user: User = Depends(require_role(["admin", "at", "ad"])),
db: Session = Depends(get_db),
):
"""
Get all coaches for this school with case counts
"""
if current_user.school_id != school_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied for this school"
)

school = db.query(School).filter(School.id == school_id).first()
if not school:
    raise HTTPException(
        status_code=status.HTTP_404_NOT_FOUND,
        detail=f"School '{school_id}' not found"
    )

# Get all coaches for this school
coaches = db.query(User).filter(
    and_(User.school_id == school_id, User.role == "coach")
).order_by(User.last_name, User.first_name).all()

# Get active case count per sport
active_cases = db.query(Case).filter(
    and_(Case.school_id == school_id, Case.status == "active")
).all()

# Get athletes to resolve sport from case
all_athletes = db.query(Athlete).filter(Athlete.school_id == school_id).all()
athlete_map = {a.id: a for a in all_athletes}

# Count active cases per sport
sport_case_count = {}
for c in active_cases:
    athlete = athlete_map.get(c.athlete_id)
    if athlete and athlete.sports:
        for s in athlete.sports:
            sport_case_count[s] = sport_case_count.get(s, 0) + 1

coach_list = []
for coach in coaches:
    assigned_sports = coach.assigned_sports or []
    coach_active_count = sum(sport_case_count.get(s, 0) for s in assigned_sports)

    coach_list.append(AdminCoachSummary(
        id=coach.id,
        first_name=coach.first_name,
        last_name=coach.last_name,
        email=coach.email,
        assigned_sports=assigned_sports,
        assigned_teams=coach.assigned_teams or [],
        active_cases_count=coach_active_count
    ))

return AdminCoachesResponse(coaches=coach_list)

============== ADMIN STATS ENDPOINT ==============

@router.get("/stats", response_model=AdminStatsResponse)
async def get_admin_stats(
request: Request,
school_id: str = Query(..., description="School identifier"),
current_user: User = Depends(require_role(["admin", "at", "ad"])),
db: Session = Depends(get_db),
):
"""
Get extended admin stats broken down by sport and grade
"""
if current_user.school_id != school_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied for this school"
)

school = db.query(School).filter(School.id == school_id).first()
if not school:
    raise HTTPException(
        status_code=status.HTTP_404_NOT_FOUND,
        detail=f"School '{school_id}' not found"
    )

# Get all athletes
athletes = db.query(Athlete).filter(Athlete.school_id == school_id).all()
total_athletes = len(athletes)

# Stats
out_count = sum(1 for a in athletes if a.current_status == "out")
modified_count = sum(1 for a in athletes if a.current_status == "restricted")

# Active cases
active_cases = db.query(Case).filter(
    and_(Case.school_id == school_id, Case.status == "active")
).all()
active_case_count = len(active_cases)

# Get coaches per sport
coaches = db.query(User).filter(
    and_(User.school_id == school_id, User.role == "coach")
).all()

# Get ATs
ats = db.query(User).filter(
    and_(User.school_id == school_id, User.role == "at")
).all()

# Map athlete_id to case for sport resolution
athlete_case_map = {c.athlete_id: c for c in active_cases}

# Build sport stats
all_sports = set()
for a in athletes:
    for s in (a.sports or []):
        all_sports.add(s)

by_sport = []
for sport in sorted(all_sports):
    sport_athletes = [a for a in athletes if sport in (a.sports or [])]
    sport_active_cases = sum(1 for a in sport_athletes if a.id in athlete_case_map)
    sport_coaches = sum(1 for c in coaches if sport in (c.assigned_sports or []))
    sport_ats = len(ats) if ats else 0

    by_sport.append(SportStats(
        name=sport,
        athletes=len(sport_athletes),
        active_cases=sport_active_cases,
        coaches=sport_coaches,
        ats=sport_ats
    ))

by_sport.sort(key=lambda x: x.athletes, reverse=True)

# Build grade stats
grade_counts = {}
athlete_map = {a.id: a for a in athletes}
for a in athletes:
    g = a.grade
    if g not in grade_counts:
        grade_counts[g] = {"athletes": 0, "cases": 0}
    grade_counts[g]["athletes"] += 1

for c in active_cases:
    athlete = athlete_map.get(c.athlete_id)
    if athlete and athlete.grade in grade_counts:
        grade_counts[athlete.grade]["cases"] += 1

by_grade = []
for grade in sorted(grade_counts.keys()):
    by_grade.append(GradeStats(
        grade=grade,
        athletes=grade_counts[grade]["athletes"],
        cases=grade_counts[grade]["cases"]
    ))

return AdminStatsResponse(
    total_athletes=total_athletes,
    active_cases=active_case_count,
    out_today=out_count,
    modified=modified_count,
    by_sport=by_sport,
    by_grade=by_grade
)