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 = {
|
||||
|
||||
@@ -65,3 +65,20 @@ def setup():
|
||||
"user": _store().public_user(updated),
|
||||
"must_setup": False,
|
||||
})
|
||||
|
||||
|
||||
@auth_bp.patch("/profile")
|
||||
@require_auth
|
||||
def profile():
|
||||
"""Self-service profile update: name (and optional email). Any authenticated user."""
|
||||
user = current_user()
|
||||
data = request.get_json(silent=True) or {}
|
||||
username = user.get("username") or user.get("id")
|
||||
try:
|
||||
if "name" in data:
|
||||
_store().set_name(username, data.get("name"))
|
||||
if "email" in data:
|
||||
_store().set_email(username, data.get("email"))
|
||||
except AuthError as exc:
|
||||
raise ApiError(str(exc), 400)
|
||||
return jsonify({"user": _store().public_user(_store().get_user(username))})
|
||||
|
||||
@@ -137,6 +137,12 @@ class UserStore:
|
||||
raise AuthError("a user with this email already exists")
|
||||
return self.users.update(self._norm(username), email=email)
|
||||
|
||||
def set_name(self, username: str, name: str) -> dict[str, Any]:
|
||||
name = (name or "").strip()
|
||||
if not name:
|
||||
raise AuthError("name is required")
|
||||
return self.users.update(self._norm(username), name=name)
|
||||
|
||||
def complete_setup(self, username: str, email: str, new_password: str) -> dict[str, Any]:
|
||||
"""First-time admin setup: set email + password, clear must_setup."""
|
||||
if not new_password or len(new_password) < 4:
|
||||
|
||||
Reference in New Issue
Block a user