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).
120 lines
5.3 KiB
Vue
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>
|