ux(ui): beginner-friendly guidance across trainee/admin flows + fix AdminUsers to username

- Chat: add 'How to practice' guide card; render revealed persona as readable
  label:value grid instead of JSON.
- AdminUsers: switch create form to username (matches username login), add step guide,
  role explanation (user=ฝึก, admin=ดูแลระบบ).
- Analytics (admin overview) + MyBoard (personal): add plain-Thai explainer subtitle.
- Rebuild dist. Verified user create with username -> 201.
This commit is contained in:
Macky
2026-08-08 10:34:24 +07:00
parent 39d1aa04e4
commit 22d3e51980
29 changed files with 103 additions and 38 deletions

View File

@@ -66,6 +66,8 @@ const messages = {
tierB: 'Tier B — Unsure',
tierC: 'Tier C — Not interested but has pain',
selectPersona: 'Select a persona to practice',
chatGuideTitle: 'How to practice',
chatGuideText: 'You are playing the salesperson. Chat with this customer and try to close the sale. Ask about their needs, solve their pain, and handle their objections. When you are done, press "Finish & get result" to see your score and coaching.',
chat: 'Chat',
start: 'Start',
send: 'Send',
@@ -149,6 +151,8 @@ const messages = {
tierB: 'ระดับ B — ยังไม่แน่ใจ',
tierC: 'ระดับ C — ไม่สนใจแต่มีปัญหา',
selectPersona: 'เลือกบุคคลต้นแบบเพื่อฝึก',
chatGuideTitle: 'วิธีฝึก',
chatGuideText: 'คุณรับบทเป็นพนักงานขาย พูดคุยกับลูกค้าคนนี้เพื่อพยายามปิดการขายให้ได้ สอบถามความต้องการ แก้ไขปัญหาของลูกค้า และรับมือกับข้อโต้แย้ง เมื่อพอใจแล้วกด "สรุปผล" เพื่อดูคะแนนและข้อเสนอแนะ',
chat: 'แชท',
start: 'เริ่มต้น',
send: 'ส่ง',

View File

@@ -1,23 +1,38 @@
<template>
<div>
<h2>{{ i18n.t('users') }}</h2>
<h2>👥 {{ i18n.t('users') }}</h2>
<div class="card guide">
<strong>📋 เพมผใชงาน</strong>
<ol style="margin:8px 0 0;padding-left:20px;line-height:1.8">
<li>กรอก <strong>อผใช (สำหรบเขาสระบบ)</strong>, , และรหสผานเรมต</li>
<li>เลอกสทธ: <strong>user</strong> = เขาฝกขาย, <strong>admin</strong> = ดการระบบ</li>
<li>กด <strong>สราง</strong> นำชอผใช + รหสผานไปแจงใหใชนเขาสระบบไดเลย</li>
</ol>
</div>
<div class="card" style="margin-bottom:16px">
<h4>+ {{ i18n.t('create') }} user</h4>
<h4 style="margin-top:0"> {{ i18n.t('create') }} {{ i18n.t('users').toLowerCase() }}</h4>
<div class="row">
<input v-model="form.name" placeholder="Name" style="flex:1" />
<input v-model="form.email" placeholder="Email" style="flex:1" />
<input v-model="form.password" type="password" placeholder="Temp password" style="flex:1" />
<input v-model="form.username" placeholder="ชื่อผู้ใช้ (เข้าสู่ระบบ)" style="flex:1" />
<input v-model="form.name" placeholder="ชื่อจริง" style="flex:1" />
<input v-model="form.password" type="password" placeholder="รหัสผ่านเริ่มต้น" style="flex:1" />
</div>
<div class="row" style="margin-top:10px">
<select v-model="form.role" style="flex:1">
<option value="user">user</option>
<option value="admin">admin</option>
<option value="user">🙂 user เขาฝ</option>
<option value="admin">🛠 admin แลระบบ</option>
</select>
<button class="primary" @click="create">{{ i18n.t('create') }}</button>
<button class="primary" @click="create" :disabled="!form.username || !form.password"> {{ i18n.t('create') }}</button>
</div>
<div class="error" v-if="error">{{ error }}</div>
</div>
<div v-if="users.length === 0" class="card empty-state">
<strong>งไมใชงาน</strong><span>สรางผใชงานคนแรกดวยฟอรมดานบน</span>
</div>
<div class="card" v-for="u in users" :key="u.id" style="margin-bottom:8px;display:flex;align-items:center;gap:12px">
<strong style="flex:1">{{ u.name }} ({{ u.email }})</strong>
<strong style="flex:1">{{ u.name }} ({{ u.username }})</strong>
<span class="badge">{{ u.role }}</span>
<span class="badge" :class="u.active ? 'won' : 'lost'">{{ u.active ? 'active' : 'inactive' }}</span>
</div>
@@ -31,16 +46,20 @@ import { i18n } from '../i18n'
const users = ref([])
const error = ref('')
const form = ref({ name: '', email: '', password: '', role: 'user' })
const form = ref({ username: '', name: '', password: '', role: 'user' })
async function load() { users.value = (await api.adminListUsers()).users }
async function create() {
error.value = ''
try {
await api.adminCreateUser({ ...form.value })
form.value = { name: '', email: '', password: '', role: 'user' }
form.value = { username: '', name: '', password: '', role: 'user' }
await load()
} catch (e) { error.value = e.message }
}
onMounted(load)
</script>
<style scoped>
.guide { background: #eef2ff; border-color: #c7d2fe; margin-bottom: 16px; }
</style>

View File

@@ -1,7 +1,10 @@
<template>
<div>
<div class="row" style="align-items:center;margin-bottom:16px">
<h2 style="margin:0">📊 {{ i18n.t('adminOverview') }}</h2>
<div>
<h2 style="margin:0">📊 {{ i18n.t('adminOverview') }}</h2>
<div class="muted" style="margin-top:4px">ภาพรวมผลการฝกของทมทงหมด เลอกชวงเวลาเพอดสถ</div>
</div>
<div class="row" style="margin-left:auto;gap:10px">
<router-link to="/admin/users"><button class="users-btn">👥 {{ i18n.t('users') }}</button></router-link>
<router-link to="/admin/new-group"><button class="primary"> {{ i18n.t('addProduct') }}</button></router-link>

View File

@@ -1,6 +1,13 @@
<template>
<div>
<router-link :to="`/groups/${gid}/personas`" class="btn-back"> {{ i18n.t('personas') }}</router-link>
<!-- How-to-train guide -->
<div class="card guide">
<strong>🎯 {{ i18n.t('chatGuideTitle') }}</strong>
<p style="margin:6px 0 0;line-height:1.7">{{ i18n.t('chatGuideText') }}</p>
</div>
<div class="row" style="align-items:center;margin-bottom:12px">
<h2 style="margin:0">{{ persona ? persona.name : '...' }}</h2>
<span class="badge" :class="persona && persona.channel">{{ persona ? persona.channel : '' }}</span>
@@ -42,8 +49,13 @@
<ul><li v-for="(c, i) in debrief.coaching" :key="i">{{ c }}</li></ul>
</div>
<details>
<summary>{{ i18n.t('reveal') }}</summary>
<pre class="json">{{ JSON.stringify(debrief.revealed_persona, null, 2) }}</pre>
<summary>🔎 {{ i18n.t('reveal') }}</summary>
<div class="reveal-grid" v-if="debrief.revealed_persona">
<div v-for="(v, k) in debrief.revealed_persona" :key="k" class="rev">
<span class="rk">{{ fieldLabel(k) }}</span>
<span class="rv">{{ fmt(v) }}</span>
</div>
</div>
</details>
<router-link to="/"><button class="primary" style="margin-top:12px">{{ i18n.t('dashboard') }}</button></router-link>
</div>
@@ -116,6 +128,23 @@ async function finish() {
sending.value = false
}
}
const FIELD_LABELS = {
name: 'ชื่อ', tier: 'ระดับ', difficulty: 'ความยาก', profession: 'อาชีพ',
age_group: 'ช่วงอายุ', location: 'พื้นที่', income: 'รายได้', budget: 'งบประมาณ',
lifestyle: 'ไลฟ์สไตล์', background: 'ภูมิหลัง', personality: 'บุคลิก',
communication_style: 'สไตล์การสื่อสาร', goal: 'เป้าหมาย', decision_timeline: 'กรอบตัดสินใจ',
pains: 'ปัญหา (Pain)', objections: 'ข้อโต้แย้ง', negotiation_levers: 'สิ่งที่ใช้ต่อรอง',
opener: 'บทเปิดบทสนทนา', channel: 'ช่องทาง', initiation_mode: 'ใครเริ่มก่อน', product_context: 'บริบทสินค้า',
}
function fieldLabel(k) {
return FIELD_LABELS[k] || k
}
function fmt(v) {
if (Array.isArray(v)) return v.join(', ')
if (v && typeof v === 'object') return JSON.stringify(v)
return String(v == null ? '—' : v)
}
</script>
<style scoped>
@@ -137,4 +166,10 @@ async function finish() {
.debrief { margin-top: 16px; }
.json { background: #0f172a; color: #9ca3af; padding: 10px; border-radius: 8px; font-size: 11px; overflow: auto; max-height: 260px; }
button.danger { background: var(--red); color: #fff; border: none; }
.guide { background: #eef2ff; border-color: #c7d2fe; margin-bottom: 12px; }
.reveal-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px; }
.rev { display: flex; flex-direction: column; background: #f8fafc; border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; }
.rk { font-size: 12px; color: var(--muted); }
.rv { font-size: 13px; color: var(--ink); margin-top: 2px; }
@media (max-width: 640px) { .reveal-grid { grid-template-columns: 1fr; } }
</style>

View File

@@ -1,6 +1,9 @@
<template>
<div>
<h2>📊 {{ i18n.t('myDashboard') }}</h2>
<div>
<h2 style="margin:0">📈 {{ i18n.t('myDashboard') }}</h2>
<div class="muted" style="margin-top:4px;margin-bottom:16px">สรปผลการฝกของตวคณเอง าปดการขายไดคร งฝกกบใครบาง</div>
</div>
<!-- Win/Lose summary -->
<div class="row stat-row" v-if="!loading">