feat(ui): 3-tab UI per spec — admin dashboard + date filter, personal dashboard, training w/ difficulty, settings profile
Tab 1 Admin overview (/): aggregate stats + DATE FILTER (?from&to), hardest personas, admin quick actions (Users / + Add product). Tab 2 My dashboard (/my/board): per-user win/lose/session summary (works for admin too). Tab 3 Training (/training): product list w/ status + persona count; admins see all groups (draft->analyze/edit), trainees see ready->personas; persona list shows DIFFICULTY (1-5 stars); admins get a Manage-personas button. Settings (/settings): profile (name/email) via new PATCH /api/auth/profile + change password + language. Backend: analytics date filter; profile endpoint; /api/me/board + /api/chat/sessions opened to any authed user. Rebuilt frontend/dist.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
"""Admin analytics: aggregate trainee results."""
|
||||
"""Admin analytics: aggregate trainee results (supports date-range filter)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import Blueprint, jsonify
|
||||
import datetime
|
||||
from flask import Blueprint, jsonify, request
|
||||
|
||||
from .helpers import ApiError, current_user, require_auth, require_roles
|
||||
|
||||
@@ -18,22 +19,51 @@ def _stores():
|
||||
}
|
||||
|
||||
|
||||
def _parse_date_iso(value: str | None, *, end: bool = False) -> str | None:
|
||||
"""Parse a YYYY-MM-DD into an ISO datetime bound for created_at filtering."""
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
d = datetime.date.fromisoformat(value.strip())
|
||||
except ValueError:
|
||||
return None
|
||||
if end:
|
||||
# end-of-day bound (inclusive)
|
||||
return datetime.datetime.combine(d, datetime.time(23, 59, 59, 999999), tzinfo=datetime.timezone.utc).isoformat()
|
||||
return datetime.datetime.combine(d, datetime.time(0, 0, 0), tzinfo=datetime.timezone.utc).isoformat()
|
||||
|
||||
|
||||
@analytics_bp.get("")
|
||||
@require_auth
|
||||
@require_roles("admin")
|
||||
def analytics():
|
||||
s = _stores()
|
||||
actor = current_user()
|
||||
|
||||
# Date filter (optional) from ?from=YYYY-MM-DD&to=YYYY-MM-DD on created_at
|
||||
date_from = _parse_date_iso(request.args.get("from"))
|
||||
date_to = _parse_date_iso(request.args.get("to"), end=True)
|
||||
|
||||
def _in_window(sess) -> bool:
|
||||
created = (sess.get("created_at") or "")[:19]
|
||||
if not created:
|
||||
return True
|
||||
if date_from and created < date_from[:19]:
|
||||
return False
|
||||
if date_to and created > date_to[:19]:
|
||||
return False
|
||||
return True
|
||||
|
||||
if actor.get("role") == "super_admin":
|
||||
sessions = s["sessions"].sessions.all()
|
||||
users = s["users"].list_users()
|
||||
sessions = [x for x in s["sessions"].sessions.all() if _in_window(x)]
|
||||
else:
|
||||
org_id = actor.get("org_id")
|
||||
# users in this org
|
||||
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
|
||||
x for x in s["sessions"].sessions.all()
|
||||
if x.get("user_id") in user_ids and _in_window(x)
|
||||
]
|
||||
|
||||
overall = {
|
||||
|
||||
Reference in New Issue
Block a user