feat(ip): protect persona 'formula' — secret fields only super_admin sees/edits
IP protection so casual copying yields inferior results: - SECRET_PERSONA_FIELDS (pains/objections/negotiation_levers/opener/tolerance + pain rootCause/resolutionConditions): only super_admin can view/edit them. - list_personas/get_persona/update_persona/get_group strip these for role=admin (and hide sales_kit + pain-fit report from admins too). - update_persona rejects admin attempts to set secret fields (403). - PersonaForm hides the 'การขาย' recipe section for non-super-admin (shows locked note); auth.isSuperAdmin getter added. Rebuilt dist. Added test_ip_protection.
This commit is contained in:
@@ -14,6 +14,29 @@ from .helpers import ApiError, current_user, require_auth, require_roles
|
||||
|
||||
groups_bp = Blueprint("groups", __name__)
|
||||
|
||||
# Fields that encode the "formula"/process of a persona. Only super_admin may see/edit
|
||||
# them; admins get the persona but NOT these — so a casual copy yields inferior results.
|
||||
SECRET_PERSONA_FIELDS = {
|
||||
"pains", "objections", "negotiation_levers", "opener",
|
||||
"rootCause", "resolutionConditions", "tolerance",
|
||||
}
|
||||
|
||||
|
||||
def strip_secret_fields(persona: dict) -> dict:
|
||||
"""Return a copy of a persona with secret/process fields removed."""
|
||||
out = dict(persona)
|
||||
for f in SECRET_PERSONA_FIELDS:
|
||||
out.pop(f, None)
|
||||
pains = out.get("pains")
|
||||
if isinstance(pains, list):
|
||||
cleaned = []
|
||||
for p in pains:
|
||||
if isinstance(p, dict):
|
||||
p = {k: v for k, v in p.items() if k not in ("rootCause", "resolutionConditions")}
|
||||
cleaned.append(p)
|
||||
out["pains"] = cleaned
|
||||
return out
|
||||
|
||||
_ANALYZE_LOCKS: dict[str, threading.Lock] = {}
|
||||
_ANALYZE_GUARD = threading.Lock()
|
||||
|
||||
@@ -203,6 +226,12 @@ def get_group(gid: str):
|
||||
view["personas"] = [revealable_view(p) for p in group.get("personas", [])]
|
||||
view["sales_kit"] = None
|
||||
view["report"] = None
|
||||
elif actor.get("role") != "super_admin":
|
||||
# admin: see group, but not secret/process persona fields nor the sales-kit/report
|
||||
# (pain-fit analysis is the IP to protect).
|
||||
view["personas"] = [strip_secret_fields(p) for p in group.get("personas", [])]
|
||||
view["sales_kit"] = None
|
||||
view["report"] = None
|
||||
return jsonify({"group": view})
|
||||
|
||||
|
||||
@@ -216,8 +245,11 @@ def list_personas(gid: str):
|
||||
if group.get("status") != "ready":
|
||||
raise ApiError("group not ready", 403)
|
||||
personas = [revealable_view(p) for p in group.get("personas", [])]
|
||||
else:
|
||||
elif actor.get("role") == "super_admin":
|
||||
personas = group.get("personas", [])
|
||||
else:
|
||||
# admin: see persona but not the secret/process fields (IP protection)
|
||||
personas = [strip_secret_fields(p) for p in group.get("personas", [])]
|
||||
# attach per-user status (won/lost/not-tried) for trainees
|
||||
if actor.get("role") == "user":
|
||||
sess = _stores().get("session_store")
|
||||
@@ -244,7 +276,10 @@ def get_persona(gid: str, pid: str):
|
||||
ensure = ensure_persona_shape(p)
|
||||
if actor.get("role") == "user":
|
||||
return jsonify({"persona": revealable_view(ensure)})
|
||||
if actor.get("role") == "super_admin":
|
||||
return jsonify({"persona": ensure})
|
||||
# admin: hidden secret/process fields (IP protection)
|
||||
return jsonify({"persona": strip_secret_fields(ensure)})
|
||||
|
||||
|
||||
@groups_bp.put("/<gid>/personas/<pid>")
|
||||
@@ -254,13 +289,22 @@ def update_persona(gid: str, pid: str):
|
||||
s = _stores()
|
||||
_get_owned_group(s, gid)
|
||||
data = request.get_json(silent=True) or {}
|
||||
actor = current_user()
|
||||
# IP protection: only super_admin may set/alter secret formula fields.
|
||||
if actor.get("role") != "super_admin":
|
||||
for f in SECRET_PERSONA_FIELDS:
|
||||
if f in data:
|
||||
raise ApiError(f"field '{f}' is locked (super_admin only)", 403)
|
||||
try:
|
||||
updated = s["groups"].update_persona(gid, pid, data)
|
||||
except ValueError as exc:
|
||||
raise ApiError(str(exc), 404)
|
||||
return jsonify({"persona": ensure_persona_shape(updated["personas"][
|
||||
full = ensure_persona_shape(updated["personas"][
|
||||
next(i for i, p in enumerate(updated["personas"]) if p["id"] == pid)
|
||||
])})
|
||||
])
|
||||
if actor.get("role") == "super_admin":
|
||||
return jsonify({"persona": full})
|
||||
return jsonify({"persona": strip_secret_fields(full)})
|
||||
|
||||
|
||||
@groups_bp.post("/<gid>/reanalyze")
|
||||
|
||||
61
backend/scripts/test_ip_protection.py
Normal file
61
backend/scripts/test_ip_protection.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Test: IP protection — admin cannot see/edit secret persona fields; super_admin can."""
|
||||
import os, sys, tempfile, warnings
|
||||
from pathlib import Path
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
BACKEND = str(Path(__file__).resolve().parents[1])
|
||||
sys.path.insert(0, BACKEND)
|
||||
|
||||
from app.factory import create_app
|
||||
from app.config import Config
|
||||
|
||||
td = tempfile.mkdtemp()
|
||||
Config.DATA_DIR = Path(td)
|
||||
sys.path.insert(0, BACKEND + "/scripts")
|
||||
from mock_llm import MockLLM
|
||||
|
||||
app = create_app()
|
||||
app.extensions["llm"] = MockLLM()
|
||||
C = app.test_client()
|
||||
|
||||
def tok(u, p): return C.post("/api/auth/login", json={"username": u, "password": p}).get_json()["token"]
|
||||
|
||||
AT = tok("admin", "1234"); AH = {"Authorization": f"Bearer {AT}"}
|
||||
C.post("/api/auth/setup", headers=AH, json={"username": "admin", "email": "a@b.co", "password": "newpass"})
|
||||
AT = tok("admin", "newpass"); AH = {"Authorization": f"Bearer {AT}"}
|
||||
|
||||
gid = C.post("/api/groups", headers=AH, json={"product": "CRM", "segment": "SME", "channel": "line", "language": "th"}).get_json()["group"]["id"]
|
||||
C.post(f"/api/groups/{gid}/analyze", headers=AH)
|
||||
personas = C.get(f"/api/groups/{gid}/personas", headers=AH).get_json()["personas"] # super_admin sees all
|
||||
assert personas and "pains" in personas[0], "super_admin should see secret fields"
|
||||
print("[ok] super_admin sees secret fields")
|
||||
|
||||
# create an admin (not super) user
|
||||
C.post("/api/admin/users", headers=AH, json={"username": "adm", "name": "Adm", "password": "pppp", "role": "admin"})
|
||||
AT2 = tok("adm", "pppp"); AH2 = {"Authorization": f"Bearer {AT2}"}
|
||||
pid = personas[0]["id"]
|
||||
|
||||
# admin list_personas: secret fields stripped
|
||||
admin_list = C.get(f"/api/groups/{gid}/personas", headers=AH2).get_json()["personas"]
|
||||
assert "pains" not in admin_list[0] and "tolerance" not in admin_list[0], "admin list should strip secrets"
|
||||
print("[ok] admin list strips secret fields")
|
||||
|
||||
# admin update with a secret field -> 403
|
||||
r = C.put(f"/api/groups/{gid}/personas/{pid}", headers=AH2, json={"name": "X", "tolerance": 1})
|
||||
assert r.status_code == 403, r.get_json()
|
||||
print("[ok] admin cannot set secret field (403)")
|
||||
|
||||
# admin update of non-secret field -> ok, but response still strips secrets
|
||||
r = C.put(f"/api/groups/{gid}/personas/{pid}", headers=AH2, json={"name": "Edited Name"})
|
||||
assert r.status_code == 200, r.get_json()
|
||||
body = r.get_json()["persona"]
|
||||
assert body["name"] == "Edited Name"
|
||||
assert "pains" not in body, "admin update response should strip secrets"
|
||||
print("[ok] admin can edit non-secret field; response strips secrets")
|
||||
|
||||
# super_admin can update secret field
|
||||
r = C.put(f"/api/groups/{gid}/personas/{pid}", headers=AH, json={"tolerance": 5})
|
||||
assert r.status_code == 200, r.get_json()
|
||||
print("[ok] super_admin can edit secret field")
|
||||
|
||||
print("ALL IP-PROTECTION TESTS PASSED")
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as k,_ as h,n as b,a as r,b as e,l as m,u as o,m as l,t as n,i as u,w as p,v,E as U,g as f,F as V,s as M,x as w,r as y,o as i,B as C}from"./index-DOaJiTJw.js";import{U as z}from"./users-SQMCzZpr.js";/**
|
||||
import{c as k,_ as h,n as b,a as r,b as e,l as m,u as o,m as l,t as n,i as u,w as p,v,E as U,g as f,F as V,s as M,x as w,r as y,o as i,B as C}from"./index-BCax2GDh.js";import{U as z}from"./users-NPxa3eQ_.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as x,_ as L,n as M,a as p,b as s,l as i,u as a,m as c,t as e,i as n,p as w,w as f,v as b,q as U,g as N,F as R,s as j,x as A,y as B,r as m,h as E,o as y}from"./index-DOaJiTJw.js";import{U as P}from"./users-SQMCzZpr.js";import{P as S}from"./plus-CODydpxS.js";/**
|
||||
import{c as x,_ as L,n as M,a as p,b as s,l as i,u as a,m as c,t as e,i as n,p as w,w as f,v as b,q as U,g as N,F as R,s as j,x as A,y as B,r as m,h as E,o as y}from"./index-BCax2GDh.js";import{U as P}from"./users-NPxa3eQ_.js";import{P as S}from"./plus-B9zMVgv0.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as P,_ as R,n as G,x as S,a as o,l as k,p as D,u as a,b as s,t,i as n,F as T,s as L,m as g,B as w,g as h,w as K,v as $,d as H,k as J,r as d,A as O,C as U,y as Y,o as l}from"./index-DOaJiTJw.js";import{T as q}from"./target-QJk_fUDu.js";import{A as Q}from"./arrow-left-BUCak2lw.js";/**
|
||||
import{c as P,_ as R,n as G,x as S,a as o,l as k,p as D,u as a,b as s,t,i as n,F as T,s as L,m as g,B as w,g as h,w as K,v as $,d as H,k as J,r as d,A as O,C as U,y as Y,o as l}from"./index-BCax2GDh.js";import{T as q}from"./target-BFC5gbdb.js";import{A as Q}from"./arrow-left-diPpM4Dj.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as g,_ as M,a as f,l as d,p as V,b as e,u as l,m as r,t as s,i as o,w as u,v as y,E as k,g as C,y as z,r as v,x as B,j as A,o as x}from"./index-DOaJiTJw.js";import{A as P}from"./arrow-left-BUCak2lw.js";/**
|
||||
import{c as g,_ as M,a as f,l as d,p as V,b as e,u as l,m as r,t as s,i as o,w as u,v as y,E as k,g as C,y as z,r as v,x as B,j as A,o as x}from"./index-BCax2GDh.js";import{A as P}from"./arrow-left-diPpM4Dj.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
9
frontend/dist/assets/GroupEdit-Bq-uo5sC.js
vendored
9
frontend/dist/assets/GroupEdit-Bq-uo5sC.js
vendored
File diff suppressed because one or more lines are too long
1
frontend/dist/assets/GroupEdit-CFwXUOV9.css
vendored
Normal file
1
frontend/dist/assets/GroupEdit-CFwXUOV9.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.sec[data-v-cef61bd8]{margin-top:20px}.sec h4[data-v-cef61bd8]{margin:0 0 10px;padding-bottom:6px;border-bottom:1px solid var(--border)}.row[data-v-cef61bd8]{display:flex;gap:12px;flex-wrap:wrap}.f[data-v-cef61bd8]{flex:1;min-width:140px}label[data-v-cef61bd8]{font-size:13px;color:var(--muted);display:block;margin:10px 0 4px}input[data-v-cef61bd8],select[data-v-cef61bd8],textarea[data-v-cef61bd8]{width:100%}.actions[data-v-cef61bd8]{margin-top:20px}.locked[data-v-cef61bd8]{background:#fffaf5;border:1px dashed #d6c7a1;border-radius:12px;padding:12px 14px}.locked h4[data-v-cef61bd8]{color:#b45309}.grid[data-v-c682ddb6]{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:14px}.pcard[data-v-c682ddb6]{display:flex;flex-direction:column}.edit-btn[data-v-c682ddb6]{margin-top:auto;display:inline-flex;align-items:center;gap:6px}.star[data-v-c682ddb6]{color:#d8dbe3}.star.on[data-v-c682ddb6]{color:#f59e0b}.badge.tier-a[data-v-c682ddb6]{background:#dcfce7;color:#166534}.badge.tier-b[data-v-c682ddb6]{background:#fef9c3;color:#854d0e}.badge.tier-c[data-v-c682ddb6]{background:#fee2e2;color:#991b1b}.guide[data-v-c682ddb6]{background:#eef2ff;border-color:#c7d2fe;margin-bottom:16px}.modal-backdrop[data-v-c682ddb6]{position:fixed;top:0;right:0;bottom:0;left:0;background:#0f172a80;display:flex;justify-content:center;align-items:flex-start;padding:40px 16px;z-index:50;overflow:auto}.modal[data-v-c682ddb6]{background:#fff;border-radius:14px;padding:24px;width:100%;max-width:720px;box-shadow:0 20px 50px #00000040}
|
||||
1
frontend/dist/assets/GroupEdit-D9lmggXK.css
vendored
1
frontend/dist/assets/GroupEdit-D9lmggXK.css
vendored
@@ -1 +0,0 @@
|
||||
.sec[data-v-738758fc]{margin-top:20px}.sec h4[data-v-738758fc]{margin:0 0 10px;padding-bottom:6px;border-bottom:1px solid var(--border)}.row[data-v-738758fc]{display:flex;gap:12px;flex-wrap:wrap}.f[data-v-738758fc]{flex:1;min-width:140px}label[data-v-738758fc]{font-size:13px;color:var(--muted);display:block;margin:10px 0 4px}input[data-v-738758fc],select[data-v-738758fc],textarea[data-v-738758fc]{width:100%}.actions[data-v-738758fc]{margin-top:20px}.grid[data-v-c682ddb6]{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:14px}.pcard[data-v-c682ddb6]{display:flex;flex-direction:column}.edit-btn[data-v-c682ddb6]{margin-top:auto;display:inline-flex;align-items:center;gap:6px}.star[data-v-c682ddb6]{color:#d8dbe3}.star.on[data-v-c682ddb6]{color:#f59e0b}.badge.tier-a[data-v-c682ddb6]{background:#dcfce7;color:#166534}.badge.tier-b[data-v-c682ddb6]{background:#fef9c3;color:#854d0e}.badge.tier-c[data-v-c682ddb6]{background:#fee2e2;color:#991b1b}.guide[data-v-c682ddb6]{background:#eef2ff;border-color:#c7d2fe;margin-bottom:16px}.modal-backdrop[data-v-c682ddb6]{position:fixed;top:0;right:0;bottom:0;left:0;background:#0f172a80;display:flex;justify-content:center;align-items:flex-start;padding:40px 16px;z-index:50;overflow:auto}.modal[data-v-c682ddb6]{background:#fff;border-radius:14px;padding:24px;width:100%;max-width:720px;box-shadow:0 20px 50px #00000040}
|
||||
9
frontend/dist/assets/GroupEdit-PGVs7eZc.js
vendored
Normal file
9
frontend/dist/assets/GroupEdit-PGVs7eZc.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
import{c as e,a as o,b as l,l as n,u as i,m as s,t as r,i as a,D as u,o as d}from"./index-DOaJiTJw.js";import{B as g}from"./book-open-Ckxqlz0w.js";import{L as p}from"./layout-dashboard-CmUmS5d8.js";import{T as m}from"./target-QJk_fUDu.js";import{S as y}from"./sparkles-CwNig9QR.js";/**
|
||||
import{c as e,a as o,b as l,l as n,u as i,m as s,t as r,i as a,D as u,o as d}from"./index-BCax2GDh.js";import{B as g}from"./book-open-C7SxBVkN.js";import{L as p}from"./layout-dashboard-Djj77ueX.js";import{T as m}from"./target-BFC5gbdb.js";import{S as y}from"./sparkles-E3Xd6eY7.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as k,_ as x,a as y,b as e,t as l,u as a,i as n,w as h,v as M,d as w,e as B,f as _,g as E,r,h as f,j as S,k as V,o as s}from"./index-DOaJiTJw.js";/**
|
||||
import{c as k,_ as x,a as y,b as e,t as l,u as a,i as n,w as h,v as M,d as w,e as B,f as _,g as E,r,h as f,j as S,k as V,o as s}from"./index-BCax2GDh.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1 +1 @@
|
||||
import{_ as f,n as x,x as v,a as r,b as t,l as b,u as e,m as k,t as s,i as a,g as _,w as B,z as N,F as D,s as S,r as p,A as c,o as i,B as T}from"./index-DOaJiTJw.js";import{L as V}from"./layout-dashboard-CmUmS5d8.js";const z={style:{margin:"0"}},L={key:0,class:"row stat-row"},M={class:"card stat"},C={class:"card stat won"},F={class:"card stat lost"},j={class:"card stat"},A={key:1,class:"card"},E={key:2,class:"card empty-state"},G={class:"card",style:{"margin-top":"20px"}},I={style:{"margin-top":"0"}},q={__name:"MyBoard",setup(H){const o=p([]),u=p([]),m=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{m.value=!1}}),(n,l)=>(i(),r("div",null,[t("div",null,[t("h2",z,[b(e(V),{size:22,"stroke-width":1.8,style:{"vertical-align":"-4px"}}),k(" "+s(e(a).t("myDashboard")),1)]),l[0]||(l[0]=t("div",{class:"muted",style:{"margin-top":"4px","margin-bottom":"16px"}},"สรุปผลการฝึกของตัวคุณเอง — ดูว่าปิดการขายได้กี่ครั้ง ยังฝึกกับใครบ้าง",-1))]),m.value?_("",!0):(i(),r("div",L,[t("div",M,[t("span",null,s(e(a).t("total")),1),t("strong",null,s(y.value),1)]),t("div",C,[t("span",null,s(e(a).t("won")),1),t("strong",null,s(g.value),1)]),t("div",F,[t("span",null,s(e(a).t("lost")),1),t("strong",null,s(h.value),1)]),t("div",j,[t("span",null,s(e(a).t("notTried")),1),t("strong",null,s(w.value),1)])])),m.value?(i(),r("div",A,[...l[1]||(l[1]=[t("div",{class:"skeleton",style:{height:"50px"}},null,-1)])])):o.value.length===0?(i(),r("div",E,[...l[2]||(l[2]=[t("strong",null,"No practice yet",-1),t("span",null,"Go to Training and try closing a sale with a persona.",-1)])])):_("",!0),B(t("div",G,[t("h3",I,s(e(a).t("mySessions")),1),(i(!0),r(D,null,S(u.value,d=>(i(),r("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:T(["badge",d.outcome])},s(d.outcome==="won"?e(a).t("won"):e(a).t("lost")),3)]))),128))],512),[[N,u.value.length]])]))}},O=f(q,[["__scopeId","data-v-6767b6b8"]]);export{O as default};
|
||||
import{_ as f,n as x,x as v,a as r,b as t,l as b,u as e,m as k,t as s,i as a,g as _,w as B,z as N,F as D,s as S,r as p,A as c,o as i,B as T}from"./index-BCax2GDh.js";import{L as V}from"./layout-dashboard-Djj77ueX.js";const z={style:{margin:"0"}},L={key:0,class:"row stat-row"},M={class:"card stat"},C={class:"card stat won"},F={class:"card stat lost"},j={class:"card stat"},A={key:1,class:"card"},E={key:2,class:"card empty-state"},G={class:"card",style:{"margin-top":"20px"}},I={style:{"margin-top":"0"}},q={__name:"MyBoard",setup(H){const o=p([]),u=p([]),m=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{m.value=!1}}),(n,l)=>(i(),r("div",null,[t("div",null,[t("h2",z,[b(e(V),{size:22,"stroke-width":1.8,style:{"vertical-align":"-4px"}}),k(" "+s(e(a).t("myDashboard")),1)]),l[0]||(l[0]=t("div",{class:"muted",style:{"margin-top":"4px","margin-bottom":"16px"}},"สรุปผลการฝึกของตัวคุณเอง — ดูว่าปิดการขายได้กี่ครั้ง ยังฝึกกับใครบ้าง",-1))]),m.value?_("",!0):(i(),r("div",L,[t("div",M,[t("span",null,s(e(a).t("total")),1),t("strong",null,s(y.value),1)]),t("div",C,[t("span",null,s(e(a).t("won")),1),t("strong",null,s(g.value),1)]),t("div",F,[t("span",null,s(e(a).t("lost")),1),t("strong",null,s(h.value),1)]),t("div",j,[t("span",null,s(e(a).t("notTried")),1),t("strong",null,s(w.value),1)])])),m.value?(i(),r("div",A,[...l[1]||(l[1]=[t("div",{class:"skeleton",style:{height:"50px"}},null,-1)])])):o.value.length===0?(i(),r("div",E,[...l[2]||(l[2]=[t("strong",null,"No practice yet",-1),t("span",null,"Go to Training and try closing a sale with a persona.",-1)])])):_("",!0),B(t("div",G,[t("h3",I,s(e(a).t("mySessions")),1),(i(!0),r(D,null,S(u.value,d=>(i(),r("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:T(["badge",d.outcome])},s(d.outcome==="won"?e(a).t("won"):e(a).t("lost")),3)]))),128))],512),[[N,u.value.length]])]))}},O=f(q,[["__scopeId","data-v-6767b6b8"]]);export{O as default};
|
||||
@@ -1 +1 @@
|
||||
import{_ as T,n as $,a as r,l as d,p as m,b as t,t as s,u as e,i as n,h as f,m as _,g as w,F as y,s as p,k as L,x as N,y as V,r as b,o as a,S as I,B as h,f as A}from"./index-DOaJiTJw.js";import{T as S}from"./target-QJk_fUDu.js";import{A as F}from"./arrow-left-BUCak2lw.js";const j={class:"row",style:{"align-items":"center"}},D={style:{margin:"0"}},E={class:"muted",style:{"margin-left":"auto"}},M={key:0,class:"card guide"},R={key:1,class:"row",style:{margin:"12px 0",gap:"10px"}},q={class:"primary"},G={class:"grid"},H={class:"row",style:{"justify-content":"space-between"}},J={class:"diff"},K={class:"muted"},O={class:"muted"},Q={class:"muted"},U={class:"muted",style:{"margin-top":"6px"}},W={class:"primary",style:{width:"100%"}},X={class:"primary",style:{width:"100%"}},Y={key:1,class:"muted",style:{"margin-top":"auto","font-size":"12px"}},Z={__name:"Personas",setup(tt){const c=L().params.gid,k=b([]),B=b(!0);async function C(){try{k.value=(await N.listPersonas(c)).personas}finally{B.value=!1}}function z(l){return k.value.filter(i=>i.tier===l)}function P(l){return n.t(l==="A"?"tierA":l==="B"?"tierB":"tierC")}function v(l){return l==="won"?n.t("won"):l==="lost"?n.t("lost"):n.t("notTried")}return $(C),(l,i)=>{const u=V("router-link");return a(),r("div",null,[d(u,{to:"/training",class:"btn-back"},{default:m(()=>[d(e(F),{size:16,"stroke-width":2}),_(" "+s(e(n).t("training")),1)]),_:1}),t("div",j,[t("h2",D,s(e(n).t("personas")),1),t("span",E,s(e(n).t("selectPersona")),1)]),e(f).isAdmin?w("",!0):(a(),r("div",M,[t("strong",null,[d(e(S),{size:17,"stroke-width":1.8,style:{"vertical-align":"-3px"}}),i[0]||(i[0]=_(" วิธีฝึก",-1))]),i[1]||(i[1]=t("ol",{style:{margin:"8px 0 0","padding-left":"20px","line-height":"1.8"}},[t("li",null,"เลือกลูกค้าจำลอง (บุคคลต้นแบบ) คนหนึ่งที่อยากฝึกด้วย"),t("li",null,"ระดับ A ง่ายสุด → ระดับ C ยากสุด (ดูจากดาว ★ ความยาก)"),t("li",null,"กดปุ่มสำหรับฝึกแชทกับลูกค้าคนนั้น (ฝึกได้คนละครั้งเท่านั้น)")],-1))])),e(f).isAdmin?(a(),r("div",R,[d(u,{to:`/admin/groups/${e(c)}/edit`},{default:m(()=>[t("button",q,[d(e(I),{size:18,"stroke-width":1.8}),_(" "+s(e(n).t("managePersonas")),1)])]),_:1},8,["to"]),i[2]||(i[2]=t("span",{class:"muted"},"Admin: จัดการรายละเอียดบุคคลต้นแบบได้ที่นี่",-1))])):w("",!0),(a(),r(y,null,p(["A","B","C"],g=>t("div",{key:g,style:{margin:"20px 0"}},[t("h4",null,s(P(g)),1),t("div",G,[(a(!0),r(y,null,p(z(g),o=>(a(),r("div",{key:o.id,class:"card pcard lift"},[t("div",H,[t("strong",null,s(o.name),1),t("span",{class:h(["badge",o.my_outcome])},s(v(o.my_outcome)),3)]),t("div",J,[(a(),r(y,null,p(5,x=>t("span",{key:x,class:h(["star",{on:x<=(o.difficulty||1)}])},"★",2)),64)),t("span",K,s(e(n).t("difficulty"))+" "+s(o.difficulty||1)+"/5",1)]),t("div",O,[_(s(o.profession)+" · "+s(o.age_group)+" · "+s(o.location),1),i[3]||(i[3]=t("br",null,null,-1)),t("span",{class:h(["badge",o.channel])},s(o.channel),3),t("span",Q," · "+s(o.initiation_mode==="seller"?e(n).t("sellerInitiated"):e(n).t("customerInitiated")),1)]),t("div",U,s(o.product_context),1),e(f).isAdmin?(a(),A(u,{key:0,to:`/groups/${e(c)}/chat/${o.id}`,style:{"margin-top":"auto"}},{default:m(()=>[t("button",W,s(e(n).t("chat")),1)]),_:1},8,["to"])):(a(),r(y,{key:1},[o.my_outcome==="not_tried"?(a(),A(u,{key:0,to:`/groups/${e(c)}/chat/${o.id}`,style:{"margin-top":"auto"}},{default:m(()=>[t("button",X,s(e(n).t("chat")),1)]),_:1},8,["to"])):(a(),r("div",Y,"✓ "+s(e(n).t("trained"))+" ("+s(v(o.my_outcome))+")",1))],64))]))),128))])])),64))])}}},at=T(Z,[["__scopeId","data-v-46d50c99"]]);export{at as default};
|
||||
import{_ as T,n as $,a as r,l as d,p as m,b as t,t as s,u as e,i as n,h as f,m as _,g as w,F as y,s as p,k as L,x as N,y as V,r as b,o as a,S as I,B as h,f as A}from"./index-BCax2GDh.js";import{T as S}from"./target-BFC5gbdb.js";import{A as F}from"./arrow-left-diPpM4Dj.js";const j={class:"row",style:{"align-items":"center"}},D={style:{margin:"0"}},E={class:"muted",style:{"margin-left":"auto"}},M={key:0,class:"card guide"},R={key:1,class:"row",style:{margin:"12px 0",gap:"10px"}},q={class:"primary"},G={class:"grid"},H={class:"row",style:{"justify-content":"space-between"}},J={class:"diff"},K={class:"muted"},O={class:"muted"},Q={class:"muted"},U={class:"muted",style:{"margin-top":"6px"}},W={class:"primary",style:{width:"100%"}},X={class:"primary",style:{width:"100%"}},Y={key:1,class:"muted",style:{"margin-top":"auto","font-size":"12px"}},Z={__name:"Personas",setup(tt){const c=L().params.gid,k=b([]),B=b(!0);async function C(){try{k.value=(await N.listPersonas(c)).personas}finally{B.value=!1}}function z(l){return k.value.filter(i=>i.tier===l)}function P(l){return n.t(l==="A"?"tierA":l==="B"?"tierB":"tierC")}function v(l){return l==="won"?n.t("won"):l==="lost"?n.t("lost"):n.t("notTried")}return $(C),(l,i)=>{const u=V("router-link");return a(),r("div",null,[d(u,{to:"/training",class:"btn-back"},{default:m(()=>[d(e(F),{size:16,"stroke-width":2}),_(" "+s(e(n).t("training")),1)]),_:1}),t("div",j,[t("h2",D,s(e(n).t("personas")),1),t("span",E,s(e(n).t("selectPersona")),1)]),e(f).isAdmin?w("",!0):(a(),r("div",M,[t("strong",null,[d(e(S),{size:17,"stroke-width":1.8,style:{"vertical-align":"-3px"}}),i[0]||(i[0]=_(" วิธีฝึก",-1))]),i[1]||(i[1]=t("ol",{style:{margin:"8px 0 0","padding-left":"20px","line-height":"1.8"}},[t("li",null,"เลือกลูกค้าจำลอง (บุคคลต้นแบบ) คนหนึ่งที่อยากฝึกด้วย"),t("li",null,"ระดับ A ง่ายสุด → ระดับ C ยากสุด (ดูจากดาว ★ ความยาก)"),t("li",null,"กดปุ่มสำหรับฝึกแชทกับลูกค้าคนนั้น (ฝึกได้คนละครั้งเท่านั้น)")],-1))])),e(f).isAdmin?(a(),r("div",R,[d(u,{to:`/admin/groups/${e(c)}/edit`},{default:m(()=>[t("button",q,[d(e(I),{size:18,"stroke-width":1.8}),_(" "+s(e(n).t("managePersonas")),1)])]),_:1},8,["to"]),i[2]||(i[2]=t("span",{class:"muted"},"Admin: จัดการรายละเอียดบุคคลต้นแบบได้ที่นี่",-1))])):w("",!0),(a(),r(y,null,p(["A","B","C"],g=>t("div",{key:g,style:{margin:"20px 0"}},[t("h4",null,s(P(g)),1),t("div",G,[(a(!0),r(y,null,p(z(g),o=>(a(),r("div",{key:o.id,class:"card pcard lift"},[t("div",H,[t("strong",null,s(o.name),1),t("span",{class:h(["badge",o.my_outcome])},s(v(o.my_outcome)),3)]),t("div",J,[(a(),r(y,null,p(5,x=>t("span",{key:x,class:h(["star",{on:x<=(o.difficulty||1)}])},"★",2)),64)),t("span",K,s(e(n).t("difficulty"))+" "+s(o.difficulty||1)+"/5",1)]),t("div",O,[_(s(o.profession)+" · "+s(o.age_group)+" · "+s(o.location),1),i[3]||(i[3]=t("br",null,null,-1)),t("span",{class:h(["badge",o.channel])},s(o.channel),3),t("span",Q," · "+s(o.initiation_mode==="seller"?e(n).t("sellerInitiated"):e(n).t("customerInitiated")),1)]),t("div",U,s(o.product_context),1),e(f).isAdmin?(a(),A(u,{key:0,to:`/groups/${e(c)}/chat/${o.id}`,style:{"margin-top":"auto"}},{default:m(()=>[t("button",W,s(e(n).t("chat")),1)]),_:1},8,["to"])):(a(),r(y,{key:1},[o.my_outcome==="not_tried"?(a(),A(u,{key:0,to:`/groups/${e(c)}/chat/${o.id}`,style:{"margin-top":"auto"}},{default:m(()=>[t("button",X,s(e(n).t("chat")),1)]),_:1},8,["to"])):(a(),r("div",Y,"✓ "+s(e(n).t("trained"))+" ("+s(v(o.my_outcome))+")",1))],64))]))),128))])])),64))])}}},at=T(Z,[["__scopeId","data-v-46d50c99"]]);export{at as default};
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as M,_ as T,r as c,h as u,a as h,b as e,l as k,u as a,S as $,m as v,t as o,i as s,w as b,v as x,g as V,B as S,x as B,o as g}from"./index-DOaJiTJw.js";import{L as D}from"./lock-WqnzR3ED.js";/**
|
||||
import{c as M,_ as T,r as c,h as u,a as h,b as e,l as k,u as a,S as $,m as v,t as o,i as s,w as b,v as x,g as V,B as S,x as B,o as g}from"./index-BCax2GDh.js";import{L as D}from"./lock-uERjsrGr.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1 +1 @@
|
||||
import{_ as V,a as v,b as e,l as S,u as s,m as g,t,i as a,h as w,w as _,v as y,d as f,g as K,r,j as N,o as m}from"./index-DOaJiTJw.js";import{L as T}from"./lock-WqnzR3ED.js";const B={class:"setup-wrap"},U={class:"card setup-card"},C={class:"muted"},D={key:0,class:"error",role:"alert"},L=["disabled"],M={key:0,class:"spinner"},P={key:1},j={__name:"Setup",setup(z){const x=N(),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 w.finishSetup(i.value.trim(),l.value),x.push("/")}catch(h){o.value=h.message}finally{d.value=!1}}return(h,u)=>{var b,k;return m(),v("div",B,[e("div",U,[e("h1",null,[S(s(T),{size:22,"stroke-width":1.8,style:{"vertical-align":"-4px"}}),g(" "+t(s(a).t("setupTitle")),1)]),e("p",C,[g(t(s(a).t("setupSubtitle"))+" ",1),e("strong",null,t(((b=s(w).user)==null?void 0:b.name)||((k=s(w).user)==null?void 0:k.username)),1)]),e("label",null,t(s(a).t("email")),1),_(e("input",{"onUpdate:modelValue":u[0]||(u[0]=n=>i.value=n),type:"email",autocomplete:"email",class:"wide",onKeyup:f(c,["enter"])},null,544),[[y,i.value]]),e("label",null,t(s(a).t("newPassword")),1),_(e("input",{"onUpdate:modelValue":u[1]||(u[1]=n=>l.value=n),type:"password",autocomplete:"new-password",class:"wide",onKeyup:f(c,["enter"])},null,544),[[y,l.value]]),e("label",null,t(s(a).t("confirmPassword")),1),_(e("input",{"onUpdate:modelValue":u[2]||(u[2]=n=>p.value=n),type:"password",autocomplete:"new-password",class:"wide",onKeyup:f(c,["enter"])},null,544),[[y,p.value]]),o.value?(m(),v("div",D,t(o.value),1)):K("",!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",M)):(m(),v("span",P,t(s(a).t("save")),1))],8,L)])])}}},R=V(j,[["__scopeId","data-v-477fc775"]]);export{R as default};
|
||||
import{_ as V,a as v,b as e,l as S,u as s,m as g,t,i as a,h as w,w as _,v as y,d as f,g as K,r,j as N,o as m}from"./index-BCax2GDh.js";import{L as T}from"./lock-uERjsrGr.js";const B={class:"setup-wrap"},U={class:"card setup-card"},C={class:"muted"},D={key:0,class:"error",role:"alert"},L=["disabled"],M={key:0,class:"spinner"},P={key:1},j={__name:"Setup",setup(z){const x=N(),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 w.finishSetup(i.value.trim(),l.value),x.push("/")}catch(h){o.value=h.message}finally{d.value=!1}}return(h,u)=>{var b,k;return m(),v("div",B,[e("div",U,[e("h1",null,[S(s(T),{size:22,"stroke-width":1.8,style:{"vertical-align":"-4px"}}),g(" "+t(s(a).t("setupTitle")),1)]),e("p",C,[g(t(s(a).t("setupSubtitle"))+" ",1),e("strong",null,t(((b=s(w).user)==null?void 0:b.name)||((k=s(w).user)==null?void 0:k.username)),1)]),e("label",null,t(s(a).t("email")),1),_(e("input",{"onUpdate:modelValue":u[0]||(u[0]=n=>i.value=n),type:"email",autocomplete:"email",class:"wide",onKeyup:f(c,["enter"])},null,544),[[y,i.value]]),e("label",null,t(s(a).t("newPassword")),1),_(e("input",{"onUpdate:modelValue":u[1]||(u[1]=n=>l.value=n),type:"password",autocomplete:"new-password",class:"wide",onKeyup:f(c,["enter"])},null,544),[[y,l.value]]),e("label",null,t(s(a).t("confirmPassword")),1),_(e("input",{"onUpdate:modelValue":u[2]||(u[2]=n=>p.value=n),type:"password",autocomplete:"new-password",class:"wide",onKeyup:f(c,["enter"])},null,544),[[y,p.value]]),o.value?(m(),v("div",D,t(o.value),1)):K("",!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",M)):(m(),v("span",P,t(s(a).t("save")),1))],8,L)])])}}},R=V(j,[["__scopeId","data-v-477fc775"]]);export{R as default};
|
||||
@@ -1 +1 @@
|
||||
import{_ as w,n as b,x as B,h as d,a as l,b as t,l as c,u as s,m as _,t as e,i as n,p as m,f as h,g as f,F as C,s as A,r as g,y as N,o as i,B as k}from"./index-DOaJiTJw.js";import{T as P}from"./target-QJk_fUDu.js";import{B as T}from"./book-open-Ckxqlz0w.js";import{P as z}from"./plus-CODydpxS.js";const V={class:"row",style:{"align-items":"center","margin-bottom":"16px"}},F={style:{margin:"0"}},L={class:"row",style:{"margin-left":"auto",gap:"10px"}},S={class:"soft"},$={class:"primary"},j={class:"muted"},D={key:0,class:"card",style:{"min-height":"80px"}},E={key:1,class:"card empty-state"},G={key:0},I={key:1},M={class:"grid"},O={class:"card lift train-card"},q={class:"row",style:{"justify-content":"space-between"}},H={class:"muted",style:{margin:"6px 0 12px"}},J={class:"row",style:{gap:"8px"}},K={class:"muted"},Q={class:"primary",style:{width:"100%","margin-top":"12px"}},R={__name:"Training",setup(U){const u=g([]),y=g(!0);function v(a){return a.sales_kit&&a.sales_kit.productName||a.input&&a.input.product||""}function x(a){return a.personas||a.persona_count||0}return b(async()=>{try{const a=(await B.listGroups()).groups||[];u.value=d.isAdmin?a:a.filter(r=>r.status==="ready")}finally{y.value=!1}}),(a,r)=>{const p=N("router-link");return i(),l("div",null,[t("div",V,[t("h2",F,[c(s(P),{size:22,"stroke-width":1.8,style:{"vertical-align":"-4px"}}),_(" "+e(s(n).t("training")),1)]),t("div",L,[c(p,{to:"/guide"},{default:m(()=>[t("button",S,[c(s(T),{size:18,"stroke-width":1.8}),r[0]||(r[0]=_(" คู่มือ",-1))])]),_:1}),s(d).isAdmin?(i(),h(p,{key:0,to:"/admin/new-group"},{default:m(()=>[t("button",$,[c(s(z),{size:18,"stroke-width":2}),_(" "+e(s(n).t("addProduct")),1)])]),_:1})):f("",!0)])]),t("p",j,e(s(n).t("trainingSubtitle")),1),y.value?(i(),l("div",D,[...r[1]||(r[1]=[t("div",{class:"skeleton",style:{height:"50px"}},null,-1)])])):u.value.length===0?(i(),l("div",E,[t("strong",null,e(s(n).t("noTraining")),1),s(d).isAdmin?(i(),l("span",G,'Click "'+e(s(n).t("addProduct"))+'" to create a persona group.',1)):(i(),l("span",I,"Ask an admin to create a persona group first."))])):f("",!0),t("div",M,[(i(!0),l(C,null,A(u.value,o=>(i(),h(p,{key:o.id,to:s(d).isAdmin?`/admin/groups/${o.id}/edit`:`/groups/${o.id}/personas`,style:{"text-decoration":"none"}},{default:m(()=>[t("div",O,[t("div",q,[t("strong",null,e(o.title),1),t("span",{class:k(["badge",o.status==="ready"?"ready":"draft"])},e(o.status),3)]),t("div",H,e(v(o)),1),t("div",J,[t("span",{class:k(["badge",o.channel||"line"])},e(o.channel||"line"),3),t("span",K,e(x(o))+" "+e(s(n).t("personas").toLowerCase()),1)]),t("button",Q,e(s(d).isAdmin?o.status==="ready"?s(n).t("managePersonas"):s(n).t("analyze"):s(n).t("selectPersona")),1)])]),_:2},1032,["to"]))),128))])])}}},tt=w(R,[["__scopeId","data-v-e39df241"]]);export{tt as default};
|
||||
import{_ as w,n as b,x as B,h as d,a as l,b as t,l as c,u as s,m as _,t as e,i as n,p as m,f as h,g as f,F as C,s as A,r as g,y as N,o as i,B as k}from"./index-BCax2GDh.js";import{T as P}from"./target-BFC5gbdb.js";import{B as T}from"./book-open-C7SxBVkN.js";import{P as z}from"./plus-B9zMVgv0.js";const V={class:"row",style:{"align-items":"center","margin-bottom":"16px"}},F={style:{margin:"0"}},L={class:"row",style:{"margin-left":"auto",gap:"10px"}},S={class:"soft"},$={class:"primary"},j={class:"muted"},D={key:0,class:"card",style:{"min-height":"80px"}},E={key:1,class:"card empty-state"},G={key:0},I={key:1},M={class:"grid"},O={class:"card lift train-card"},q={class:"row",style:{"justify-content":"space-between"}},H={class:"muted",style:{margin:"6px 0 12px"}},J={class:"row",style:{gap:"8px"}},K={class:"muted"},Q={class:"primary",style:{width:"100%","margin-top":"12px"}},R={__name:"Training",setup(U){const u=g([]),y=g(!0);function v(a){return a.sales_kit&&a.sales_kit.productName||a.input&&a.input.product||""}function x(a){return a.personas||a.persona_count||0}return b(async()=>{try{const a=(await B.listGroups()).groups||[];u.value=d.isAdmin?a:a.filter(r=>r.status==="ready")}finally{y.value=!1}}),(a,r)=>{const p=N("router-link");return i(),l("div",null,[t("div",V,[t("h2",F,[c(s(P),{size:22,"stroke-width":1.8,style:{"vertical-align":"-4px"}}),_(" "+e(s(n).t("training")),1)]),t("div",L,[c(p,{to:"/guide"},{default:m(()=>[t("button",S,[c(s(T),{size:18,"stroke-width":1.8}),r[0]||(r[0]=_(" คู่มือ",-1))])]),_:1}),s(d).isAdmin?(i(),h(p,{key:0,to:"/admin/new-group"},{default:m(()=>[t("button",$,[c(s(z),{size:18,"stroke-width":2}),_(" "+e(s(n).t("addProduct")),1)])]),_:1})):f("",!0)])]),t("p",j,e(s(n).t("trainingSubtitle")),1),y.value?(i(),l("div",D,[...r[1]||(r[1]=[t("div",{class:"skeleton",style:{height:"50px"}},null,-1)])])):u.value.length===0?(i(),l("div",E,[t("strong",null,e(s(n).t("noTraining")),1),s(d).isAdmin?(i(),l("span",G,'Click "'+e(s(n).t("addProduct"))+'" to create a persona group.',1)):(i(),l("span",I,"Ask an admin to create a persona group first."))])):f("",!0),t("div",M,[(i(!0),l(C,null,A(u.value,o=>(i(),h(p,{key:o.id,to:s(d).isAdmin?`/admin/groups/${o.id}/edit`:`/groups/${o.id}/personas`,style:{"text-decoration":"none"}},{default:m(()=>[t("div",O,[t("div",q,[t("strong",null,e(o.title),1),t("span",{class:k(["badge",o.status==="ready"?"ready":"draft"])},e(o.status),3)]),t("div",H,e(v(o)),1),t("div",J,[t("span",{class:k(["badge",o.channel||"line"])},e(o.channel||"line"),3),t("span",K,e(x(o))+" "+e(s(n).t("personas").toLowerCase()),1)]),t("button",Q,e(s(d).isAdmin?o.status==="ready"?s(n).t("managePersonas"):s(n).t("analyze"):s(n).t("selectPersona")),1)])]),_:2},1032,["to"]))),128))])])}}},tt=w(R,[["__scopeId","data-v-e39df241"]]);export{tt as default};
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as e}from"./index-DOaJiTJw.js";/**
|
||||
import{c as e}from"./index-BCax2GDh.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-DOaJiTJw.js";/**
|
||||
import{c as a}from"./index-BCax2GDh.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
import{c as t}from"./index-DOaJiTJw.js";/**
|
||||
import{c as t}from"./index-BCax2GDh.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as e}from"./index-DOaJiTJw.js";/**
|
||||
import{c as e}from"./index-BCax2GDh.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as e}from"./index-DOaJiTJw.js";/**
|
||||
import{c as e}from"./index-BCax2GDh.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as a}from"./index-DOaJiTJw.js";/**
|
||||
import{c as a}from"./index-BCax2GDh.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c}from"./index-DOaJiTJw.js";/**
|
||||
import{c}from"./index-BCax2GDh.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
@@ -1,4 +1,4 @@
|
||||
import{c as e}from"./index-DOaJiTJw.js";/**
|
||||
import{c as e}from"./index-BCax2GDh.js";/**
|
||||
* @license lucide-vue-next v1.0.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
2
frontend/dist/index.html
vendored
2
frontend/dist/index.html
vendored
@@ -4,7 +4,7 @@
|
||||
<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-DOaJiTJw.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BCax2GDh.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B9WjPRES.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
<textarea v-model="d.communication_style"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Section: การขาย -->
|
||||
<div class="sec">
|
||||
<!-- Section: การขาย (secret — super_admin only) -->
|
||||
<div v-if="auth.isSuperAdmin" class="sec">
|
||||
<h4>🎯 การขาย</h4>
|
||||
<label>เป้าหมาย</label><textarea v-model="d.goal"></textarea>
|
||||
<label>กรอบเวลาในการตัดสินใจ</label><input v-model="d.decision_timeline" />
|
||||
@@ -50,6 +50,12 @@
|
||||
<label>ช่องทางสำหรับลูกค้าเปิดบทสนทนา (opener)</label>
|
||||
<textarea v-model="d.opener"></textarea>
|
||||
</div>
|
||||
<div v-else class="sec locked">
|
||||
<h4>🔒 ส่วนสูตรการขาย</h4>
|
||||
<p class="muted">ปิดการแก้ไข — เฉพาะผู้ดูแลระดับสูงเท่านั้นที่ดู/แก้ได้ (เพื่อรักษาความได้เปรียบ)</p>
|
||||
<label>เป้าหมาย</label><textarea v-model="d.goal"></textarea>
|
||||
<label>กรอบเวลาในการตัดสินใจ</label><input v-model="d.decision_timeline" />
|
||||
</div>
|
||||
|
||||
<!-- Section: ช่องทาง/โหมด -->
|
||||
<div class="sec">
|
||||
@@ -62,7 +68,7 @@
|
||||
<select v-model="d.initiation_mode"><option value="customer">ลูกค้าทักก่อน</option><option value="seller">เราต้องเริ่มขาย (เชิงรุก)</option></select>
|
||||
</div>
|
||||
</div>
|
||||
<label class="muted" v-if="d.special">พิเศษ: {{ d.special }}</label>
|
||||
<label class="muted" v-if="auth.isSuperAdmin && d.special">พิเศษ: {{ d.special }}</label>
|
||||
</div>
|
||||
|
||||
<div class="row actions">
|
||||
@@ -74,6 +80,7 @@
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { auth } from '../store/auth'
|
||||
|
||||
const props = defineProps({ persona: { type: Object, required: true } })
|
||||
const emit = defineEmits(['save', 'cancel'])
|
||||
@@ -126,4 +133,6 @@ function save() {
|
||||
label { font-size: 13px; color: var(--muted); display: block; margin: 10px 0 4px; }
|
||||
input, select, textarea { width: 100%; }
|
||||
.actions { margin-top: 20px; }
|
||||
.locked { background: #fffaf5; border: 1px dashed #d6c7a1; border-radius: 12px; padding: 12px 14px; }
|
||||
.locked h4 { color: #b45309; }
|
||||
</style>
|
||||
|
||||
@@ -12,6 +12,9 @@ export const auth = reactive({
|
||||
get isAdmin() {
|
||||
return this.role === 'admin' || this.role === 'super_admin'
|
||||
},
|
||||
get isSuperAdmin() {
|
||||
return this.role === 'super_admin'
|
||||
},
|
||||
async load() {
|
||||
if (!this.token) return null
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user