Files
sales-trainer/frontend/src/views/Training.vue
Macky c263cc81df feat(ui): app UX/UI redesign + marketing landing site
App (Vue SPA) — 100% presentational, global design-token rebuild in
style.css + polished App/Login/Chat/MyBoard/Personas/Setup/Training.
No logic/API/route/i18n changes (verified: build clean, 4/4 vitest,
independent review PASS 5/5).

website/ — new self-contained static marketing landing site (TH-primary
+ EN toggle, CSS chat mockup, Hallmark theme), verified responsive at
320-768 (no horizontal scroll). Pricing uses placeholders pending owner.

Docs: engineering-log 2026-08-19 + HANDOFF updated.

NOT pushed (push to main auto-deploys to EasyPanel production;
operator approval + JWT_SECRET check required before deploy).
2026-08-19 15:46:21 +07:00

120 lines
5.3 KiB
Vue

<template>
<div>
<div class="page-head">
<div>
<h2 class="page-title"><Target :size="22" :stroke-width="1.8" /> {{ i18n.t('training') }}</h2>
<div class="page-sub">{{ i18n.t('trainingSubtitle') }}</div>
</div>
<div class="row" style="gap:10px">
<router-link to="/guide"><button class="soft"><BookOpen :size="18" :stroke-width="1.8" /> </button></router-link>
<router-link v-if="auth.isAdmin" to="/admin/new-group">
<button class="primary"><Plus :size="18" :stroke-width="2" /> {{ i18n.t('addProduct') }}</button>
</router-link>
</div>
</div>
<div v-if="loading" class="card" style="min-height:80px">
<div class="skeleton" style="height:50px"></div>
</div>
<div v-else-if="groups.length === 0" class="card empty-state">
<span class="empty-illust"><PackageOpen :size="26" :stroke-width="1.6" /></span>
<strong>{{ i18n.t('noTraining') }}</strong>
<span v-if="auth.isAdmin">Click "{{ i18n.t('addProduct') }}" to create a persona group.</span>
<span v-else>Ask an admin to create a persona group first.</span>
</div>
<div class="grid">
<div v-for="g in groups" :key="g.id" class="card lift train-card">
<div class="row" style="justify-content:space-between;align-items:flex-start">
<router-link
:to="auth.isAdmin ? `/admin/groups/${g.id}/edit` : `/groups/${g.id}/personas`"
style="text-decoration:none;color:inherit;min-width:0"
>
<strong class="g-title">{{ g.title }}</strong>
</router-link>
<div class="row" style="gap:6px;align-items:center">
<span class="badge" :class="g.status === 'ready' ? 'ready' : 'draft'">{{ g.status }}</span>
<button
v-if="auth.isAdmin"
class="del-btn"
:title="'ลบกลุ่มนี้ (ติดลบถาวร)'"
@click.prevent.stop="removeGroup(g)"
>
<Trash2 :size="16" :stroke-width="1.8" />
</button>
</div>
</div>
<router-link
:to="auth.isAdmin ? `/admin/groups/${g.id}/edit` : `/groups/${g.id}/personas`"
style="text-decoration:none;color:inherit;flex:1;display:flex;flex-direction:column"
>
<div class="muted g-product">{{ productName(g) }}</div>
<div class="row" style="gap:8px;margin-top:auto;padding-top:10px">
<span class="persona-count"><Users :size="14" :stroke-width="1.8" /> {{ personaCount(g) }} {{ i18n.t('personas').toLowerCase() }}</span>
</div>
<button class="primary" style="width:100%;margin-top:14px">
{{ auth.isAdmin ? (g.status === 'ready' ? i18n.t('managePersonas') : i18n.t('analyze')) : i18n.t('selectPersona') }}
</button>
</router-link>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { Target, Plus, BookOpen, Trash2, Users, PackageOpen } from 'lucide-vue-next'
import { api } from '../api'
import { auth } from '../store/auth'
import { i18n } from '../i18n'
const groups = ref([])
const loading = ref(true)
function productName(g) {
return (g.sales_kit && g.sales_kit.productName) || (g.input && g.input.product) || ''
}
function personaCount(g) {
return (g.personas || g.persona_count || 0)
}
async function removeGroup(g) {
if (!confirm(`ลบกลุ่ม "${g.title}" และบุคคลต้นแบบทั้งหมด? (ติดลบถาวร)`)) return
try {
await api.deleteGroup(g.id)
groups.value = groups.value.filter((x) => x.id !== g.id)
} catch (e) {
alert(i18n.t('requestFailed'))
}
}
onMounted(async () => {
try {
const all = (await api.listGroups()).groups || []
// Admins see all groups (incl. drafts to manage/analyze); trainees only ready ones.
groups.value = auth.isAdmin ? all : all.filter((g) => g.status === 'ready')
} finally {
loading.value = false
}
})
</script>
<style scoped>
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; }
.train-card { display: flex; flex-direction: column; }
.g-title { font-size: var(--text-md); letter-spacing: var(--tracking-tight); display: inline-block; }
.g-product { margin: 6px 0 0; font-size: 13px; line-height: 1.5; }
.persona-count { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); font-size: 13px; font-weight: 600; }
.persona-count svg { color: var(--accent); }
.badge.ready { background: var(--green-soft); color: var(--green-strong); border: 1px solid #bbf7d0; }
.badge.draft { background: var(--amber-soft); color: var(--amber-strong); border: 1px solid #fde68a; }
button.soft { background: var(--accent-soft); border-color: transparent; color: var(--accent-strong); }
.badge.facebook { background: var(--sky-soft); color: var(--sky); border: 1px solid #bae6fd; }
.badge.line { background: var(--green-soft); color: var(--green-strong); border: 1px solid #bbf7d0; }
.del-btn {
background: transparent; border: 1px solid var(--border); border-radius: var(--radius-sm);
min-height: 36px; width: 36px; padding: 0; display: inline-flex; align-items: center;
justify-content: center; color: var(--muted); cursor: pointer;
}
.del-btn:hover { background: var(--red-soft); border-color: #fecaca; color: var(--red); }
</style>