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:
@@ -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: 'ส่ง',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user