"""School admin portal — school-scoped read endpoints.""" from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, desc, and_ from datetime import date, timedelta from app.auth.dependencies import require_school_admin, get_current_user from app.database import get_db from app.models.user import HubUser, UserRole from app.models.school import School from app.models.license import License from app.models.billing import Invoice from app.models.sms import SmsJob, SmsJobStatus from app.models.ticket import SupportTicket router = APIRouter(prefix="/api/portal", tags=["school-portal"]) async def _get_school(current_user: HubUser, db: AsyncSession) -> School: if not current_user.school_id: raise HTTPException(400, "No school linked to your account") school = (await db.execute(select(School).where(School.id == current_user.school_id))).scalar_one_or_none() if not school: raise HTTPException(404, "School not found") return school @router.get("/overview") async def portal_overview( current_user: HubUser = Depends(require_school_admin), db: AsyncSession = Depends(get_db), ): school = await _get_school(current_user, db) lic = (await db.execute(select(License).where(License.school_id == school.id))).scalar_one_or_none() pending_inv = (await db.execute( select(func.count()).where( and_(Invoice.school_id == school.id, Invoice.status.in_(["sent", "overdue"])) ) )).scalar_one() sms_this_month = (await db.execute( select(func.count()).where( and_(SmsJob.school_id == school.id, SmsJob.status == SmsJobStatus.sent, func.date_trunc("month", SmsJob.sent_at) == func.date_trunc("month", func.current_date())) ) )).scalar_one() open_tickets = (await db.execute( select(func.count()).where( and_(SupportTicket.school_id == school.id, SupportTicket.status.in_(["open", "in_progress"])) ) )).scalar_one() return { "school": {"id": school.id, "name": school.name, "status": school.status.value, "tier": school.tier.value}, "license": { "key": lic.key if lic else None, "status": lic.status.value if lic else None, "expires_at": lic.expires_at.isoformat() if lic and lic.expires_at else None, "last_seen": lic.last_validated_at.isoformat() if lic and lic.last_validated_at else None, }, "sms_credits": float(school.sms_credits), "sms_credit_low_threshold": school.sms_credit_low_threshold, "sms_this_month": sms_this_month, "pending_invoices": pending_inv, "open_tickets": open_tickets, }