- Rewrite TH translations to professional/natural Thai (e.g. app 'ระบบฝึกทักษะการขาย' instead of 'ตัวฝึกขาย', 'บุคคลต้นแบบ' for persona, tiers 'พร้อมตัดสินใจซื้อ', 'จุดอ่อนที่ควรฝึกเพิ่มเติม', 'ปิดการขายได้/ไม่ได้' for won/lost, etc.) - Users button in admin overview now solid sky-blue (was faint .soft gray) so it's visible.
103 lines
4.6 KiB
Vue
103 lines
4.6 KiB
Vue
<template>
|
||
<div>
|
||
<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="users-btn">👥 {{ 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>Avg score</div><strong>{{ a.overall.avg_score }}</strong></div>
|
||
<div class="card stat"><div>Trainees</div><strong>{{ a.trainee_count }}</strong></div>
|
||
</div>
|
||
|
||
<!-- Close rate with progress bar -->
|
||
<div class="card">
|
||
<div class="row" style="justify-content:space-between;align-items:center">
|
||
<strong>{{ i18n.t('closeRate') }}</strong>
|
||
<strong>{{ a.overall.close_rate }}%</strong>
|
||
</div>
|
||
<div class="bar"><div class="bar-fill" :style="{ width: (a.overall.close_rate || 0) + '%' }"></div></div>
|
||
<div class="muted" style="font-size:12px;margin-top:6px">
|
||
{{ a.overall.wins }} {{ i18n.t('won').toLowerCase() }} / {{ a.overall.total_sessions }} sessions
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hardest personas -->
|
||
<h3 style="margin-top:20px">{{ 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>
|
||
|
||
<script setup>
|
||
import { onMounted, ref } from 'vue'
|
||
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: [] })
|
||
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; }
|
||
.bar { background: #eef0f5; border-radius: 999px; height: 10px; overflow: hidden; margin-top: 10px; }
|
||
.bar-fill { background: linear-gradient(90deg, var(--accent), var(--accent-2)); height: 100%; border-radius: 999px; transition: width .4s ease; }
|
||
.users-btn { background: #0ea5e9; border-color: transparent; color: #fff; font-weight: 600; }
|
||
.users-btn:not(:disabled):hover { background: #0284c7; box-shadow: 0 6px 16px rgba(14,165,233,.35); }
|
||
</style>
|