"""Admin analytics: aggregate trainee results, with optional date-range filter.""" from __future__ import annotations import datetime from flask import Blueprint, jsonify, request from .helpers import current_user, require_auth, require_roles analytics_bp = Blueprint("analytics", __name__) def _stores(): from flask import current_app return { "sessions": current_app.extensions["session_store"], "groups": current_app.extensions["group_store"], "users": current_app.extensions["user_store"], } def _parse_date(val: str): if not val: return None try: return datetime.datetime.fromisoformat(val) except ValueError: try: return datetime.datetime.strptime(val, "%Y-%m-%d") except ValueError: return None @analytics_bp.get("") @require_auth @require_roles("admin") def analytics(): s = _stores() actor = current_user() # Optional date-range filter (inclusive): ?from=YYYY-MM-DD&to=YYYY-MM-DD d_from = _parse_date((request.args.get("from") or "").strip()) d_to = _parse_date((request.args.get("to") or "").strip()) if d_to: # include the whole "to" day d_to = d_to.replace(hour=23, minute=59, second=59, microsecond=999999) if actor.get("role") == "super_admin": sessions = s["sessions"].sessions.all() users = s["users"].list_users() else: org_id = actor.get("org_id") users = s["users"].list_users(org_id=org_id) user_ids = {u["id"] for u in users} sessions = [ x for x in s["sessions"].sessions.all() if x.get("user_id") in user_ids ] if d_from or d_to: filtered = [] for x in sessions: ts = x.get("created_at") or x.get("finished_at") or "" if not ts: continue try: dt = datetime.datetime.fromisoformat(ts) except ValueError: continue if d_from and dt < d_from: continue if d_to and dt > d_to: continue filtered.append(x) sessions = filtered overall = { "total_sessions": len(sessions), "wins": sum(1 for x in sessions if x.get("outcome") == "won"), "losses": sum(1 for x in sessions if x.get("outcome") == "lost"), } overall["close_rate"] = ( round(overall["wins"] / overall["total_sessions"] * 100, 1) if overall["total_sessions"] else 0 ) # average score scores = [ (x.get("debrief") or {}).get("score", 0) for x in sessions if x.get("outcome") ] overall["avg_score"] = round(sum(scores) / len(scores), 1) if scores else 0 # hardest personas = personas with most losses (lowest avg score) by_persona: dict = {} for x in sessions: key = (x.get("group_id"), x.get("persona_id"), x.get("persona_name", "?")) if key not in by_persona: by_persona[key] = {"plays": 0, "losses": 0, "wins": 0, "scores": []} rec = by_persona[key] rec["plays"] += 1 rec["scores"].append((x.get("debrief") or {}).get("score", 0)) if x.get("outcome") == "won": rec["wins"] += 1 elif x.get("outcome") == "lost": rec["losses"] += 1 hardest = sorted( ( { "persona_name": k[2], "plays": v["plays"], "wins": v["wins"], "losses": v["losses"], "avg_score": round(sum(v["scores"]) / len(v["scores"]), 1) if v["scores"] else 0, } for k, v in by_persona.items() ), key=lambda r: (r["losses"], -r["avg_score"]), )[:10] # per-user summary so the admin overview can show per-trainee results by_user: dict = {} for x in sessions: uid = x.get("user_id", "?") if uid not in by_user: by_user[uid] = {"sessions": 0, "wins": 0, "losses": 0} by_user[uid]["sessions"] += 1 if x.get("outcome") == "won": by_user[uid]["wins"] += 1 elif x.get("outcome") == "lost": by_user[uid]["losses"] += 1 name_by_id = {u.get("id"): u.get("name") or u.get("username") for u in users} per_user = [ { "user_id": uid, "name": name_by_id.get(uid, uid), **v, } for uid, v in by_user.items() ] return jsonify({ "overall": overall, "trainee_count": len(users), "hardest_personas": hardest, "per_user": per_user, })