feat(training): delete group button (admin only) on Training page

- Backend DELETE /api/groups/<gid> (admin, own-org enforced via _get_owned_group);
  cascades removal of the group's sessions. GroupStore.delete added.
- Training.vue: each group card gets a trash (Trash2 line icon) delete button shown only
  to admins (auth.isAdmin); confirm dialog; @click.stop so it doesn't navigate. Card
  restructured so the delete button is not inside the clickable router-link.
- test_user_journey: trainee cannot delete (403), admin deletes, group gone (404).
All suites pass. Rebuilt dist.
This commit is contained in:
Macky
2026-08-09 10:20:10 +07:00
parent 0b92430aab
commit 5b167b4546
30 changed files with 114 additions and 43 deletions

View File

@@ -43,6 +43,7 @@ export const api = {
createGroup: (formData) => request('POST', '/api/groups', formData, true),
listGroups: () => request('GET', '/api/groups'),
getGroup: (id) => request('GET', `/api/groups/${id}`),
deleteGroup: (id) => request('DELETE', `/api/groups/${id}`),
analyzeGroup: (id, opts = {}) => request('POST', `/api/groups/${id}/analyze${opts.append ? '?append=true' : ''}`),
listPersonas: (gid) => request('GET', `/api/groups/${gid}/personas`),
getPersona: (gid, pid) => request('GET', `/api/groups/${gid}/personas/${pid}`),

View File

@@ -20,17 +20,30 @@
<span v-else>Ask an admin to create a persona group first.</span>
</div>
<div class="grid">
<router-link
v-for="g in groups"
:key="g.id"
:to="auth.isAdmin ? `/admin/groups/${g.id}/edit` : `/groups/${g.id}/personas`"
style="text-decoration:none"
>
<div class="card lift train-card">
<div class="row" style="justify-content:space-between">
<div v-for="g in groups" :key="g.id" class="card lift train-card">
<div class="row" style="justify-content:space-between">
<router-link
:to="auth.isAdmin ? `/admin/groups/${g.id}/edit` : `/groups/${g.id}/personas`"
style="text-decoration:none;color:inherit;min-width:0"
>
<strong>{{ 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"
>
<div class="muted" style="margin:6px 0 12px">{{ productName(g) }}</div>
<div class="row" style="gap:8px">
<span class="badge" :class="g.channel || 'line'">{{ g.channel || 'line' }}</span>
@@ -39,15 +52,15 @@
<button class="primary" style="width:100%;margin-top:12px">
{{ auth.isAdmin ? (g.status === 'ready' ? i18n.t('managePersonas') : i18n.t('analyze')) : i18n.t('selectPersona') }}
</button>
</div>
</router-link>
</router-link>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { Target, Plus, BookOpen } from 'lucide-vue-next'
import { Target, Plus, BookOpen, Trash2 } from 'lucide-vue-next'
import { api } from '../api'
import { auth } from '../store/auth'
import { i18n } from '../i18n'
@@ -62,6 +75,16 @@ 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(e.message)
}
}
onMounted(async () => {
try {
const all = (await api.listGroups()).groups || []
@@ -81,4 +104,10 @@ onMounted(async () => {
button.soft { background: #f1f3f9; border-color: transparent; }
.badge.facebook { background: #e0f2fe; color: #0369a1; }
.badge.line { background: #dcfce7; color: #15803d; }
.del-btn {
background: transparent; border: 1px solid var(--border); border-radius: 8px;
min-height: 32px; width: 32px; padding: 0; display: inline-flex; align-items: center;
justify-content: center; color: var(--muted); cursor: pointer;
}
.del-btn:hover { background: #fee2e2; border-color: #fecaca; color: var(--red); }
</style>