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:
Macky
2026-08-07 22:35:49 +07:00
parent ac35be8906
commit b05907ae62
40 changed files with 319 additions and 88 deletions

View File

@@ -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 = {

View File

@@ -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))})

View File

@@ -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:

View File

@@ -1 +1 @@
import{k as x,c as r,a,t,u,i as d,w as n,v as m,B as g,e as _,F as U,p as k,l as f,r as p,o as i,s as V}from"./index-EpRXsI-R.js";const b={class:"card",style:{"margin-bottom":"16px"}},B={class:"row"},C={key:0,class:"error"},h={style:{flex:"1"}},M={class:"badge"},E={__name:"AdminUsers",setup(N){const v=p([]),o=p(""),s=p({name:"",email:"",password:"",role:"user"});async function c(){v.value=(await f.adminListUsers()).users}async function w(){o.value="";try{await f.adminCreateUser({...s.value}),s.value={name:"",email:"",password:"",role:"user"},await c()}catch(y){o.value=y.message}}return x(c),(y,l)=>(i(),r("div",null,[a("h2",null,t(u(d).t("users")),1),a("div",b,[a("h4",null,"+ "+t(u(d).t("create"))+" user",1),a("div",B,[n(a("input",{"onUpdate:modelValue":l[0]||(l[0]=e=>s.value.name=e),placeholder:"Name",style:{flex:"1"}},null,512),[[m,s.value.name]]),n(a("input",{"onUpdate:modelValue":l[1]||(l[1]=e=>s.value.email=e),placeholder:"Email",style:{flex:"1"}},null,512),[[m,s.value.email]]),n(a("input",{"onUpdate:modelValue":l[2]||(l[2]=e=>s.value.password=e),type:"password",placeholder:"Temp password",style:{flex:"1"}},null,512),[[m,s.value.password]]),n(a("select",{"onUpdate:modelValue":l[3]||(l[3]=e=>s.value.role=e),style:{flex:"1"}},[...l[4]||(l[4]=[a("option",{value:"user"},"user",-1),a("option",{value:"admin"},"admin",-1)])],512),[[g,s.value.role]]),a("button",{class:"primary",onClick:w},t(u(d).t("create")),1)]),o.value?(i(),r("div",C,t(o.value),1)):_("",!0)]),(i(!0),r(U,null,k(v.value,e=>(i(),r("div",{class:"card",key:e.id,style:{"margin-bottom":"8px",display:"flex","align-items":"center",gap:"12px"}},[a("strong",h,t(e.name)+" ("+t(e.email)+")",1),a("span",M,t(e.role),1),a("span",{class:V(["badge",e.active?"won":"lost"])},t(e.active?"active":"inactive"),3)]))),128))]))}};export{E as default};
import{k as x,c as r,a,t,u,i as d,w as n,v as m,B as g,e as _,F as U,n as k,p as f,r as p,o as i,y as V}from"./index-Hx407ukZ.js";const b={class:"card",style:{"margin-bottom":"16px"}},B={class:"row"},C={key:0,class:"error"},h={style:{flex:"1"}},M={class:"badge"},E={__name:"AdminUsers",setup(N){const v=p([]),o=p(""),s=p({name:"",email:"",password:"",role:"user"});async function c(){v.value=(await f.adminListUsers()).users}async function w(){o.value="";try{await f.adminCreateUser({...s.value}),s.value={name:"",email:"",password:"",role:"user"},await c()}catch(y){o.value=y.message}}return x(c),(y,l)=>(i(),r("div",null,[a("h2",null,t(u(d).t("users")),1),a("div",b,[a("h4",null,"+ "+t(u(d).t("create"))+" user",1),a("div",B,[n(a("input",{"onUpdate:modelValue":l[0]||(l[0]=e=>s.value.name=e),placeholder:"Name",style:{flex:"1"}},null,512),[[m,s.value.name]]),n(a("input",{"onUpdate:modelValue":l[1]||(l[1]=e=>s.value.email=e),placeholder:"Email",style:{flex:"1"}},null,512),[[m,s.value.email]]),n(a("input",{"onUpdate:modelValue":l[2]||(l[2]=e=>s.value.password=e),type:"password",placeholder:"Temp password",style:{flex:"1"}},null,512),[[m,s.value.password]]),n(a("select",{"onUpdate:modelValue":l[3]||(l[3]=e=>s.value.role=e),style:{flex:"1"}},[...l[4]||(l[4]=[a("option",{value:"user"},"user",-1),a("option",{value:"admin"},"admin",-1)])],512),[[g,s.value.role]]),a("button",{class:"primary",onClick:w},t(u(d).t("create")),1)]),o.value?(i(),r("div",C,t(o.value),1)):_("",!0)]),(i(!0),r(U,null,k(v.value,e=>(i(),r("div",{class:"card",key:e.id,style:{"margin-bottom":"8px",display:"flex","align-items":"center",gap:"12px"}},[a("strong",h,t(e.name)+" ("+t(e.email)+")",1),a("span",M,t(e.role),1),a("span",{class:V(["badge",e.active?"won":"lost"])},t(e.active?"active":"inactive"),3)]))),128))]))}};export{E as default};

View File

