Files
sales-trainer/frontend/src/components/PersonaForm.vue
Macky cb2642a659 fix(ui): no raw JSON on Training list, pain no longer [object Object], add Chat button on GroupEdit, drop facebook/line from persona
- list_groups returns lightweight summaries (id/title/status/persona_count + product) —
  never the full personas array, so Training no longer shows a long JSON blob.
- PersonaForm toLines extracts .description/.text from pain/objection objects (fixes
  '[object Object]'); save re-wraps pains as {description}.
- GroupEdit persona cards get a 'แชท' (MessageSquare) button so you can start chatting
  right where you land after creating (chat was only on Personas page before).
- channel: default neutral 'social', removed facebook/line validation + removed from
  revealable_view (scenario now sets the tone, not fb/line).
All 9 backend suites pass. Rebuilt dist.
2026-08-09 11:15:13 +07:00

136 lines
6.8 KiB
Vue

<template>
<div class="persona-form">
<h3>{{ persona.name || 'New persona' }}</h3>
<p class="muted">{{ persona.profession }} · {{ persona.personality }}</p>
<!-- Section: อมลพนฐาน -->
<div class="sec">
<h4>👤 อมลพนฐาน</h4>
<div class="row">
<div class="f"><label></label><input v-model="d.name" /></div>
<div class="f"><label>ระดบความยาก (1-5)</label><input v-model.number="d.difficulty" type="number" min="1" max="5" /></div>
<div class="f"><label>ระดบลกค</label>
<select v-model="d.tier"><option value="A">A — พร้อมตัดสินใจซื้อ</option><option value="B">B — ยังไม่แน่ใจ</option><option value="C">C ไม่สนใจแต่มีปัญหา</option></select>
</div>
</div>
<div class="row">
<div class="f"><label>อาช</label><input v-model="d.profession" /></div>
<div class="f"><label>วงอาย</label><input v-model="d.age_group" /></div>
<div class="f"><label>นท</label><input v-model="d.location" /></div>
</div>
</div>
<!-- Section: อมลลกค (การขาย) -->
<div class="sec">
<h4>💼 อมลลกค</h4>
<div class="row">
<div class="f"><label>รายได/สถานะการเง</label><input v-model="d.income" /></div>
<div class="f"><label>งบประมาณ</label><input v-model="d.budget" /></div>
<div class="f"><label>ไลฟสไตล</label><input v-model="d.lifestyle" /></div>
</div>
<label>หล</label>
<textarea v-model="d.background"></textarea>
<label>คล / ดค</label>
<textarea v-model="d.personality"></textarea>
<label>สไตลการสอสาร</label>
<textarea v-model="d.communication_style"></textarea>
</div>
<!-- Section: การขาย (secret super_admin only) -->
<div v-if="auth.isSuperAdmin" class="sec">
<h4>🎯 การขาย</h4>
<label>เปาหมาย</label><textarea v-model="d.goal"></textarea>
<label>กรอบเวลาในการตดสนใจ</label><input v-model="d.decision_timeline" />
<label>Pain (ญหา) แบบ 1 อบรรท</label>
<textarea v-model="painsText" placeholder="เช่น ต้นทุนสูงเกินไป&#10;ระบบล้าสมัย"></textarea>
<label>อโตแย (Objections) 1 อบรรท</label>
<textarea v-model="objectionsText"></textarea>
<label>งทใชอรอง (เช วนลด, ฟรดต)</label>
<textarea v-model="negLeversText"></textarea>
<label>องทางสำหรบลกคาเปดบทสนทนา (opener)</label>
<textarea v-model="d.opener"></textarea>
</div>
<div v-else class="sec locked">
<h4>🔒 วนสตรการขาย</h4>
<p class="muted">ดการแกไข เฉพาะผแลระดบสงเทานนท/แกได (เพอรกษาความไดเปรยบ)</p>
<label>เปาหมาย</label><textarea v-model="d.goal"></textarea>
<label>กรอบเวลาในการตดสนใจ</label><input v-model="d.decision_timeline" />
</div>
<!-- Section: องทาง/โหมด (removed scenario chosen at chat time) -->
<div v-if="auth.isSuperAdmin && d.special" class="sec">
<label class="muted">เศษ: {{ d.special }}</label>
</div>
<div class="row actions">
<button class="primary" @click="save" :disabled="busy">{{ busy ? 'กำลังบันทึก…' : '💾 บันทึกบุคคลต้นแบบ' }}</button>
<button @click="$emit('cancel')">ปิด</button>
</div>
</div>
</template>
<script setup>
import { reactive, ref, watch } from 'vue'
import { auth } from '../store/auth'
const props = defineProps({ persona: { type: Object, required: true } })
const emit = defineEmits(['save', 'cancel'])
const d = reactive({
name: '', difficulty: 1, tier: 'B', profession: '', age_group: '', location: '',
income: '', budget: '', lifestyle: '', background: '', personality: '',
communication_style: '', goal: '', decision_timeline: '', opener: '', channel: 'line',
initiation_mode: 'customer', special: '',
})
const painsText = ref('')
const objectionsText = ref('')
const negLeversText = ref('')
const busy = ref(false)
function toLines(a) {
if (!a) return ''
if (Array.isArray(a)) {
return a.map((x) => (typeof x === 'string' ? x : (x && x.description) || (x && x.text) || '')).filter(Boolean).join('\n')
}
return String(a)
}
function fromLines(s) { return s.split('\n').map(x => x.trim()).filter(Boolean) }
watch(() => props.persona, (p) => {
Object.assign(d, {
name: p?.name || '', difficulty: p?.difficulty ?? 1, tier: p?.tier || 'B',
profession: p?.profession || '', age_group: p?.age_group || '', location: p?.location || '',
income: p?.income || '', budget: p?.budget || '', lifestyle: p?.lifestyle || '',
background: p?.background || '', personality: p?.personality || '',
communication_style: p?.communication_style || '', goal: p?.goal || '',
decision_timeline: p?.decision_timeline || '', opener: p?.opener || '',
channel: p?.channel || 'line', initiation_mode: p?.initiation_mode || 'customer',
special: p?.special || '',
})
painsText.value = toLines(p?.pains)
objectionsText.value = toLines(p?.objections)
negLeversText.value = toLines(p?.negotiation_levers)
}, { immediate: true })
function save() {
emit('save', {
...d,
pains: fromLines(painsText.value).map((t) => ({ description: t })),
objections: fromLines(objectionsText.value),
negotiation_levers: fromLines(negLeversText.value),
})
}
</script>
<style scoped>
.sec { margin-top: 20px; }
.sec h4 { margin: 0 0 10px; padding-bottom: 6px; border-bottom: 1px solid var(--border); }
.row { display: flex; gap: 12px; flex-wrap: wrap; }
.f { flex: 1; min-width: 140px; }
label { font-size: 13px; color: var(--muted); display: block; margin: 10px 0 4px; }
input, select, textarea { width: 100%; }
.actions { margin-top: 20px; }
.locked { background: #fffaf5; border: 1px dashed #d6c7a1; border-radius: 12px; padding: 12px 14px; }
.locked h4 { color: #b45309; }
</style>