feat(ui): 3-tab layout (Admin overview / My dashboard / Training) + Settings page
- App.vue: top tab bar (Admin overview [admin], My dashboard, Training) + Settings icon - router: / = admin overview, /my/board, /training, /settings; non-admin '/' -> /my/board - New views: Training.vue (pick ready product group -> personas), MyBoard.vue (win/lose + sessions summary), Settings.vue (change email/password, language) - Backend: allow /api/me/board + /api/chat/sessions for any authenticated user (so the My dashboard tab works for admins too), not just 'user' role - i18n EN/TH keys; rebuilt frontend/dist Verified: build ok, /api/me/board + /api/chat/sessions + /api/groups all 200 for admin.
This commit is contained in:
61
frontend/src/views/MyBoard.vue
Normal file
61
frontend/src/views/MyBoard.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>📊 {{ i18n.t('myDashboard') }}</h2>
|
||||
|
||||
<!-- Win/Lose summary -->
|
||||
<div class="row stat-row" v-if="!loading">
|
||||
<div class="card stat"><span>{{ i18n.t('total') }}</span><strong>{{ total }}</strong></div>
|
||||
<div class="card stat won"><span>{{ i18n.t('won') }}</span><strong>{{ won }}</strong></div>
|
||||
<div class="card stat lost"><span>{{ i18n.t('lost') }}</span><strong>{{ lost }}</strong></div>
|
||||
<div class="card stat"><span>{{ i18n.t('notTried') }}</span><strong>{{ notTried }}</strong></div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="card"><div class="skeleton" style="height:50px"></div></div>
|
||||
<div v-else-if="items.length === 0" class="card empty-state">
|
||||
<strong>No practice yet</strong>
|
||||
<span>Go to Training and try closing a sale with a persona.</span>
|
||||
</div>
|
||||
|
||||
<!-- Recent sessions -->
|
||||
<div v-show="sessions.length" class="card" style="margin-top:20px">
|
||||
<h3 style="margin-top:0">{{ i18n.t('mySessions') }}</h3>
|
||||
<div v-for="s in sessions" :key="s.id" style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--border)">
|
||||
<span>{{ s.persona_name }}</span>
|
||||
<span class="badge" :class="s.outcome">{{ s.outcome === 'won' ? i18n.t('won') : i18n.t('lost') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { api } from '../api'
|
||||
import { i18n } from '../i18n'
|
||||
|
||||
const items = ref([])
|
||||
const sessions = ref([])
|
||||
const loading = ref(true)
|
||||
|
||||
const total = computed(() => items.value.length)
|
||||
const won = computed(() => items.value.filter((i) => i.my_outcome === 'won').length)
|
||||
const lost = computed(() => items.value.filter((i) => i.my_outcome === 'lost').length)
|
||||
const notTried = computed(() => items.value.filter((i) => i.my_outcome === 'not_tried').length)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
items.value = (await api.myBoard()).board || []
|
||||
sessions.value = (await api.mySessions()).sessions || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-row { gap: 16px; }
|
||||
.stat { text-align: center; min-width: 100px; }
|
||||
.stat span { display: block; font-size: 12px; color: var(--muted); }
|
||||
.stat strong { font-size: 26px; }
|
||||
.stat.won strong { color: var(--green); }
|
||||
.stat.lost strong { color: var(--red); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user