@@ -0,0 +1 @@
.stat[data-v-c411a8cc]{text-align:center;min-width:110px}.stat div[data-v-c411a8cc]{color:var(--muted);font-size:12px}.stat strong[data-v-c411a8cc]{font-size:20px}.filter-bar[data-v-c411a8cc]{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.filter-bar input[type=date][data-v-c411a8cc]{width:auto}.grid[data-v-c411a8cc]{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:14px}.tier-card[data-v-c411a8cc]{display:flex;flex-direction:column}.badge.won[data-v-c411a8cc]{background:#dcfce7;color:#166534}.badge.lost[data-v-c411a8cc]{background:#fee2e2;color:#991b1b}.badge.not_tried[data-v-c411a8cc]{background:#eef2ff;color:#4338ca}

View File

@@ -1 +0,0 @@
import{_ as i,k as d,l as v,c as o,a as s,t,u as _,i as u,F as c,p as g,r as p,o as n}from"./index-EpRXsI-R.js";const m={class:"row",style:{gap:"16px",margin:"16px 0"}},y={class:"card stat"},f={class:"card stat"},w={style:{color:"var(--green)"}},x={class:"card stat"},h={style:{color:"var(--red)"}},k={class:"card stat"},b={class:"card stat"},A={class:"badge lost"},B={class:"badge won"},L={class:"muted"},F={__name:"Analytics",setup(S){const l=p({overall:{total_sessions:0,wins:0,losses:0,close_rate:0,avg_score:0},trainee_count:0,hardest_personas:[]});return d(async()=>{l.value=await v.analytics()}),(W,a)=>(n(),o("div",null,[s("h2",null,t(_(u).t("analytics")),1),s("div",m,[s("div",y,[a[0]||(a[0]=s("div",null,"Sessions",-1)),s("strong",null,t(l.value.overall.total_sessions),1)]),s("div",f,[a[1]||(a[1]=s("div",null,"Wins",-1)),s("strong",w,t(l.value.overall.wins),1)]),s("div",x,[a[2]||(a[2]=s("div",null,"Losses",-1)),s("strong",h,t(l.value.overall.losses),1)]),s("div",k,[a[3]||(a[3]=s("div",null,"Close rate",-1)),s("strong",null,t(l.value.overall.close_rate)+"%",1)]),s("div",b,[a[4]||(a[4]=s("div",null,"Avg score",-1)),s("strong",null,t(l.value.overall.avg_score),1)])]),s("h3",null,"Trainees: "+t(l.value.trainee_count),1),a[5]||(a[5]=s("h3",null,"Hardest personas",-1)),(n(!0),o(c,null,g(l.value.hardest_personas,(e,r)=>(n(),o("div",{class:"card",key:r,style:{"margin-bottom":"8px"}},[s("strong",null,t(e.persona_name),1),s("span",A,t(e.losses)+"L",1),s("span",B,t(e.wins)+"W",1),s("span",L,"· avg "+t(e.avg_score),1)]))),128))]))}},D=i(F,[["__scopeId","data-v-8828276e"]]);export{D as default};

View File

@@ -1 +0,0 @@
.stat[data-v-8828276e]{text-align:center;min-width:110px}.stat div[data-v-8828276e]{color:var(--muted);font-size:12px}.stat strong[data-v-8828276e]{font-size:20px}

View File

@@ -0,0 +1 @@
import{_ as x,k,c as u,a as s,t as e,u as l,i as n,l as m,m as y,w as f,v as h,e as C,F as N,n as V,p as A,q as B,r as c,o as _}from"./index-Hx407ukZ.js";const L={class:"row",style:{"align-items":"center","margin-bottom":"16px"}},D={style:{margin:"0"}},F={class:"row",style:{"margin-left":"auto",gap:"10px"}},M={class:"soft"},P={class:"primary"},S={class:"card filter-bar"},T={class:"muted"},U=["disabled"],W=["disabled"],j={class:"row",style:{gap:"16px",margin:"16px 0"}},q={class:"card stat"},E={class:"card stat"},I={style:{color:"var(--green)"}},O={class:"card stat"},R={style:{color:"var(--red)"}},z={class:"card stat"},G={class:"card stat"},H={class:"card stat"},J={key:0,class:"card empty-state"},K={class:"grid"},Q={class:"row",style:{"justify-content":"space-between"}},X={class:"muted"},Y={class:"row",style:{"margin-top":"10px",gap:"8px"}},Z={class:"badge won"},$={class:"badge lost"},ss={class:"badge not_tried"},ts={class:"muted",style:{"margin-top":"8px"}},es={__name:"Analytics",setup(as){const a=c({overall:{total_sessions:0,wins:0,losses:0,close_rate:0,avg_score:0},trainee_count:0,hardest_personas:[]}),i=c(""),r=c(""),d=c(!1);async function v(){d.value=!0;try{a.value=await A.analytics({from:i.value,to:r.value})}finally{d.value=!1}}function w(){v()}function b(){i.value="",r.value="",v()}return k(v),(os,t)=>{const p=B("router-link");return _(),u("div",null,[s("div",L,[s("h2",D,"📊 "+e(l(n).t("adminOverview")),1),s("div",F,[m(p,{to:"/admin/users"},{default:y(()=>[s("button",M,"👥 "+e(l(n).t("users")),1)]),_:1}),m(p,{to:"/admin/new-group"},{default:y(()=>[s("button",P," "+e(l(n).t("addProduct")),1)]),_:1})])]),s("div",S,[s("span",T,"🗓 "+e(l(n).t("dateRange")),1),f(s("input",{type:"date","onUpdate:modelValue":t[0]||(t[0]=o=>i.value=o)},null,512),[[h,i.value]]),t[2]||(t[2]=s("span",null,"",-1)),f(s("input",{type:"date","onUpdate:modelValue":t[1]||(t[1]=o=>r.value=o)},null,512),[[h,r.value]]),s("button",{class:"primary",onClick:w,disabled:d.value},"→ "+e(l(n).t("apply")),9,U),s("button",{onClick:b,disabled:d.value},e(l(n).t("clear")),9,W)]),s("div",j,[s("div",q,[t[3]||(t[3]=s("div",null,"Sessions",-1)),s("strong",null,e(a.value.overall.total_sessions),1)]),s("div",E,[t[4]||(t[4]=s("div",null,"Wins",-1)),s("strong",I,e(a.value.overall.wins),1)]),s("div",O,[t[5]||(t[5]=s("div",null,"Losses",-1)),s("strong",R,e(a.value.overall.losses),1)]),s("div",z,[t[6]||(t[6]=s("div",null,"Close rate",-1)),s("strong",null,e(a.value.overall.close_rate)+"%",1)]),s("div",G,[t[7]||(t[7]=s("div",null,"Avg score",-1)),s("strong",null,e(a.value.overall.avg_score),1)]),s("div",H,[t[8]||(t[8]=s("div",null,"Trainees",-1)),s("strong",null,e(a.value.trainee_count),1)])]),s("h3",null,e(l(n).t("hardestPersonas")),1),a.value.hardest_personas.length===0?(_(),u("div",J,[...t[9]||(t[9]=[s("strong",null,"No data",-1),s("span",null,"No sessions in this date range.",-1)])])):C("",!0),s("div",K,[(_(!0),u(N,null,V(a.value.hardest_personas,(o,g)=>(_(),u("div",{key:g,class:"card lift tier-card"},[s("div",Q,[s("strong",null,e(o.persona_name),1),s("span",X,"#"+e(g+1),1)]),s("div",Y,[s("span",Z,e(o.wins)+"W",1),s("span",$,e(o.losses)+"L",1),s("span",ss,e(o.plays)+" plays",1)]),s("div",ts,e(l(n).t("score"))+": "+e(o.avg_score),1)]))),128))])])}}},ns=x(es,[["__scopeId","data-v-c411a8cc"]]);export{ns as default};

View File

@@ -1 +1 @@
import{_ as M,k as q,l as k,c as t,m as B,n as D,u as l,a as s,t as e,s as C,e as i,j as m,i as n,F,p as I,w as z,v as A,b as E,h as H,r as c,A as J,q as L,o}from"./index-EpRXsI-R.js";const O={class:"row",style:{"align-items":"center","margin-bottom":"12px"}},P={style:{margin:"0"}},R={key:0,class:"muted"},U=["disabled"],$={key:0,class:"spinner",style:{"margin-right":"4px"}},G={key:0,class:"card task"},Q={key:0},W={key:0,class:"bubble msg-customer muted"},X={key:2,class:"composer"},Y=["disabled","placeholder"],Z=["disabled"],ee={key:3,class:"card debrief"},se={key:0},ae={class:"json"},te={class:"primary",style:{"margin-top":"12px"}},le={__name:"Chat",setup(ne){const N=H(),_=N.params.gid,g=N.params.pid,r=c(null),f=c(!1),v=c([]),p=c(""),d=c(!1),u=c(null),w=c(""),j=c(null);function T(){J(()=>{y.value&&(y.value.scrollTop=y.value.scrollHeight)})}const y=c(null);q(async()=>{r.value=(await k.getPersona(_,g)).persona;const a=await k.chatStart(_,g);j.value=a.session.id,a.session.task&&(w.value=a.session.task),v.value=a.session.messages||[],f.value=!0,v.value.length&&T()});async function V(){if(p.value.trim()){d.value=!0;try{const a=await k.chatSend(_,g,p.value.trim());v.value=a.messages,p.value="",T()}catch(a){alert(a.message)}finally{d.value=!1}}}async function K(){if(confirm(n.t("finish")+"?")){d.value=!0;try{const a=await k.chatFinish(_,g);u.value=a.debrief,v.value=a.session.messages}catch(a){alert(a.message)}finally{d.value=!1}}}return(a,b)=>{const S=L("router-link");return o(),t("div",null,[B(S,{to:`/groups/${l(_)}/personas`,class:"btn-back"},{default:D(()=>[m("← "+e(l(n).t("personas")),1)]),_:1},8,["to"]),s("div",O,[s("h2",P,e(r.value?r.value.name:"..."),1),s("span",{class:C(["badge",r.value&&r.value.channel])},e(r.value?r.value.channel:""),3),r.value?(o(),t("span",R,e(r.value.profession)+" · "+e(r.value.age_group),1)):i("",!0),s("button",{class:"danger",style:{"margin-left":"auto"},onClick:K,disabled:v.value.length===0||!!u.value},[d.value?(o(),t("span",$)):i("",!0),m(e(l(n).t("finish")),1)],8,U)]),f.value?i("",!0):(o(),t("div",G,[s("strong",null,"📣 "+e(l(n).t("sellerInitiated")),1),w.value?(o(),t("div",Q,e(w.value),1)):i("",!0)])),f.value?(o(),t("div",{key:1,class:"thread",ref_key:"thread",ref:y},[(o(!0),t(F,null,I(v.value,(h,x)=>(o(),t("div",{key:x,class:C(["bubble",h.role==="seller"?"msg-seller":"msg-customer"])},e(h.text),3))),128)),d.value?(o(),t("div",W,"...")):i("",!0)],512)):i("",!0),f.value&&!u.value?(o(),t("div",X,[z(s("input",{"onUpdate:modelValue":b[0]||(b[0]=h=>p.value=h),onKeyup:E(V,["enter"]),disabled:d.value,placeholder:l(n).t("send")},null,40,Y),[[A,p.value]]),s("button",{class:"primary",onClick:V,disabled:d.value||!p.value.trim()},e(l(n).t("send")),9,Z)])):i("",!0),u.value?(o(),t("div",ee,[s("h3",null,e(l(n).t("debrief")),1),s("p",null,[s("span",{class:C(["badge",u.value.outcome])},e(u.value.outcome==="won"?l(n).t("won"):l(n).t("lost")),3),m(" — "+e(l(n).t("score"))+": ",1),s("strong",null,e(u.value.score),1)]),s("p",null,[s("strong",null,e(l(n).t("pain"))+":",1),m(" "+e(u.value.pain||"—"),1)]),s("p",null,[s("strong",null,e(l(n).t("why"))+":",1),m(" "+e(u.value.why),1)]),u.value.coaching&&u.value.coaching.length?(o(),t("div",se,[b[1]||(b[1]=s("strong",null,"Coaching:",-1)),s("ul",null,[(o(!0),t(F,null,I(u.value.coaching,(h,x)=>(o(),t("li",{key:x},e(h),1))),128))])])):i("",!0),s("details",null,[s("summary",null,e(l(n).t("reveal")),1),s("pre",ae,e(JSON.stringify(u.value.revealed_persona,null,2)),1)]),B(S,{to:"/"},{default:D(()=>[s("button",te,e(l(n).t("dashboard")),1)]),_:1})])):i("",!0)])}}},ue=M(le,[["__scopeId","data-v-4f54526a"]]);export{ue as default};
import{_ as M,k as q,p as k,c as t,l as B,m as D,u as l,a as s,t as e,y as C,e as i,j as m,i as n,F,n as I,w as z,v as A,b as E,h as H,r as c,A as J,q as L,o}from"./index-Hx407ukZ.js";const O={class:"row",style:{"align-items":"center","margin-bottom":"12px"}},P={style:{margin:"0"}},R={key:0,class:"muted"},U=["disabled"],$={key:0,class:"spinner",style:{"margin-right":"4px"}},G={key:0,class:"card task"},Q={key:0},W={key:0,class:"bubble msg-customer muted"},X={key:2,class:"composer"},Y=["disabled","placeholder"],Z=["disabled"],ee={key:3,class:"card debrief"},se={key:0},ae={class:"json"},te={class:"primary",style:{"margin-top":"12px"}},le={__name:"Chat",setup(ne){const N=H(),_=N.params.gid,g=N.params.pid,r=c(null),f=c(!1),v=c([]),p=c(""),d=c(!1),u=c(null),w=c(""),j=c(null);function T(){J(()=>{y.value&&(y.value.scrollTop=y.value.scrollHeight)})}const y=c(null);q(async()=>{r.value=(await k.getPersona(_,g)).persona;const a=await k.chatStart(_,g);j.value=a.session.id,a.session.task&&(w.value=a.session.task),v.value=a.session.messages||[],f.value=!0,v.value.length&&T()});async function V(){if(p.value.trim()){d.value=!0;try{const a=await k.chatSend(_,g,p.value.trim());v.value=a.messages,p.value="",T()}catch(a){alert(a.message)}finally{d.value=!1}}}async function K(){if(confirm(n.t("finish")+"?")){d.value=!0;try{const a=await k.chatFinish(_,g);u.value=a.debrief,v.value=a.session.messages}catch(a){alert(a.message)}finally{d.value=!1}}}return(a,b)=>{const S=L("router-link");return o(),t("div",null,[B(S,{to:`/groups/${l(_)}/personas`,class:"btn-back"},{default:D(()=>[m("← "+e(l(n).t("personas")),1)]),_:1},8,["to"]),s("div",O,[s("h2",P,e(r.value?r.value.name:"..."),1),s("span",{class:C(["badge",r.value&&r.value.channel])},e(r.value?r.value.channel:""),3),r.value?(o(),t("span",R,e(r.value.profession)+" · "+e(r.value.age_group),1)):i("",!0),s("button",{class:"danger",style:{"margin-left":"auto"},onClick:K,disabled:v.value.length===0||!!u.value},[d.value?(o(),t("span",$)):i("",!0),m(e(l(n).t("finish")),1)],8,U)]),f.value?i("",!0):(o(),t("div",G,[s("strong",null,"📣 "+e(l(n).t("sellerInitiated")),1),w.value?(o(),t("div",Q,e(w.value),1)):i("",!0)])),f.value?(o(),t("div",{key:1,class:"thread",ref_key:"thread",ref:y},[(o(!0),t(F,null,I(v.value,(h,x)=>(o(),t("div",{key:x,class:C(["bubble",h.role==="seller"?"msg-seller":"msg-customer"])},e(h.text),3))),128)),d.value?(o(),t("div",W,"...")):i("",!0)],512)):i("",!0),f.value&&!u.value?(o(),t("div",X,[z(s("input",{"onUpdate:modelValue":b[0]||(b[0]=h=>p.value=h),onKeyup:E(V,["enter"]),disabled:d.value,placeholder:l(n).t("send")},null,40,Y),[[A,p.value]]),s("button",{class:"primary",onClick:V,disabled:d.value||!p.value.trim()},e(l(n).t("send")),9,Z)])):i("",!0),u.value?(o(),t("div",ee,[s("h3",null,e(l(n).t("debrief")),1),s("p",null,[s("span",{class:C(["badge",u.value.outcome])},e(u.value.outcome==="won"?l(n).t("won"):l(n).t("lost")),3),m(" — "+e(l(n).t("score"))+": ",1),s("strong",null,e(u.value.score),1)]),s("p",null,[s("strong",null,e(l(n).t("pain"))+":",1),m(" "+e(u.value.pain||"—"),1)]),s("p",null,[s("strong",null,e(l(n).t("why"))+":",1),m(" "+e(u.value.why),1)]),u.value.coaching&&u.value.coaching.length?(o(),t("div",se,[b[1]||(b[1]=s("strong",null,"Coaching:",-1)),s("ul",null,[(o(!0),t(F,null,I(u.value.coaching,(h,x)=>(o(),t("li",{key:x},e(h),1))),128))])])):i("",!0),s("details",null,[s("summary",null,e(l(n).t("reveal")),1),s("pre",ae,e(JSON.stringify(u.value.revealed_persona,null,2)),1)]),B(S,{to:"/"},{default:D(()=>[s("button",te,e(l(n).t("dashboard")),1)]),_:1})])):i("",!0)])}}},ue=M(le,[["__scopeId","data-v-4f54526a"]]);export{ue as default};

View File

@@ -1 +0,0 @@
.grid[data-v-559fb44c]{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:16px}.group-card[data-v-559fb44c]{display:flex;flex-direction:column}.group-card a[data-v-559fb44c]{margin-top:auto}.link-card[data-v-559fb44c]{text-align:center;min-width:150px}

View File

@@ -1 +0,0 @@
import{_ as k,k as v,l as g,c as r,a as t,t as e,u as s,i as a,f as d,m as c,n as l,e as u,F as f,p as x,r as y,q as b,o,s as w,x as h}from"./index-EpRXsI-R.js";const A={class:"row",style:{"margin-bottom":"16px"}},B={style:{margin:"0"}},N={key:0,style:{"margin-left":"auto"}},C={class:"primary"},D={key:0,class:"row",style:{gap:"16px","margin-bottom":"20px"}},V={class:"card link-card"},F={class:"card link-card"},P={key:1,class:"row",style:{gap:"16px","margin-bottom":"20px"}},$={class:"card link-card"},j={class:"card link-card"},q={class:"card link-card"},z={key:2,class:"card",style:{"min-height":"120px"}},E={key:3,class:"card empty-state"},G={key:0},I={key:1},L={class:"grid"},M={class:"row",style:{"justify-content":"space-between"}},S={class:"muted",style:{margin:"6px 0 12px"}},T={class:"primary"},H={__name:"Dashboard",setup(J){const _=y([]),p=y(!0);return v(async()=>{try{_.value=(await g.listGroups()).groups}finally{p.value=!1}}),(K,m)=>{const i=b("router-link");return o(),r("div",null,[t("div",A,[t("h2",B,e(s(a).t("dashboard")),1),s(d).isAdmin?(o(),r("div",N,[c(i,{to:"/admin/new-group"},{default:l(()=>[t("button",C,e(s(a).t("groupBuilder"))+" +",1)]),_:1})])):u("",!0)]),s(d).isAdmin?(o(),r("div",D,[c(i,{to:"/admin/users",style:{"text-decoration":"none"}},{default:l(()=>[t("div",V,"👥 "+e(s(a).t("users")),1)]),_:1}),c(i,{to:"/admin/analytics",style:{"text-decoration":"none"}},{default:l(()=>[t("div",F,"📊 "+e(s(a).t("analytics")),1)]),_:1})])):u("",!0),s(d).role==="user"?(o(),r("div",P,[c(i,{to:"/my/sessions",style:{"text-decoration":"none"}},{default:l(()=>[t("div",$,"🎯 "+e(s(a).t("myTraining")),1)]),_:1}),c(i,{to:"/my/weak-areas",style:{"text-decoration":"none"}},{default:l(()=>[t("div",j,"⚠️ "+e(s(a).t("weakAreas")),1)]),_:1}),c(i,{to:"/my/generate",style:{"text-decoration":"none"}},{default:l(()=>[t("div",q,"✨ "+e(s(a).t("generatePersona")),1)]),_:1})])):u("",!0),t("h3",null,e(s(a).t("groups")),1),p.value?(o(),r("div",z,[...m[0]||(m[0]=[t("div",{class:"skeleton",style:{height:"60px"}},null,-1),t("div",{class:"skeleton",style:{height:"60px","margin-top":"10px"}},null,-1)])])):_.value.length===0?(o(),r("div",E,[t("strong",null,e(s(d).isAdmin?"No persona groups yet":"No groups available"),1),s(d).isAdmin?(o(),r("span",G,e(s(a).t("groupBuilder"))+" to start.",1)):(o(),r("span",I,"Ask an admin to create a group."))])):u("",!0),t("div",L,[(o(!0),r(f,null,x(_.value,n=>(o(),r("div",{key:n.id,class:"card group-card lift"},[t("div",M,[t("strong",null,e(n.title),1),t("span",{class:w(["badge",n.status])},e(n.status),3)]),t("div",S,e(n.sales_kit&&n.sales_kit.productName||n.input&&n.input.product||""),1),s(d).isAdmin?(o(),h(i,{key:0,to:`/admin/groups/${n.id}/edit`},{default:l(()=>[t("button",null,e(s(a).t("personas"))+" / "+e(s(a).t("create")),1)]),_:1},8,["to"])):n.status==="ready"?(o(),h(i,{key:1,to:`/groups/${n.id}/personas`},{default:l(()=>[t("button",T,e(s(a).t("selectPersona")),1)]),_:1},8,["to"])):u("",!0)]))),128))])])}}},Q=k(H,[["__scopeId","data-v-559fb44c"]]);export{Q as default};

View File

@@ -1 +1 @@
import{c as g,a as e,t as l,u as o,i as n,w as r,v,B as m,e as x,r as p,g as k,o as f,l as w}from"./index-EpRXsI-R.js";const V={class:"card"},B={class:"row"},U={style:{flex:"1"}},C={value:"facebook"},D={value:"line"},F={style:{flex:"1"}},S={value:"th"},E={value:"en"},G={key:0,class:"error"},M=["disabled"],O={__name:"GroupBuilder",setup(N){const h=k(),t=p({product:"",segment:"",description:"",channel:"facebook",language:"th"}),c=p([]),d=p(""),i=p(!1);function _(a){c.value=Array.from(a.target.files||[])}async function b(){i.value=!0,d.value="";try{const a=new FormData;a.append("product",t.value.product),a.append("segment",t.value.segment),a.append("description",t.value.description),a.append("channel",t.value.channel),a.append("language",t.value.language),c.value.forEach(y=>a.append("files",y));const u=(await w.createGroup(a)).group.id;h.push(`/admin/groups/${u}/edit`)}catch(a){d.value=a.message}finally{i.value=!1}}return(a,s)=>(f(),g("div",V,[e("h2",null,l(o(n).t("groupBuilder")),1),e("label",null,l(o(n).t("product")),1),r(e("textarea",{"onUpdate:modelValue":s[0]||(s[0]=u=>t.value.product=u),rows:"3",placeholder:"e.g. Cloud POS for small restaurants"},null,512),[[v,t.value.product]]),e("label",null,l(o(n).t("segment")),1),r(e("input",{"onUpdate:modelValue":s[1]||(s[1]=u=>t.value.segment=u)},null,512),[[v,t.value.segment]]),e("label",null,l(o(n).t("description")),1),r(e("textarea",{"onUpdate:modelValue":s[2]||(s[2]=u=>t.value.description=u),rows:"3"},null,512),[[v,t.value.description]]),e("div",B,[e("div",U,[e("label",null,l(o(n).t("channel")),1),r(e("select",{"onUpdate:modelValue":s[3]||(s[3]=u=>t.value.channel=u)},[e("option",C,l(o(n).t("facebook")),1),e("option",D,l(o(n).t("line")),1)],512),[[m,t.value.channel]])]),e("div",F,[e("label",null,l(o(n).t("language")),1),r(e("select",{"onUpdate:modelValue":s[4]||(s[4]=u=>t.value.language=u)},[e("option",S,l(o(n).t("thai")),1),e("option",E,l(o(n).t("english")),1)],512),[[m,t.value.language]])])]),e("label",null,"📎 Files (.pdf/.md/.txt) — "+l(o(n).t("product"))+" can come from here",1),e("input",{type:"file",multiple:"",accept:".pdf,.md,.txt",onChange:_},null,32),d.value?(f(),g("div",G,l(d.value),1)):x("",!0),e("button",{class:"primary",style:{"margin-top":"16px"},disabled:i.value||!t.value.product&&!c.value.length,onClick:b},l(i.value?"...":o(n).t("create")),9,M)]))}};export{O as default};
import{c as g,a as e,t as l,u as o,i as n,w as r,v,B as m,e as x,r as p,g as k,o as f,p as w}from"./index-Hx407ukZ.js";const V={class:"card"},B={class:"row"},U={style:{flex:"1"}},C={value:"facebook"},D={value:"line"},F={style:{flex:"1"}},S={value:"th"},E={value:"en"},G={key:0,class:"error"},M=["disabled"],O={__name:"GroupBuilder",setup(N){const h=k(),t=p({product:"",segment:"",description:"",channel:"facebook",language:"th"}),c=p([]),d=p(""),i=p(!1);function _(a){c.value=Array.from(a.target.files||[])}async function b(){i.value=!0,d.value="";try{const a=new FormData;a.append("product",t.value.product),a.append("segment",t.value.segment),a.append("description",t.value.description),a.append("channel",t.value.channel),a.append("language",t.value.language),c.value.forEach(y=>a.append("files",y));const u=(await w.createGroup(a)).group.id;h.push(`/admin/groups/${u}/edit`)}catch(a){d.value=a.message}finally{i.value=!1}}return(a,s)=>(f(),g("div",V,[e("h2",null,l(o(n).t("groupBuilder")),1),e("label",null,l(o(n).t("product")),1),r(e("textarea",{"onUpdate:modelValue":s[0]||(s[0]=u=>t.value.product=u),rows:"3",placeholder:"e.g. Cloud POS for small restaurants"},null,512),[[v,t.value.product]]),e("label",null,l(o(n).t("segment")),1),r(e("input",{"onUpdate:modelValue":s[1]||(s[1]=u=>t.value.segment=u)},null,512),[[v,t.value.segment]]),e("label",null,l(o(n).t("description")),1),r(e("textarea",{"onUpdate:modelValue":s[2]||(s[2]=u=>t.value.description=u),rows:"3"},null,512),[[v,t.value.description]]),e("div",B,[e("div",U,[e("label",null,l(o(n).t("channel")),1),r(e("select",{"onUpdate:modelValue":s[3]||(s[3]=u=>t.value.channel=u)},[e("option",C,l(o(n).t("facebook")),1),e("option",D,l(o(n).t("line")),1)],512),[[m,t.value.channel]])]),e("div",F,[e("label",null,l(o(n).t("language")),1),r(e("select",{"onUpdate:modelValue":s[4]||(s[4]=u=>t.value.language=u)},[e("option",S,l(o(n).t("thai")),1),e("option",E,l(o(n).t("english")),1)],512),[[m,t.value.language]])])]),e("label",null,"📎 Files (.pdf/.md/.txt) — "+l(o(n).t("product"))+" can come from here",1),e("input",{type:"file",multiple:"",accept:".pdf,.md,.txt",onChange:_},null,32),d.value?(f(),g("div",G,l(d.value),1)):x("",!0),e("button",{class:"primary",style:{"margin-top":"16px"},disabled:i.value||!t.value.product&&!c.value.length,onClick:b},l(i.value?"...":o(n).t("create")),9,M)]))}};export{O as default};

View File

@@ -1 +1 @@
import{_ as S,k as E,c as n,m as J,n as O,a as t,t as e,u as d,i as l,e as m,j as k,F as b,p as x,h as z,l as y,q as G,r as v,o}from"./index-EpRXsI-R.js";const V={class:"row",style:{"align-items":"center","margin-bottom":"16px"}},j={style:{margin:"0"}},A=["disabled"],P={key:0,class:"spinner",style:{"margin-right":"4px"}},F={key:0,class:"error"},I={key:1,class:"card empty-state"},L={class:"grid"},T={class:"row"},q={key:0,class:"badge"},D={class:"muted"},M={class:"muted",style:{"margin-top":"4px"}},R={style:{"margin-top":"8px"},open:""},$={class:"json"},H=["onClick"],K={__name:"GroupEdit",setup(Q){const _=z().params.gid,g=v(null),p=v([]),i=v(!1),u=v("");async function f(){const s=await y.getGroup(_);g.value=s.group,p.value=(await y.listPersonas(_)).personas}function N(s){return p.value.filter(r=>r.tier===s)}function w(s){return l.t(s==="A"?"tierA":s==="B"?"tierB":"tierC")}async function C(){i.value=!0,u.value="";try{const s=await y.analyzeGroup(_);p.value=s.personas,await f()}catch(s){u.value=s.message}finally{i.value=!1}}function B(s){const r=prompt("Edit persona JSON (full fields):",JSON.stringify(s,null,2));if(r)try{const c=JSON.parse(r);y.updatePersona(_,s.id,c).then(f)}catch(c){u.value="Invalid JSON: "+c.message}}return E(f),(s,r)=>{const c=G("router-link");return o(),n("div",null,[J(c,{to:"/",class:"btn-back"},{default:O(()=>[k("← "+e(d(l).t("dashboard")),1)]),_:1}),t("div",V,[t("h2",j,e(d(l).t("groupBuilder"))+" — "+e(g.value&&g.value.title),1),t("button",{class:"primary",style:{"margin-left":"auto"},onClick:C,disabled:i.value},[i.value?(o(),n("span",P)):m("",!0),k(e(d(l).t("analyze")),1)],8,A)]),u.value?(o(),n("div",F,e(u.value),1)):m("",!0),p.value.length===0&&!i.value?(o(),n("div",I,[r[0]||(r[0]=t("strong",null,"No personas yet",-1)),t("span",null,"Click "+e(d(l).t("analyze"))+" to generate the 15 personas (5 per tier).",1)])):m("",!0),(o(),n(b,null,x(["A","B","C"],h=>t("div",{key:h,style:{"margin-bottom":"20px"}},[t("h4",null,e(w(h)),1),t("div",L,[(o(!0),n(b,null,x(N(h),a=>(o(),n("div",{key:a.id,class:"card pcard"},[t("div",T,[t("strong",null,e(a.name),1),a.special==="wrong_text"?(o(),n("span",q,"⚠️ wrong_text")):m("",!0)]),t("div",D,e(a.profession)+" · "+e(a.age_group)+" · "+e(a.channel)+" · "+e(a.initiation_mode),1),t("div",M,"diff "+e(a.difficulty)+" · "+e(a.income)+" · "+e(a.personality),1),t("details",R,[t("summary",null,e(d(l).t("reveal")),1),t("pre",$,e(JSON.stringify(a,null,2)),1)]),t("button",{onClick:W=>B(a)},"✏️ Edit",8,H)]))),128))])])),64))])}}},Y=S(K,[["__scopeId","data-v-ee1c1944"]]);export{Y as default};
import{_ as S,k as E,c as n,l as J,m as O,a as t,t as e,u as d,i as l,e as m,j as k,F as b,n as x,h as z,p as y,q as G,r as v,o}from"./index-Hx407ukZ.js";const V={class:"row",style:{"align-items":"center","margin-bottom":"16px"}},j={style:{margin:"0"}},A=["disabled"],P={key:0,class:"spinner",style:{"margin-right":"4px"}},F={key:0,class:"error"},I={key:1,class:"card empty-state"},L={class:"grid"},T={class:"row"},q={key:0,class:"badge"},D={class:"muted"},M={class:"muted",style:{"margin-top":"4px"}},R={style:{"margin-top":"8px"},open:""},$={class:"json"},H=["onClick"],K={__name:"GroupEdit",setup(Q){const _=z().params.gid,g=v(null),p=v([]),i=v(!1),u=v("");async function f(){const s=await y.getGroup(_);g.value=s.group,p.value=(await y.listPersonas(_)).personas}function N(s){return p.value.filter(r=>r.tier===s)}function w(s){return l.t(s==="A"?"tierA":s==="B"?"tierB":"tierC")}async function C(){i.value=!0,u.value="";try{const s=await y.analyzeGroup(_);p.value=s.personas,await f()}catch(s){u.value=s.message}finally{i.value=!1}}function B(s){const r=prompt("Edit persona JSON (full fields):",JSON.stringify(s,null,2));if(r)try{const c=JSON.parse(r);y.updatePersona(_,s.id,c).then(f)}catch(c){u.value="Invalid JSON: "+c.message}}return E(f),(s,r)=>{const c=G("router-link");return o(),n("div",null,[J(c,{to:"/",class:"btn-back"},{default:O(()=>[k("← "+e(d(l).t("dashboard")),1)]),_:1}),t("div",V,[t("h2",j,e(d(l).t("groupBuilder"))+" — "+e(g.value&&g.value.title),1),t("button",{class:"primary",style:{"margin-left":"auto"},onClick:C,disabled:i.value},[i.value?(o(),n("span",P)):m("",!0),k(e(d(l).t("analyze")),1)],8,A)]),u.value?(o(),n("div",F,e(u.value),1)):m("",!0),p.value.length===0&&!i.value?(o(),n("div",I,[r[0]||(r[0]=t("strong",null,"No personas yet",-1)),t("span",null,"Click "+e(d(l).t("analyze"))+" to generate the 15 personas (5 per tier).",1)])):m("",!0),(o(),n(b,null,x(["A","B","C"],h=>t("div",{key:h,style:{"margin-bottom":"20px"}},[t("h4",null,e(w(h)),1),t("div",L,[(o(!0),n(b,null,x(N(h),a=>(o(),n("div",{key:a.id,class:"card pcard"},[t("div",T,[t("strong",null,e(a.name),1),a.special==="wrong_text"?(o(),n("span",q,"⚠️ wrong_text")):m("",!0)]),t("div",D,e(a.profession)+" · "+e(a.age_group)+" · "+e(a.channel)+" · "+e(a.initiation_mode),1),t("div",M,"diff "+e(a.difficulty)+" · "+e(a.income)+" · "+e(a.personality),1),t("details",R,[t("summary",null,e(d(l).t("reveal")),1),t("pre",$,e(JSON.stringify(a,null,2)),1)]),t("button",{onClick:W=>B(a)},"✏️ Edit",8,H)]))),128))])])),64))])}}},Y=S(K,[["__scopeId","data-v-ee1c1944"]]);export{Y as default};

View File

@@ -1 +1 @@
import{_ as b,c as d,a as e,t as s,u as c,i as o,w as y,v as x,b as w,d as k,e as S,r as l,f,g as V,h as B,o as v}from"./index-EpRXsI-R.js";const C={class:"login-wrap"},D={class:"card login-card"},K={class:"pw-wrap"},E=["type"],L=["aria-label"],M={key:0,class:"error",role:"alert"},N=["disabled"],R={key:0,class:"spinner"},U={key:1},q={__name:"Login",setup(H){const g=B(),_=V(),n=l(""),u=l(""),a=l(!1),r=l(""),i=l(!1);async function m(){r.value="",i.value=!0;try{await f.login(n.value.trim(),u.value),f.mustSetup?_.push({path:"/setup"}):_.push(g.query.redirect||"/")}catch{r.value=o.t("loginError")}finally{i.value=!1}}return(h,t)=>(v(),d("div",C,[e("div",D,[e("h1",null,"🎯 "+s(c(o).t("app")),1),t[3]||(t[3]=e("p",{class:"muted",style:{"margin-top":"-8px"}},"Sales training simulator",-1)),e("label",null,s(c(o).t("username")),1),y(e("input",{"onUpdate:modelValue":t[0]||(t[0]=p=>n.value=p),type:"text",autocomplete:"username",onKeyup:w(m,["enter"])},null,544),[[x,n.value]]),e("label",null,s(c(o).t("password")),1),e("div",K,[y(e("input",{"onUpdate:modelValue":t[1]||(t[1]=p=>u.value=p),type:a.value?"text":"password",autocomplete:"current-password",onKeyup:w(m,["enter"])},null,40,E),[[k,u.value]]),e("button",{type:"button",class:"pw-toggle",onClick:t[2]||(t[2]=p=>a.value=!a.value),"aria-label":a.value?"Hide password":"Show password"},s(a.value?"🙈":"👁"),9,L)]),r.value?(v(),d("div",M,s(r.value),1)):S("",!0),e("button",{class:"primary",style:{width:"100%","margin-top":"16px"},disabled:i.value||!n.value||!u.value,onClick:m},[i.value?(v(),d("span",R)):(v(),d("span",U,s(c(o).t("login")),1))],8,N)])]))}},P=b(q,[["__scopeId","data-v-f7904d9f"]]);export{P as default};
import{_ as b,c as d,a as e,t as s,u as c,i as o,w as y,v as x,b as w,d as k,e as S,r as l,f,g as V,h as B,o as v}from"./index-Hx407ukZ.js";const C={class:"login-wrap"},D={class:"card login-card"},K={class:"pw-wrap"},E=["type"],L=["aria-label"],M={key:0,class:"error",role:"alert"},N=["disabled"],R={key:0,class:"spinner"},U={key:1},q={__name:"Login",setup(H){const g=B(),_=V(),n=l(""),u=l(""),a=l(!1),r=l(""),i=l(!1);async function m(){r.value="",i.value=!0;try{await f.login(n.value.trim(),u.value),f.mustSetup?_.push({path:"/setup"}):_.push(g.query.redirect||"/")}catch{r.value=o.t("loginError")}finally{i.value=!1}}return(h,t)=>(v(),d("div",C,[e("div",D,[e("h1",null,"🎯 "+s(c(o).t("app")),1),t[3]||(t[3]=e("p",{class:"muted",style:{"margin-top":"-8px"}},"Sales training simulator",-1)),e("label",null,s(c(o).t("username")),1),y(e("input",{"onUpdate:modelValue":t[0]||(t[0]=p=>n.value=p),type:"text",autocomplete:"username",onKeyup:w(m,["enter"])},null,544),[[x,n.value]]),e("label",null,s(c(o).t("password")),1),e("div",K,[y(e("input",{"onUpdate:modelValue":t[1]||(t[1]=p=>u.value=p),type:a.value?"text":"password",autocomplete:"current-password",onKeyup:w(m,["enter"])},null,40,E),[[k,u.value]]),e("button",{type:"button",class:"pw-toggle",onClick:t[2]||(t[2]=p=>a.value=!a.value),"aria-label":a.value?"Hide password":"Show password"},s(a.value?"🙈":"👁"),9,L)]),r.value?(v(),d("div",M,s(r.value),1)):S("",!0),e("button",{class:"primary",style:{width:"100%","margin-top":"16px"},disabled:i.value||!n.value||!u.value,onClick:m},[i.value?(v(),d("span",R)):(v(),d("span",U,s(c(o).t("login")),1))],8,N)])]))}},P=b(q,[["__scopeId","data-v-f7904d9f"]]);export{P as default};

View File

@@ -0,0 +1 @@
import{_ as f,k as x,p as v,c as l,a as t,t as s,u as e,i as a,e as m,w as k,s as b,F as B,n as S,r as _,x as c,o as r,y as D}from"./index-Hx407ukZ.js";const M={key:0,class:"row stat-row"},N={class:"card stat"},T={class:"card stat won"},C={class:"card stat lost"},F={class:"card stat"},V={key:1,class:"card"},j={key:2,class:"card empty-state"},z={class:"card",style:{"margin-top":"20px"}},E={style:{"margin-top":"0"}},G={__name:"MyBoard",setup(I){const o=_([]),u=_([]),p=_(!0),y=c(()=>o.value.length),g=c(()=>o.value.filter(n=>n.my_outcome==="won").length),h=c(()=>o.value.filter(n=>n.my_outcome==="lost").length),w=c(()=>o.value.filter(n=>n.my_outcome==="not_tried").length);return x(async()=>{try{o.value=(await v.myBoard()).board||[],u.value=(await v.mySessions()).sessions||[]}finally{p.value=!1}}),(n,i)=>(r(),l("div",null,[t("h2",null,"📊 "+s(e(a).t("myDashboard")),1),p.value?m("",!0):(r(),l("div",M,[t("div",N,[t("span",null,s(e(a).t("total")),1),t("strong",null,s(y.value),1)]),t("div",T,[t("span",null,s(e(a).t("won")),1),t("strong",null,s(g.value),1)]),t("div",C,[t("span",null,s(e(a).t("lost")),1),t("strong",null,s(h.value),1)]),t("div",F,[t("span",null,s(e(a).t("notTried")),1),t("strong",null,s(w.value),1)])])),p.value?(r(),l("div",V,[...i[0]||(i[0]=[t("div",{class:"skeleton",style:{height:"50px"}},null,-1)])])):o.value.length===0?(r(),l("div",j,[...i[1]||(i[1]=[t("strong",null,"No practice yet",-1),t("span",null,"Go to Training and try closing a sale with a persona.",-1)])])):m("",!0),k(t("div",z,[t("h3",E,s(e(a).t("mySessions")),1),(r(!0),l(B,null,S(u.value,d=>(r(),l("div",{key:d.id,style:{display:"flex","justify-content":"space-between",padding:"8px 0","border-bottom":"1px solid var(--border)"}},[t("span",null,s(d.persona_name),1),t("span",{class:D(["badge",d.outcome])},s(d.outcome==="won"?e(a).t("won"):e(a).t("lost")),3)]))),128))],512),[[b,u.value.length]])]))}},q=f(G,[["__scopeId","data-v-1f6ea068"]]);export{q as default};

View File

@@ -1 +0,0 @@
import{_ as f,k,l as v,c as l,a as t,t as s,u as e,i as a,e as m,w as x,y as b,F as B,p as S,r as _,z as c,o as r,s as D}from"./index-EpRXsI-R.js";const M={key:0,class:"row stat-row"},N={class:"card stat"},T={class:"card stat won"},z={class:"card stat lost"},C={class:"card stat"},F={key:1,class:"card"},V={key:2,class:"card empty-state"},j={class:"card",style:{"margin-top":"20px"}},E={style:{"margin-top":"0"}},G={__name:"MyBoard",setup(I){const o=_([]),u=_([]),p=_(!0),y=c(()=>o.value.length),g=c(()=>o.value.filter(n=>n.my_outcome==="won").length),h=c(()=>o.value.filter(n=>n.my_outcome==="lost").length),w=c(()=>o.value.filter(n=>n.my_outcome==="not_tried").length);return k(async()=>{try{o.value=(await v.myBoard()).board||[],u.value=(await v.mySessions()).sessions||[]}finally{p.value=!1}}),(n,i)=>(r(),l("div",null,[t("h2",null,"📊 "+s(e(a).t("myDashboard")),1),p.value?m("",!0):(r(),l("div",M,[t("div",N,[t("span",null,s(e(a).t("total")),1),t("strong",null,s(y.value),1)]),t("div",T,[t("span",null,s(e(a).t("won")),1),t("strong",null,s(g.value),1)]),t("div",z,[t("span",null,s(e(a).t("lost")),1),t("strong",null,s(h.value),1)]),t("div",C,[t("span",null,s(e(a).t("notTried")),1),t("strong",null,s(w.value),1)])])),p.value?(r(),l("div",F,[...i[0]||(i[0]=[t("div",{class:"skeleton",style:{height:"50px"}},null,-1)])])):o.value.length===0?(r(),l("div",V,[...i[1]||(i[1]=[t("strong",null,"No practice yet",-1),t("span",null,"Go to Training and try closing a sale with a persona.",-1)])])):m("",!0),x(t("div",j,[t("h3",E,s(e(a).t("mySessions")),1),(r(!0),l(B,null,S(u.value,d=>(r(),l("div",{key:d.id,style:{display:"flex","justify-content":"space-between",padding:"8px 0","border-bottom":"1px solid var(--border)"}},[t("span",null,s(d.persona_name),1),t("span",{class:D(["badge",d.outcome])},s(d.outcome==="won"?e(a).t("won"):e(a).t("lost")),3)]))),128))],512),[[b,u.value.length]])]))}},q=f(G,[["__scopeId","data-v-1f6ea068"]]);export{q as default};

View File

@@ -1 +0,0 @@
import{_ as B,k as C,c as l,m as L,n as y,a as t,t as s,u as r,i as o,F as p,p as f,h as T,l as A,q as I,r as g,o as i,j as h,s as v,x as N}from"./index-EpRXsI-R.js";const P={class:"row",style:{"align-items":"center"}},V={style:{margin:"0"}},z={class:"grid"},F={class:"row"},$={class:"muted"},j={class:"muted"},q={class:"muted",style:{"margin-top":"6px"}},D={class:"primary",style:{width:"100%"}},E={key:1,class:"muted",style:{"margin-top":"auto","font-size":"12px"}},M={__name:"Personas",setup(R){const d=T().params.gid,u=g([]),k=g(!0);async function b(){try{u.value=(await A.listPersonas(d)).personas}finally{k.value=!1}}function x(a){return u.value.filter(n=>n.tier===a)}function w(a){return o.t(a==="A"?"tierA":a==="B"?"tierB":"tierC")}function _(a){return a==="won"?o.t("won"):a==="lost"?o.t("lost"):o.t("notTried")}return C(b),(a,n)=>{const m=I("router-link");return i(),l("div",null,[L(m,{to:"/",class:"btn-back"},{default:y(()=>[h("← "+s(r(o).t("dashboard")),1)]),_:1}),t("div",P,[t("h2",V,s(r(o).t("personas")),1),n[0]||(n[0]=t("span",{class:"muted",style:{"margin-left":"auto"}},"Levels: choose one to practice (one-shot)",-1))]),(i(),l(p,null,f(["A","B","C"],c=>t("div",{key:c,style:{margin:"20px 0"}},[t("h4",null,s(w(c)),1),t("div",z,[(i(!0),l(p,null,f(x(c),e=>(i(),l("div",{key:e.id,class:"card pcard lift"},[t("div",F,[t("strong",null,s(e.name),1),t("span",{class:v(["badge",e.my_outcome])},s(_(e.my_outcome)),3)]),t("div",$,[h(s(e.profession)+" · "+s(e.age_group)+" · "+s(e.location),1),n[1]||(n[1]=t("br",null,null,-1)),t("span",{class:v(["badge",e.channel])},s(e.channel),3),t("span",j," · "+s(e.initiation_mode==="seller"?r(o).t("sellerInitiated"):r(o).t("customerInitiated")),1)]),t("div",q,s(e.product_context),1),e.my_outcome==="not_tried"?(i(),N(m,{key:0,to:`/groups/${r(d)}/chat/${e.id}`,style:{"margin-top":"auto"}},{default:y(()=>[t("button",D,s(r(o).t("chat")),1)]),_:1},8,["to"])):(i(),l("div",E,"✓ Trained ("+s(_(e.my_outcome))+")",1))]))),128))])])),64))])}}},H=B(M,[["__scopeId","data-v-88c74e17"]]);export{H as default};

View File

@@ -0,0 +1 @@
.grid[data-v-f8bfbe2d]{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px}.pcard[data-v-f8bfbe2d]{display:flex;flex-direction:column;min-height:190px}.diff[data-v-f8bfbe2d]{margin:8px 0}.star[data-v-f8bfbe2d]{color:#d8dbe3}.star.on[data-v-f8bfbe2d]{color:#f59e0b}.badge.won[data-v-f8bfbe2d]{background:#dcfce7;color:#166534}.badge.lost[data-v-f8bfbe2d]{background:#fee2e2;color:#991b1b}.badge.not_tried[data-v-f8bfbe2d]{background:#eef2ff;color:#4338ca}

View File

@@ -0,0 +1 @@
import{_ as $,k as N,c as r,l as v,m as u,a as t,t as s,u as a,i as o,f as b,e as V,F as _,n as y,h as z,p as I,q as L,r as k,o as n,j as x,y as f,z as w}from"./index-Hx407ukZ.js";const T={class:"row",style:{"align-items":"center"}},j={style:{margin:"0"}},F={class:"muted",style:{"margin-left":"auto"}},q={key:0,class:"row",style:{margin:"12px 0",gap:"10px"}},D={class:"primary"},E={class:"grid"},M={class:"row",style:{"justify-content":"space-between"}},R={class:"diff"},S={class:"muted"},G={class:"muted"},H={class:"muted"},J={class:"muted",style:{"margin-top":"6px"}},K={class:"primary",style:{width:"100%"}},O={class:"primary",style:{width:"100%"}},Q={key:1,class:"muted",style:{"margin-top":"auto","font-size":"12px"}},U={__name:"Personas",setup(W){const c=z().params.gid,p=k([]),B=k(!0);async function A(){try{p.value=(await I.listPersonas(c)).personas}finally{B.value=!1}}function C(i){return p.value.filter(l=>l.tier===i)}function P(i){return o.t(i==="A"?"tierA":i==="B"?"tierB":"tierC")}function g(i){return i==="won"?o.t("won"):i==="lost"?o.t("lost"):o.t("notTried")}return N(A),(i,l)=>{const d=L("router-link");return n(),r("div",null,[v(d,{to:"/training",class:"btn-back"},{default:u(()=>[x("← "+s(a(o).t("training")),1)]),_:1}),t("div",T,[t("h2",j,s(a(o).t("personas")),1),t("span",F,s(a(o).t("selectPersona")),1)]),a(b).isAdmin?(n(),r("div",q,[v(d,{to:`/admin/groups/${a(c)}/edit`},{default:u(()=>[t("button",D,"🛠 "+s(a(o).t("managePersonas")),1)]),_:1},8,["to"]),l[0]||(l[0]=t("span",{class:"muted"},"Admin: full persona data + edit available here.",-1))])):V("",!0),(n(),r(_,null,y(["A","B","C"],m=>t("div",{key:m,style:{margin:"20px 0"}},[t("h4",null,s(P(m)),1),t("div",E,[(n(!0),r(_,null,y(C(m),e=>(n(),r("div",{key:e.id,class:"card pcard lift"},[t("div",M,[t("strong",null,s(e.name),1),t("span",{class:f(["badge",e.my_outcome])},s(g(e.my_outcome)),3)]),t("div",R,[(n(),r(_,null,y(5,h=>t("span",{key:h,class:f(["star",{on:h<=(e.difficulty||1)}])},"★",2)),64)),t("span",S,s(a(o).t("difficulty"))+" "+s(e.difficulty||1)+"/5",1)]),t("div",G,[x(s(e.profession)+" · "+s(e.age_group)+" · "+s(e.location),1),l[1]||(l[1]=t("br",null,null,-1)),t("span",{class:f(["badge",e.channel])},s(e.channel),3),t("span",H," · "+s(e.initiation_mode==="seller"?a(o).t("sellerInitiated"):a(o).t("customerInitiated")),1)]),t("div",J,s(e.product_context),1),a(b).isAdmin?(n(),w(d,{key:0,to:`/groups/${a(c)}/chat/${e.id}`,style:{"margin-top":"auto"}},{default:u(()=>[t("button",K,s(a(o).t("chat")),1)]),_:1},8,["to"])):(n(),r(_,{key:1},[e.my_outcome==="not_tried"?(n(),w(d,{key:0,to:`/groups/${a(c)}/chat/${e.id}`,style:{"margin-top":"auto"}},{default:u(()=>[t("button",O,s(a(o).t("chat")),1)]),_:1},8,["to"])):(n(),r("div",Q,"✓ "+s(a(o).t("trained"))+" ("+s(g(e.my_outcome))+")",1))],64))]))),128))])])),64))])}}},Z=$(U,[["__scopeId","data-v-f8bfbe2d"]]);export{Z as default};

View File

@@ -1 +0,0 @@
.grid[data-v-88c74e17]{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px}.pcard[data-v-88c74e17]{display:flex;flex-direction:column;min-height:170px}

View File

@@ -0,0 +1 @@
import{_ as j,r as v,f as u,c as y,a as e,t as o,u as l,i as s,j as x,w as h,v as b,e as k,y as B,p as S,o as f}from"./index-Hx407ukZ.js";const D={class:"card"},E={style:{"margin-top":"0"}},M={class:"muted"},$=["value"],z={class:"row",style:{gap:"10px","margin-top":"16px"}},I=["disabled"],L={key:0,class:"spinner"},O={class:"card",style:{"margin-top":"16px"}},q={style:{"margin-top":"0"}},A={class:"row",style:{gap:"10px","margin-top":"16px"}},F=["disabled"],G={key:0,class:"spinner"},H={class:"card",style:{"margin-top":"16px"}},J={style:{"margin-top":"0"}},K={class:"row"},Q={key:0,class:"error",role:"alert"},R={key:1,class:"ok",role:"status"},W={__name:"Settings",setup(X){var V,C;const m=v(((V=u.user)==null?void 0:V.name)||""),c=v(((C=u.user)==null?void 0:C.email)||""),i=v(""),_=v(""),r=v(""),p=v(""),w=v(!1),g=v(!1);async function U(){var d,a;r.value="",p.value="",w.value=!0;try{const n={};if(m.value&&m.value!==((d=u.user)==null?void 0:d.name)&&(n.name=m.value),c.value&&c.value!==((a=u.user)==null?void 0:a.email)&&(n.email=c.value),Object.keys(n).length){const t=await S.updateProfile(n);u.user=t.user,p.value=s.t("saved")}}catch(n){r.value=n.message}finally{w.value=!1}}async function N(){var d,a,n;if(r.value="",p.value="",i.value.length<4){r.value=s.t("passwordTooShort");return}if(i.value!==_.value){r.value=s.t("passwordMismatch");return}g.value=!0;try{const t=((d=u.user)==null?void 0:d.username)||((a=u.user)==null?void 0:a.id)||"",T=await S.setup({username:t,email:c.value||((n=u.user)==null?void 0:n.email),password:i.value});u.user=T.user,i.value="",_.value="",p.value=s.t("saved")}catch(t){r.value=t.message}finally{g.value=!1}}function P(d){s.set(d)}return(d,a)=>{var n;return f(),y("div",null,[e("h2",null,"⚙️ "+o(l(s).t("settings")),1),e("div",D,[e("h3",E,"👤 "+o(l(s).t("profile")),1),e("label",null,[x(o(l(s).t("username"))+" ",1),e("span",M,"("+o(l(s).t("readonly"))+")",1)]),e("input",{value:((n=l(u).user)==null?void 0:n.username)||"",disabled:""},null,8,$),e("label",null,o(l(s).t("name")),1),h(e("input",{"onUpdate:modelValue":a[0]||(a[0]=t=>m.value=t),type:"text"},null,512),[[b,m.value]]),e("label",null,o(l(s).t("email")),1),h(e("input",{"onUpdate:modelValue":a[1]||(a[1]=t=>c.value=t),type:"email"},null,512),[[b,c.value]]),e("div",z,[e("button",{class:"primary",disabled:w.value,onClick:U},[w.value?(f(),y("span",L)):k("",!0),x(o(l(s).t("saveProfile")),1)],8,I)])]),e("div",O,[e("h3",q,"🔒 "+o(l(s).t("changePassword")),1),e("label",null,o(l(s).t("newPassword")),1),h(e("input",{"onUpdate:modelValue":a[2]||(a[2]=t=>i.value=t),type:"password",autocomplete:"new-password"},null,512),[[b,i.value]]),e("label",null,o(l(s).t("confirmPassword")),1),h(e("input",{"onUpdate:modelValue":a[3]||(a[3]=t=>_.value=t),type:"password",autocomplete:"new-password"},null,512),[[b,_.value]]),e("div",A,[e("button",{class:"primary",disabled:g.value||!i.value||i.value!==_.value,onClick:N},[g.value?(f(),y("span",G)):k("",!0),x(o(l(s).t("save")),1)],8,F)])]),e("div",H,[e("h3",J,"🌐 "+o(l(s).t("language")),1),e("div",K,[e("button",{class:B({active:l(s).locale==="th"}),onClick:a[4]||(a[4]=t=>P("th"))},"ไทย",2),e("button",{class:B({active:l(s).locale==="en"}),onClick:a[5]||(a[5]=t=>P("en"))},"English",2)])]),r.value?(f(),y("div",Q,o(r.value),1)):k("",!0),p.value?(f(),y("div",R,"✅ "+o(p.value),1)):k("",!0)])}}},Z=j(W,[["__scopeId","data-v-2dca72e5"]]);export{Z as default};

View File

@@ -1 +0,0 @@
import{_ as N,r as i,f as r,c as m,a as e,t as l,u as s,i as t,w,v as k,j as x,e as y,s as C,l as S,o as _}from"./index-EpRXsI-R.js";const D={class:"card"},E={style:{"margin-top":"0"}},T=["value"],U={class:"muted"},$={key:0,class:"error",role:"alert"},j={key:1,class:"ok",role:"status"},z=["disabled"],I={key:0,class:"spinner"},M={class:"card",style:{"margin-top":"16px"}},P={style:{"margin-top":"0"}},q={class:"row"},A={__name:"Settings",setup(F){var f,h,b;const c=i(((f=r.user)==null?void 0:f.email)||""),o=i(""),d=i(""),v=i(""),p=i(!1),V=((h=r.user)==null?void 0:h.username)||((b=r.user)==null?void 0:b.id)||"";async function B(){d.value="",v.value="",p.value=!0;try{if(c.value||o.value){const n={username:V,email:c.value};o.value&&(n.password=o.value);const a=await S.setup(n);r.user=a.user,v.value=t.t("saved")}}catch(n){d.value=n.message}finally{p.value=!1}}return(n,a)=>{var g;return _(),m("div",null,[e("h2",null,"⚙️ "+l(s(t).t("settings")),1),e("div",D,[e("h3",E,l(s(t).t("account")),1),e("label",null,l(s(t).t("username")),1),e("input",{value:((g=s(r).user)==null?void 0:g.username)||"",disabled:""},null,8,T),e("label",null,l(s(t).t("email")),1),w(e("input",{"onUpdate:modelValue":a[0]||(a[0]=u=>c.value=u),type:"email"},null,512),[[k,c.value]]),e("label",null,[x(l(s(t).t("newPassword"))+" ",1),e("span",U,"("+l(s(t).t("optional"))+")",1)]),w(e("input",{"onUpdate:modelValue":a[1]||(a[1]=u=>o.value=u),type:"password",autocomplete:"new-password"},null,512),[[k,o.value]]),d.value?(_(),m("div",$,l(d.value),1)):y("",!0),v.value?(_(),m("div",j,"✅ "+l(v.value),1)):y("",!0),e("button",{class:"primary",style:{"margin-top":"16px"},disabled:p.value,onClick:B},[p.value?(_(),m("span",I)):y("",!0),x(l(s(t).t("save")),1)],8,z)]),e("div",M,[e("h3",P,l(s(t).t("language")),1),e("div",q,[e("button",{class:C({active:s(t).locale==="th"}),onClick:a[2]||(a[2]=u=>s(t).set("th"))},"ไทย",2),e("button",{class:C({active:s(t).locale==="en"}),onClick:a[3]||(a[3]=u=>s(t).set("en"))},"English",2)])])])}}},H=N(A,[["__scopeId","data-v-ce5f07b9"]]);export{H as default};

View File

@@ -1 +0,0 @@
.active[data-v-ce5f07b9]{border-color:var(--accent);color:var(--accent);font-weight:600}.ok[data-v-ce5f07b9]{color:var(--green);font-size:13px;margin-top:8px}

View File

@@ -0,0 +1 @@
.active[data-v-2dca72e5]{border-color:var(--accent);color:var(--accent);font-weight:600}.ok[data-v-2dca72e5]{color:var(--green);font-size:13px;margin-top:10px}.error[data-v-2dca72e5]{margin-top:10px}

View File

@@ -1 +1 @@
import{_ as x,c as v,a as e,t,u as s,i as a,j as S,f as _,w,v as f,b as y,e as V,r,g as K,o as m}from"./index-EpRXsI-R.js";const T={class:"setup-wrap"},B={class:"card setup-card"},N={class:"muted"},U={key:0,class:"error",role:"alert"},C=["disabled"],D={key:0,class:"spinner"},M={key:1},P={__name:"Setup",setup(j){const k=K(),i=r(""),l=r(""),p=r(""),o=r(""),d=r(!1);async function c(){if(o.value="",l.value.length<4){o.value=a.t("passwordTooShort");return}if(l.value!==p.value){o.value=a.t("passwordMismatch");return}d.value=!0;try{await _.finishSetup(i.value.trim(),l.value),k.push("/")}catch(h){o.value=h.message}finally{d.value=!1}}return(h,u)=>{var b,g;return m(),v("div",T,[e("div",B,[e("h1",null,"🔐 "+t(s(a).t("setupTitle")),1),e("p",N,[S(t(s(a).t("setupSubtitle"))+" ",1),e("strong",null,t(((b=s(_).user)==null?void 0:b.name)||((g=s(_).user)==null?void 0:g.username)),1)]),e("label",null,t(s(a).t("email")),1),w(e("input",{"onUpdate:modelValue":u[0]||(u[0]=n=>i.value=n),type:"email",autocomplete:"email",onKeyup:y(c,["enter"])},null,544),[[f,i.value]]),e("label",null,t(s(a).t("newPassword")),1),w(e("input",{"onUpdate:modelValue":u[1]||(u[1]=n=>l.value=n),type:"password",autocomplete:"new-password",onKeyup:y(c,["enter"])},null,544),[[f,l.value]]),e("label",null,t(s(a).t("confirmPassword")),1),w(e("input",{"onUpdate:modelValue":u[2]||(u[2]=n=>p.value=n),type:"password",autocomplete:"new-password",onKeyup:y(c,["enter"])},null,544),[[f,p.value]]),o.value?(m(),v("div",U,t(o.value),1)):V("",!0),e("button",{class:"primary",style:{width:"100%","margin-top":"16px"},disabled:d.value||!i.value||!l.value||l.value!==p.value,onClick:c},[d.value?(m(),v("span",D)):(m(),v("span",M,t(s(a).t("save")),1))],8,C)])])}}},I=x(P,[["__scopeId","data-v-f190e120"]]);export{I as default};
import{_ as x,c as v,a as e,t,u as s,i as a,j as S,f as _,w,v as f,b as y,e as V,r,g as K,o as m}from"./index-Hx407ukZ.js";const T={class:"setup-wrap"},B={class:"card setup-card"},N={class:"muted"},U={key:0,class:"error",role:"alert"},C=["disabled"],D={key:0,class:"spinner"},M={key:1},P={__name:"Setup",setup(j){const k=K(),i=r(""),l=r(""),p=r(""),o=r(""),d=r(!1);async function c(){if(o.value="",l.value.length<4){o.value=a.t("passwordTooShort");return}if(l.value!==p.value){o.value=a.t("passwordMismatch");return}d.value=!0;try{await _.finishSetup(i.value.trim(),l.value),k.push("/")}catch(h){o.value=h.message}finally{d.value=!1}}return(h,u)=>{var b,g;return m(),v("div",T,[e("div",B,[e("h1",null,"🔐 "+t(s(a).t("setupTitle")),1),e("p",N,[S(t(s(a).t("setupSubtitle"))+" ",1),e("strong",null,t(((b=s(_).user)==null?void 0:b.name)||((g=s(_).user)==null?void 0:g.username)),1)]),e("label",null,t(s(a).t("email")),1),w(e("input",{"onUpdate:modelValue":u[0]||(u[0]=n=>i.value=n),type:"email",autocomplete:"email",onKeyup:y(c,["enter"])},null,544),[[f,i.value]]),e("label",null,t(s(a).t("newPassword")),1),w(e("input",{"onUpdate:modelValue":u[1]||(u[1]=n=>l.value=n),type:"password",autocomplete:"new-password",onKeyup:y(c,["enter"])},null,544),[[f,l.value]]),e("label",null,t(s(a).t("confirmPassword")),1),w(e("input",{"onUpdate:modelValue":u[2]||(u[2]=n=>p.value=n),type:"password",autocomplete:"new-password",onKeyup:y(c,["enter"])},null,544),[[f,p.value]]),o.value?(m(),v("div",U,t(o.value),1)):V("",!0),e("button",{class:"primary",style:{width:"100%","margin-top":"16px"},disabled:d.value||!i.value||!l.value||l.value!==p.value,onClick:c},[d.value?(m(),v("span",D)):(m(),v("span",M,t(s(a).t("save")),1))],8,C)])])}}},I=x(P,[["__scopeId","data-v-f190e120"]]);export{I as default};

View File

@@ -0,0 +1 @@
.grid[data-v-901c9d3d]{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:16px}.train-card[data-v-901c9d3d]{display:flex;flex-direction:column;height:100%}.badge.ready[data-v-901c9d3d]{background:#dcfce7;color:#166534}.badge.draft[data-v-901c9d3d]{background:#fef9c3;color:#854d0e}button.soft[data-v-901c9d3d]{background:#f1f3f9;border-color:transparent}.badge.facebook[data-v-901c9d3d]{background:#e0f2fe;color:#0369a1}.badge.line[data-v-901c9d3d]{background:#dcfce7;color:#15803d}

View File

@@ -0,0 +1 @@
import{_ as k,k as x,p as w,f as d,c as r,a as t,t as e,u as s,i as o,z as _,m,e as y,F as b,n as C,r as h,q as A,o as i,y as f}from"./index-Hx407ukZ.js";const B={class:"row",style:{"align-items":"center","margin-bottom":"16px"}},N={style:{margin:"0"}},P={style:{"margin-left":"auto"}},z={class:"primary"},T={class:"muted"},F={key:0,class:"card",style:{"min-height":"80px"}},L={key:1,class:"card empty-state"},S={key:0},V={key:1},$={class:"grid"},j={class:"card lift train-card"},q={class:"row",style:{"justify-content":"space-between"}},D={class:"muted",style:{margin:"6px 0 12px"}},E={class:"row",style:{gap:"8px"}},G={class:"muted"},I={class:"primary",style:{width:"100%","margin-top":"12px"}},M={__name:"Training",setup(H){const c=h([]),u=h(!0);function v(a){return a.sales_kit&&a.sales_kit.productName||a.input&&a.input.product||""}function g(a){return a.personas||a.persona_count||0}return x(async()=>{try{const a=(await w.listGroups()).groups||[];c.value=d.isAdmin?a:a.filter(l=>l.status==="ready")}finally{u.value=!1}}),(a,l)=>{const p=A("router-link");return i(),r("div",null,[t("div",B,[t("h2",N,"🎯 "+e(s(o).t("training")),1),t("div",P,[s(d).isAdmin?(i(),_(p,{key:0,to:"/admin/new-group"},{default:m(()=>[t("button",z," "+e(s(o).t("addProduct")),1)]),_:1})):y("",!0)])]),t("p",T,e(s(o).t("trainingSubtitle")),1),u.value?(i(),r("div",F,[...l[0]||(l[0]=[t("div",{class:"skeleton",style:{height:"50px"}},null,-1)])])):c.value.length===0?(i(),r("div",L,[t("strong",null,e(s(o).t("noTraining")),1),s(d).isAdmin?(i(),r("span",S,'Click "'+e(s(o).t("addProduct"))+'" to create a persona group.',1)):(i(),r("span",V,"Ask an admin to create a persona group first."))])):y("",!0),t("div",$,[(i(!0),r(b,null,C(c.value,n=>(i(),_(p,{key:n.id,to:s(d).isAdmin?`/admin/groups/${n.id}/edit`:`/groups/${n.id}/personas`,style:{"text-decoration":"none"}},{default:m(()=>[t("div",j,[t("div",q,[t("strong",null,e(n.title),1),t("span",{class:f(["badge",n.status==="ready"?"ready":"draft"])},e(n.status),3)]),t("div",D,e(v(n)),1),t("div",E,[t("span",{class:f(["badge",n.channel||"line"])},e(n.channel||"line"),3),t("span",G,e(g(n))+" "+e(s(o).t("personas").toLowerCase()),1)]),t("button",I,e(s(d).isAdmin?n.status==="ready"?s(o).t("managePersonas"):s(o).t("analyze"):s(o).t("selectPersona")),1)])]),_:2},1032,["to"]))),128))])])}}},K=k(M,[["__scopeId","data-v-901c9d3d"]]);export{K as default};

View File

@@ -1 +0,0 @@
import{_ as m,k as y,l as v,c as r,a as t,t as n,u as d,i as u,e as g,F as f,p as k,r as c,q as x,o,x as h,n as b,s as w}from"./index-EpRXsI-R.js";const N={key:0,class:"card",style:{"min-height":"80px"}},B={key:1,class:"card empty-state"},C={class:"grid"},F={class:"card lift train-card"},P={class:"row",style:{"justify-content":"space-between"}},T={class:"muted",style:{margin:"6px 0 12px"}},V={class:"primary",style:{width:"100%","margin-top":"12px"}},j={__name:"Training",setup(q){const l=c([]),i=c(!0);function p(e){return e.sales_kit&&e.sales_kit.productName||e.input&&e.input.product||""}return y(async()=>{try{const e=(await v.listGroups()).groups||[];l.value=e.filter(s=>s.status==="ready")}finally{i.value=!1}}),(e,s)=>{const _=x("router-link");return o(),r("div",null,[t("h2",null,"🎯 "+n(d(u).t("training")),1),s[3]||(s[3]=t("p",{class:"muted"},"Pick a product group to practice closing a sale.",-1)),i.value?(o(),r("div",N,[...s[0]||(s[0]=[t("div",{class:"skeleton",style:{height:"50px"}},null,-1)])])):l.value.length===0?(o(),r("div",B,[...s[1]||(s[1]=[t("strong",null,"No training groups available",-1),t("span",null,"Ask an admin to create a persona group first.",-1)])])):g("",!0),t("div",C,[(o(!0),r(f,null,k(l.value,a=>(o(),h(_,{key:a.id,to:`/groups/${a.id}/personas`,style:{"text-decoration":"none"}},{default:b(()=>[t("div",F,[t("div",P,[t("strong",null,n(a.title),1),s[2]||(s[2]=t("span",{class:"badge ready"},"ready",-1))]),t("div",T,n(p(a)),1),t("div",{class:w(["badge",a.channel])},n(a.channel||"line"),3),t("button",V,n(d(u).t("selectPersona")),1)])]),_:2},1032,["to"]))),128))])])}}},A=m(j,[["__scopeId","data-v-493baf1e"]]);export{A as default};

View File

@@ -1 +0,0 @@
.grid[data-v-493baf1e]{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:16px}.train-card[data-v-493baf1e]{display:flex;flex-direction:column;height:100%}.badge.ready[data-v-493baf1e]{background:#dcfce7;color:#166534}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sales Trainer</title>
<script type="module" crossorigin src="/assets/index-EpRXsI-R.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CSFE-aMG.css">
<script type="module" crossorigin src="/assets/index-Hx407ukZ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-HCrojZU7.css">
</head>
<body>
<div id="app"></div>

View File

@@ -36,6 +36,7 @@ export const api = {
login: (username, password) => request('POST', '/api/auth/login', { username, password }),
me: () => request('GET', '/api/auth/me'),
setup: (b) => request('POST', '/api/auth/setup', b),
updateProfile: (b) => request('PATCH', '/api/auth/profile', b),
adminCreateUser: (b) => request('POST', '/api/admin/users', b),
adminListUsers: () => request('GET', '/api/admin/users'),
adminUpdateUser: (username, b) => request('PUT', `/api/admin/users/${username}`, b),
@@ -54,5 +55,11 @@ export const api = {
weakAreas: () => request('GET', '/api/me/weak-areas'),
myPersonas: () => request('GET', '/api/me/personas'),
generatePersona: (b) => request('POST', '/api/me/personas/generate', b),
analytics: () => request('GET', '/api/analytics'),
analytics: (params = {}) => {
const qs = new URLSearchParams()
if (params.from) qs.set('from', params.from)
if (params.to) qs.set('to', params.to)
const q = qs.toString()
return request('GET', `/api/analytics${q ? `?${q}` : ''}`)
},
}

View File

@@ -23,9 +23,24 @@ const messages = {
adminOverview: 'Admin overview',
settings: 'Settings',
account: 'Account',
profile: 'Profile',
readonly: 'read-only',
name: 'Display name',
saveProfile: 'Save profile',
changePassword: 'Change password',
optional: 'optional',
saved: 'Saved',
total: 'Total',
dateRange: 'Date range',
apply: 'Apply',
clear: 'Clear',
hardestPersonas: 'Hardest personas',
managePersonas: 'Manage personas',
difficulty: 'Difficulty',
trained: 'Trained',
addProduct: 'Add product',
trainingSubtitle: 'Pick a product group, then choose a persona to practice closing the sale.',
noTraining: 'No training groups available',
groups: 'Persona Groups',
myTraining: 'My Training',
adminTools: 'Admin Tools',
@@ -90,9 +105,24 @@ const messages = {
adminOverview: 'ภาพรวม Admin',
settings: 'การตั้งค่า',
account: 'บัญชี',
profile: 'โปรไฟล์',
readonly: 'อ่านอย่างเดียว',
name: 'ชื่อที่แสดง',
saveProfile: 'บันทึกโปรไฟล์',
changePassword: 'เปลี่ยนรหัสผ่าน',
optional: 'ไม่บังคับ',
saved: 'บันทึกแล้ว',
total: 'ทั้งหมด',
dateRange: 'ช่วงวันที่',
apply: 'กรอง',
clear: 'ล้าง',
hardestPersonas: 'Persona ที่ยากสุด',
managePersonas: 'จัดการ Persona',
difficulty: 'ความยาก',
trained: 'ฝึกแล้ว',
addProduct: 'เพิ่มสินค้า',
trainingSubtitle: 'เลือกกลุ่มสินค้า แล้วเลือก persona เพื่อฝึกปิดการขาย',
noTraining: 'ยังไม่มีกลุ่มฝึก',
groups: 'กลุ่มลูกค้า (Persona)',
myTraining: 'การฝึกของฉัน',
adminTools: 'เครื่องมือ Admin',

View File

@@ -4,8 +4,8 @@ import { auth } from '../store/auth'
const routes = [
{ path: '/login', component: () => import('../views/Login.vue'), meta: { public: true } },
{ path: '/setup', component: () => import('../views/Setup.vue') },
// Tab 1: Admin overview (admin only)
{ path: '/', component: () => import('../views/Dashboard.vue'), meta: { admin: true } },
// Tab 1: Admin overview (aggregate stats + date filter, admin only)
{ path: '/', component: () => import('../views/Analytics.vue'), meta: { admin: true } },
// Tab 2: My dashboard (everyone, incl admin)
{ path: '/my/board', component: () => import('../views/MyBoard.vue') },
// Tab 3: Training — product list -> personas -> chat

View File

@@ -90,6 +90,8 @@ button { min-height: 44px; transition: transform .15s ease, box-shadow .2s ease,
button:not(:disabled):hover { box-shadow: 0 4px 12px rgba(20,24,40,.1); }
button:not(:disabled):active { transform: scale(.97); }
button.primary:not(:disabled):hover { box-shadow: 0 6px 18px rgba(79,70,229,.35); }
button.soft { background: #f1f3f9; border-color: transparent; color: var(--ink); }
button.soft:not(:disabled):hover { background: #e6eaf3; }
input, select, textarea {
min-height: 44px;

View File

@@ -1,21 +1,51 @@
<template>
<div>
<h2>{{ i18n.t('analytics') }}</h2>
<div class="row" style="align-items:center;margin-bottom:16px">
<h2 style="margin:0">📊 {{ i18n.t('adminOverview') }}</h2>
<div class="row" style="margin-left:auto;gap:10px">
<router-link to="/admin/users"><button class="soft">👥 {{ i18n.t('users') }}</button></router-link>
<router-link to="/admin/new-group"><button class="primary"> {{ i18n.t('addProduct') }}</button></router-link>
</div>
</div>
<!-- Date filter -->
<div class="card filter-bar">
<span class="muted">🗓 {{ i18n.t('dateRange') }}</span>
<input type="date" v-model="from" />
<span></span>
<input type="date" v-model="to" />
<button class="primary" @click="apply" :disabled="loading"> {{ i18n.t('apply') }}</button>
<button @click="clear" :disabled="loading">{{ i18n.t('clear') }}</button>
</div>
<!-- Headline metrics -->
<div class="row" style="gap:16px;margin:16px 0">
<div class="card stat"><div>Sessions</div><strong>{{ a.overall.total_sessions }}</strong></div>
<div class="card stat"><div>Wins</div><strong style="color:var(--green)">{{ a.overall.wins }}</strong></div>
<div class="card stat"><div>Losses</div><strong style="color:var(--red)">{{ a.overall.losses }}</strong></div>
<div class="card stat"><div>Close rate</div><strong>{{ a.overall.close_rate }}%</strong></div>
<div class="card stat"><div>Avg score</div><strong>{{ a.overall.avg_score }}</strong></div>
<div class="card stat"><div>Trainees</div><strong>{{ a.trainee_count }}</strong></div>
</div>
<h3>Trainees: {{ a.trainee_count }}</h3>
<h3>Hardest personas</h3>
<div class="card" v-for="(p, i) in a.hardest_personas" :key="i" style="margin-bottom:8px">
<strong>{{ p.persona_name }}</strong>
<span class="badge lost">{{ p.losses }}L</span>
<span class="badge won">{{ p.wins }}W</span>
<span class="muted">· avg {{ p.avg_score }}</span>
<!-- Hardest personas -->
<h3>{{ i18n.t('hardestPersonas') }}</h3>
<div v-if="a.hardest_personas.length === 0" class="card empty-state">
<strong>No data</strong><span>No sessions in this date range.</span>
</div>
<div class="grid">
<div v-for="(p, i) in a.hardest_personas" :key="i" class="card lift tier-card">
<div class="row" style="justify-content:space-between">
<strong>{{ p.persona_name }}</strong>
<span class="muted">#{{ i + 1 }}</span>
</div>
<div class="row" style="margin-top:10px;gap:8px">
<span class="badge won">{{ p.wins }}W</span>
<span class="badge lost">{{ p.losses }}L</span>
<span class="badge not_tried">{{ p.plays }} plays</span>
</div>
<div class="muted" style="margin-top:8px">{{ i18n.t('score') }}: {{ p.avg_score }}</div>
</div>
</div>
</div>
</template>
@@ -26,11 +56,32 @@ import { api } from '../api'
import { i18n } from '../i18n'
const a = ref({ overall: { total_sessions: 0, wins: 0, losses: 0, close_rate: 0, avg_score: 0 }, trainee_count: 0, hardest_personas: [] })
onMounted(async () => { a.value = await api.analytics() })
const from = ref('')
const to = ref('')
const loading = ref(false)
async function load() {
loading.value = true
try {
a.value = await api.analytics({ from: from.value, to: to.value })
} finally {
loading.value = false
}
}
function apply() { load() }
function clear() { from.value = ''; to.value = ''; load() }
onMounted(load)
</script>
<style scoped>
.stat { text-align: center; min-width: 110px; }
.stat div { color: var(--muted); font-size: 12px; }
.stat strong { font-size: 20px; }
.filter-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.filter-bar input[type=date] { width: auto; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
.tier-card { display: flex; flex-direction: column; }
.badge.won { background: #dcfce7; color: #166534; }
.badge.lost { background: #fee2e2; color: #991b1b; }
.badge.not_tried { background: #eef2ff; color: #4338ca; }
</style>

View File

@@ -1,29 +1,47 @@
<template>
<div>
<router-link to="/" class="btn-back"> {{ i18n.t('dashboard') }}</router-link>
<router-link to="/training" class="btn-back"> {{ i18n.t('training') }}</router-link>
<div class="row" style="align-items:center">
<h2 style="margin:0">{{ i18n.t('personas') }}</h2>
<span class="muted" style="margin-left:auto">Levels: choose one to practice (one-shot)</span>
<span class="muted" style="margin-left:auto">{{ i18n.t('selectPersona') }}</span>
</div>
<div class="row" v-if="auth.isAdmin" style="margin:12px 0;gap:10px">
<router-link :to="`/admin/groups/${gid}/edit`"><button class="primary">🛠 {{ i18n.t('managePersonas') }}</button></router-link>
<span class="muted">Admin: full persona data + edit available here.</span>
</div>
<div v-for="tier in ['A','B','C']" :key="tier" style="margin:20px 0">
<h4>{{ tierLabel(tier) }}</h4>
<div class="grid">
<div v-for="p in byTier(tier)" :key="p.id" class="card pcard lift">
<div class="row">
<div class="row" style="justify-content:space-between">
<strong>{{ p.name }}</strong>
<span class="badge" :class="p.my_outcome">{{ outcomeLabel(p.my_outcome) }}</span>
</div>
<!-- Difficulty -->
<div class="diff">
<span v-for="n in 5" :key="n" class="star" :class="{ on: n <= (p.difficulty || 1) }"></span>
<span class="muted"> {{ i18n.t('difficulty') }} {{ p.difficulty || 1 }}/5</span>
</div>
<div class="muted">
{{ p.profession }} · {{ p.age_group }} · {{ p.location }}<br />
<span class="badge" :class="p.channel">{{ p.channel }}</span>
<span class="muted"> · {{ p.initiation_mode === 'seller' ? i18n.t('sellerInitiated') : i18n.t('customerInitiated') }}</span>
</div>
<div class="muted" style="margin-top:6px">{{ p.product_context }}</div>
<router-link v-if="p.my_outcome === 'not_tried'" :to="`/groups/${gid}/chat/${p.id}`" style="margin-top:auto">
<button class="primary" style="width:100%">{{ i18n.t('chat') }}</button>
</router-link>
<div v-else class="muted" style="margin-top:auto;font-size:12px"> Trained ({{ outcomeLabel(p.my_outcome) }})</div>
<template v-if="auth.isAdmin">
<router-link :to="`/groups/${gid}/chat/${p.id}`" style="margin-top:auto">
<button class="primary" style="width:100%">{{ i18n.t('chat') }}</button>
</router-link>
</template>
<template v-else>
<router-link v-if="p.my_outcome === 'not_tried'" :to="`/groups/${gid}/chat/${p.id}`" style="margin-top:auto">
<button class="primary" style="width:100%">{{ i18n.t('chat') }}</button>
</router-link>
<div v-else class="muted" style="margin-top:auto;font-size:12px"> {{ i18n.t('trained') }} ({{ outcomeLabel(p.my_outcome) }})</div>
</template>
</div>
</div>
</div>
@@ -34,6 +52,7 @@
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { api } from '../api'
import { auth } from '../store/auth'
import { i18n } from '../i18n'
const route = useRoute()
@@ -55,5 +74,11 @@ onMounted(load)
<style scoped>
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; }
.pcard { display: flex; flex-direction: column; min-height: 170px; }
.pcard { display: flex; flex-direction: column; min-height: 190px; }
.diff { margin: 8px 0; }
.star { color: #d8dbe3; }
.star.on { color: #f59e0b; }
.badge.won { background: #dcfce7; color: #166534; }
.badge.lost { background: #fee2e2; color: #991b1b; }
.badge.not_tried { background: #eef2ff; color: #4338ca; }
</style>

View File

@@ -2,33 +2,50 @@
<div>
<h2> {{ i18n.t('settings') }}</h2>
<!-- Profile -->
<div class="card">
<h3 style="margin-top:0">{{ i18n.t('account') }}</h3>
<label>{{ i18n.t('username') }}</label>
<h3 style="margin-top:0">👤 {{ i18n.t('profile') }}</h3>
<label>{{ i18n.t('username') }} <span class="muted">({{ i18n.t('readonly') }})</span></label>
<input :value="auth.user?.username || ''" disabled />
<label>{{ i18n.t('name') }}</label>
<input v-model="name" type="text" />
<label>{{ i18n.t('email') }}</label>
<input v-model="email" type="email" />
<label>{{ i18n.t('newPassword') }} <span class="muted">({{ i18n.t('optional') }})</span></label>
<input v-model="password" type="password" autocomplete="new-password" />
<div class="error" v-if="error" role="alert">{{ error }}</div>
<div class="ok" v-if="ok" role="status"> {{ ok }}</div>
<button class="primary" style="margin-top:16px" :disabled="busy" @click="save">
<span v-if="busy" class="spinner"></span>{{ i18n.t('save') }}
</button>
</div>
<div class="card" style="margin-top:16px">
<h3 style="margin-top:0">{{ i18n.t('language') }}</h3>
<div class="row">
<button :class="{ active: i18n.locale === 'th' }" @click="i18n.set('th')">ไทย</button>
<button :class="{ active: i18n.locale === 'en' }" @click="i18n.set('en')">English</button>
<div class="row" style="gap:10px;margin-top:16px">
<button class="primary" :disabled="busy" @click="saveProfile">
<span v-if="busy" class="spinner"></span>{{ i18n.t('saveProfile') }}
</button>
</div>
</div>
<!-- Password -->
<div class="card" style="margin-top:16px">
<h3 style="margin-top:0">🔒 {{ i18n.t('changePassword') }}</h3>
<label>{{ i18n.t('newPassword') }}</label>
<input v-model="password" type="password" autocomplete="new-password" />
<label>{{ i18n.t('confirmPassword') }}</label>
<input v-model="confirm" type="password" autocomplete="new-password" />
<div class="row" style="gap:10px;margin-top:16px">
<button class="primary" :disabled="pwBusy || !password || password !== confirm" @click="savePassword">
<span v-if="pwBusy" class="spinner"></span>{{ i18n.t('save') }}
</button>
</div>
</div>
<!-- Language -->
<div class="card" style="margin-top:16px">
<h3 style="margin-top:0">🌐 {{ i18n.t('language') }}</h3>
<div class="row">
<button :class="{ active: i18n.locale === 'th' }" @click="setLang('th')">ไทย</button>
<button :class="{ active: i18n.locale === 'en' }" @click="setLang('en')">English</button>
</div>
</div>
<div class="error" v-if="error" role="alert">{{ error }}</div>
<div class="ok" v-if="ok" role="status"> {{ ok }}</div>
</div>
</template>
@@ -38,22 +55,25 @@ import { auth } from '../store/auth'
import { api } from '../api'
import { i18n } from '../i18n'
const name = ref(auth.user?.name || '')
const email = ref(auth.user?.email || '')
const password = ref('')
const confirm = ref('')
const error = ref('')
const ok = ref('')
const busy = ref(false)
const username = auth.user?.username || auth.user?.id || ''
const pwBusy = ref(false)
async function save() {
async function saveProfile() {
error.value = ''
ok.value = ''
busy.value = true
try {
if (email.value || password.value) {
const body = { username, email: email.value }
if (password.value) body.password = password.value
const data = await api.setup(body)
const body = {}
if (name.value && name.value !== auth.user?.name) body.name = name.value
if (email.value && email.value !== auth.user?.email) body.email = email.value
if (Object.keys(body).length) {
const data = await api.updateProfile(body)
auth.user = data.user
ok.value = i18n.t('saved')
}
@@ -63,9 +83,32 @@ async function save() {
busy.value = false
}
}
async function savePassword() {
error.value = ''
ok.value = ''
if (password.value.length < 4) { error.value = i18n.t('passwordTooShort'); return }
if (password.value !== confirm.value) { error.value = i18n.t('passwordMismatch'); return }
pwBusy.value = true
try {
const username = auth.user?.username || auth.user?.id || ''
const data = await api.setup({ username, email: email.value || auth.user?.email, password: password.value })
auth.user = data.user
password.value = ''
confirm.value = ''
ok.value = i18n.t('saved')
} catch (e) {
error.value = e.message
} finally {
pwBusy.value = false
}
}
function setLang(l) { i18n.set(l) }
</script>
<style scoped>
.active { border-color: var(--accent); color: var(--accent); font-weight: 600; }
.ok { color: var(--green); font-size: 13px; margin-top: 8px; }
.ok { color: var(--green); font-size: 13px; margin-top: 10px; }
.error { margin-top: 10px; }
</style>

View File

@@ -1,30 +1,43 @@
<template>
<div>
<h2>🎯 {{ i18n.t('training') }}</h2>
<p class="muted">Pick a product group to practice closing a sale.</p>
<div class="row" style="align-items:center;margin-bottom:16px">
<h2 style="margin:0">🎯 {{ i18n.t('training') }}</h2>
<div style="margin-left:auto">
<router-link v-if="auth.isAdmin" to="/admin/new-group">
<button class="primary"> {{ i18n.t('addProduct') }}</button>
</router-link>
</div>
</div>
<p class="muted">{{ i18n.t('trainingSubtitle') }}</p>
<div v-if="loading" class="card" style="min-height:80px">
<div class="skeleton" style="height:50px"></div>
</div>
<div v-else-if="groups.length === 0" class="card empty-state">
<strong>No training groups available</strong>
<span>Ask an admin to create a persona group first.</span>
<strong>{{ i18n.t('noTraining') }}</strong>
<span v-if="auth.isAdmin">Click "{{ i18n.t('addProduct') }}" to create a persona group.</span>
<span v-else>Ask an admin to create a persona group first.</span>
</div>
<div class="grid">
<router-link
v-for="g in groups"
:key="g.id"
:to="`/groups/${g.id}/personas`"
:to="auth.isAdmin ? `/admin/groups/${g.id}/edit` : `/groups/${g.id}/personas`"
style="text-decoration:none"
>
<div class="card lift train-card">
<div class="row" style="justify-content:space-between">
<strong>{{ g.title }}</strong>
<span class="badge ready">ready</span>
<span class="badge" :class="g.status === 'ready' ? 'ready' : 'draft'">{{ g.status }}</span>
</div>
<div class="muted" style="margin:6px 0 12px">{{ productName(g) }}</div>
<div class="badge" :class="g.channel">{{ g.channel || 'line' }}</div>
<button class="primary" style="width:100%;margin-top:12px">{{ i18n.t('selectPersona') }}</button>
<div class="row" style="gap:8px">
<span class="badge" :class="g.channel || 'line'">{{ g.channel || 'line' }}</span>
<span class="muted">{{ personaCount(g) }} {{ i18n.t('personas').toLowerCase() }}</span>
</div>
<button class="primary" style="width:100%;margin-top:12px">
{{ auth.isAdmin ? (g.status === 'ready' ? i18n.t('managePersonas') : i18n.t('analyze')) : i18n.t('selectPersona') }}
</button>
</div>
</router-link>
</div>
@@ -34,6 +47,7 @@
<script setup>
import { onMounted, ref } from 'vue'
import { api } from '../api'
import { auth } from '../store/auth'
import { i18n } from '../i18n'
const groups = ref([])
@@ -42,11 +56,15 @@ const loading = ref(true)
function productName(g) {
return (g.sales_kit && g.sales_kit.productName) || (g.input && g.input.product) || ''
}
function personaCount(g) {
return (g.personas || g.persona_count || 0)
}
onMounted(async () => {
try {
const all = (await api.listGroups()).groups || []
groups.value = all.filter((g) => g.status === 'ready')
// Admins see all groups (incl. drafts to manage/analyze); trainees only ready ones.
groups.value = auth.isAdmin ? all : all.filter((g) => g.status === 'ready')
} finally {
loading.value = false
}
@@ -57,4 +75,8 @@ onMounted(async () => {
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; }
.train-card { display: flex; flex-direction: column; height: 100%; }
.badge.ready { background: #dcfce7; color: #166534; }
.badge.draft { background: #fef9c3; color: #854d0e; }
button.soft { background: #f1f3f9; border-color: transparent; }
.badge.facebook { background: #e0f2fe; color: #0369a1; }
.badge.line { background: #dcfce7; color: #15803d; }
</style>