- 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.
136 lines
6.8 KiB
Vue
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="เช่น ต้นทุนสูงเกินไป ระบบล้าสมัย"></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>
|