feat(i18n + export + guide): locale-aware scenarios/debrief; CSV export; how-to-use page

Backend:
- SCENARIOS localized (th/en): scenario preamble + persona tone adaptation follow the
  trainee's locale; start accepts {locale} and stores it on the session; send/auto-finish
  use it so debrief text (why/coaching/turning points) is in the active language.
- /api/analytics/export returns per-trainee finished-session CSV (admin).

Frontend:
- Scenario picker labels localize by i18n.locale.
- New /guide page (non-IT how-to), linked from Training.
- Analytics: date-filter bar uses line icon + 'Download CSV' button (fetch w/ auth).
Rebuilt dist.
This commit is contained in:
Macky
2026-08-08 15:06:48 +07:00
parent aa2eb8dd37
commit 15c60ae400
40 changed files with 302 additions and 107 deletions

View File

@@ -111,3 +111,63 @@ def analytics():
"trainee_count": len(users),
"hardest_personas": hardest,
})
@analytics_bp.get("/export")
@require_auth
@require_roles("admin")
def export_csv():
"""Export per-trainee finished session results as CSV (for HR/offline review)."""
import csv
import io
from flask import Response, current_app
s = _stores()
users = {}
try:
user_store = current_app.extensions.get("user_store")
if user_store and hasattr(user_store, "list_users"):
for rec in user_store.list_users():
users[rec.get("id") or rec.get("username")] = rec.get("username") or rec.get("id")
except Exception:
pass
sessions = []
try:
grp_store = s["groups"]
sess_store = s["sessions"]
if hasattr(grp_store, "groups"):
group_ids = [g.get("id") for g in grp_store.groups.all()]
elif hasattr(grp_store, "all"):
group_ids = [g.get("id") for g in grp_store.all()]
else:
group_ids = []
for gid in group_ids:
try:
ss = sess_store.sessions.where(
lambda r, _gid=gid: r.get("group_id") == _gid and r.get("outcome")
)
sessions.extend(ss)
except Exception:
pass
except Exception:
pass
buf = io.StringIO()
w = csv.writer(buf)
w.writerow(["username", "persona", "scenario", "outcome", "score", "created_at"])
for sess in sessions:
w.writerow([
users.get(sess.get("user_id"), sess.get("user_id", "")),
sess.get("persona_name", ""),
sess.get("scenario", ""),
sess.get("outcome", ""),
(sess.get("debrief") or {}).get("score", ""),
sess.get("created_at", ""),
])
return Response(
buf.getvalue(),
mimetype="text/csv",
headers={"Content-Disposition": "attachment; filename=sales-trainer-results.csv"},
)