"""Super admin dashboard summary.""" from fastapi import APIRouter, Depends, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, and_, desc from datetime import date, timedelta from typing import Optional from app.auth.dependencies import require_super_admin from app.database import get_db from app.models.user import HubUser from app.models.school import School, SchoolStatus from app.models.license import License, LicenseStatus from app.models.sms import SmsJob, SmsJobStatus from app.models.billing import Invoice, InvoiceStatus from app.models.ticket import SupportTicket, TicketStatus router = APIRouter(prefix="/api/dashboard", tags=["dashboard"]) @router.get("/summary") async def get_summary( _admin: HubUser = Depends(require_super_admin), db: AsyncSession = Depends(get_db), ): total_schools = (await db.execute(select(func.count()).select_from(School))).scalar_one() active_schools = (await db.execute(select(func.count()).where(School.status == SchoolStatus.active))).scalar_one() suspended_schools = (await db.execute(select(func.count()).where(School.status == SchoolStatus.suspended))).scalar_one() expiring_soon = (await db.execute( select(func.count()).where( and_(License.expires_at != None, License.expires_at <= date.today() + timedelta(days=30), License.status == LicenseStatus.active) ) )).scalar_one() open_tickets = (await db.execute( select(func.count()).where(SupportTicket.status.in_([TicketStatus.open, TicketStatus.in_progress])) )).scalar_one() pending_invoices = (await db.execute( select(func.count()).where(Invoice.status.in_([InvoiceStatus.sent, InvoiceStatus.overdue])) )).scalar_one() sms_today = (await db.execute( select(func.count()).where( and_(func.date(SmsJob.created_at) == date.today(), SmsJob.status == SmsJobStatus.sent) ) )).scalar_one() sms_pending = (await db.execute( select(func.count()).where(SmsJob.status == SmsJobStatus.pending) )).scalar_one() return { "schools": {"total": total_schools, "active": active_schools, "suspended": suspended_schools}, "licenses": {"expiring_soon": expiring_soon}, "tickets": {"open": open_tickets}, "invoices": {"pending": pending_invoices}, "sms": {"sent_today": sms_today, "pending": sms_pending}, } @router.get("/school-health") async def get_school_health( sort_by: Optional[str] = Query("name", regex="^(name|status|sms_credits|license_expires_at|license_last_seen|created_at)$"), sort_dir: Optional[str] = Query("asc", regex="^(asc|desc)$"), status: Optional[SchoolStatus] = Query(None), search: Optional[str] = Query(None), _admin: HubUser = Depends(require_super_admin), db: AsyncSession = Depends(get_db), ): """ School health table for the super admin dashboard. Returns all schools with their license expiry, last seen, and credit status. """ stmt = select(School) if status: stmt = stmt.where(School.status == status) if search: stmt = stmt.where(School.name.ilike(f"%{search}%")) # Apply sort sort_col = { "name": School.name, "status": School.status, "sms_credits": School.sms_credits, "created_at": School.created_at, }.get(sort_by, School.name) stmt = stmt.order_by(desc(sort_col) if sort_dir == "desc" else sort_col) schools = (await db.execute(stmt)).scalars().all() # Fetch licenses in bulk all_lics = { lic.school_id: lic for lic in (await db.execute(select(License))).scalars().all() } items = [] for s in schools: lic = all_lics.get(s.id) # Days until license expires days_left = None if lic and lic.expires_at: days_left = (lic.expires_at - date.today()).days items.append({ "id": s.id, "name": s.name, "slug": s.slug, "status": s.status.value, "tier": s.tier.value, "sms_credits": float(s.sms_credits), "sms_credit_low": float(s.sms_credits) < s.sms_credit_low_threshold, "license_status": lic.status.value if lic else None, "license_expires_at": lic.expires_at.isoformat() if lic and lic.expires_at else None, "license_days_left": days_left, "license_expiring_soon": days_left is not None and 0 < days_left <= 30, "license_last_seen": lic.last_validated_at.isoformat() if lic and lic.last_validated_at else None, "created_at": s.created_at.isoformat(), }) return {"items": items, "total": len(items)} @router.get("/revenue") async def get_revenue_snapshot( _admin: HubUser = Depends(require_super_admin), db: AsyncSession = Depends(get_db), ): """ Revenue snapshot for the super admin dashboard. Returns: MRR (from active subscriptions), outstanding invoices total, overdue total. """ from app.models.billing import SchoolSubscription # MRR: sum of monthly_fee from active subscriptions mrr_result = (await db.execute( select(func.sum(SchoolSubscription.monthly_fee)).where(SchoolSubscription.is_active == True) )).scalar_one() mrr = float(mrr_result or 0) # Outstanding: all sent invoices total outstanding_result = (await db.execute( select(func.sum(Invoice.total_amount)).where(Invoice.status == InvoiceStatus.sent) )).scalar_one() outstanding = float(outstanding_result or 0) # Overdue: all overdue invoices total overdue_result = (await db.execute( select(func.sum(Invoice.total_amount)).where(Invoice.status == InvoiceStatus.overdue) )).scalar_one() overdue = float(overdue_result or 0) # Paid this month paid_result = (await db.execute( select(func.sum(Invoice.total_amount)).where( and_( Invoice.status == InvoiceStatus.paid, func.date_trunc("month", Invoice.paid_at) == func.date_trunc("month", func.current_date()) ) ) )).scalar_one() paid_this_month = float(paid_result or 0) # Invoice counts overdue_count = (await db.execute( select(func.count()).where(Invoice.status == InvoiceStatus.overdue) )).scalar_one() outstanding_count = (await db.execute( select(func.count()).where(Invoice.status == InvoiceStatus.sent) )).scalar_one() return { "mrr": mrr, "outstanding": outstanding, "outstanding_count": outstanding_count, "overdue": overdue, "overdue_count": overdue_count, "paid_this_month": paid_this_month, }