Files
sales-trainer/frontend/src/views/SessionDetail.vue
Macky 711e058b24 [verified] fix debrief pain display ([object Object]) + add 'create persona from this one' button
- formatValue(): render arrays of objects (e.g. pains) as readable text
  instead of '[object Object], [object Object]'
- SessionDetail: new 'สร้างบุคคลต้นแบบจากต้นแบบนี้' button calls the
  existing group persona-variant endpoint so trainees/orders can regenerate
  a persona based on the revealed one after a session
2026-08-18 22:01:27 +07:00

157 lines
6.5 KiB
Vue

<template>
<div>
<router-link to="/my/board" class="btn-back"> {{ i18n.t('myDashboard') }}</router-link>
<div v-if="loading" class="card"><div class="skeleton" style="height:120px"></div></div>
<div v-else-if="error" class="card error">{{ error }}</div>
<template v-else-if="session">
<div class="row" style="align-items:center;margin-bottom:12px">
<h2 style="margin:0">{{ session.persona_name || i18n.t('session') }}</h2>
<span class="badge" :class="statusClass">{{ statusLabel }}</span>
</div>
<div class="card">
<h3 style="margin-top:0">{{ i18n.t('mySessions') }}</h3>
<div class="thread">
<div
v-for="(message, index) in session.messages || []"
:key="index"
class="bubble"
:class="message.role === 'seller' ? 'msg-seller' : message.role === 'system' ? 'msg-system' : 'msg-customer'"
>{{ message.text }}</div>
<div v-if="!(session.messages || []).length" class="muted">ยังไม่มีข้อความใน session นี้</div>
</div>
</div>
<div v-if="session.debrief" class="card debrief">
<h3 style="margin-top:0">{{ i18n.t('debrief') }}</h3>
<p><span class="badge" :class="session.debrief.outcome">{{ outcomeLabel(session.debrief.outcome) }}</span>
{{ i18n.t('score') }}: <strong>{{ session.debrief.score }}</strong></p>
<p><strong>{{ i18n.t('pain') }}:</strong> {{ session.debrief.pain || '—' }}</p>
<p><strong>{{ i18n.t('why') }}:</strong> {{ session.debrief.why || '—' }}</p>
<div v-if="(session.debrief.failurePoints || []).length">
<strong>{{ i18n.t('failurePoints') }}</strong>
<ul><li v-for="(item, index) in session.debrief.failurePoints" :key="index">{{ item }}</li></ul>
</div>
<div v-if="(session.debrief.coaching || []).length">
<strong>{{ i18n.t('coaching') }}</strong>
<ul><li v-for="(item, index) in session.debrief.coaching" :key="index">{{ item }}</li></ul>
</div>
<details v-if="session.debrief.revealed_persona">
<summary>{{ i18n.t('reveal') }}</summary>
<div class="reveal-grid">
<div v-for="(value, key) in session.debrief.revealed_persona" :key="key" class="rev">
<span class="rk">{{ fieldLabel(key) }}</span>
<span class="rv">{{ formatValue(value) }}</span>
</div>
</div>
</details>
<button
v-if="session.group_id && session.persona_id"
class="soft variant-btn"
@click="makeVariant"
:disabled="creating"
>
{{ creating ? i18n.t('creating') : i18n.t('createVariant') }}
</button>
<div v-if="variantMsg" class="muted" style="margin-top:8px">{{ variantMsg }}</div>
</div>
</template>
</div>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { api } from '../api'
import { i18n } from '../i18n'
const route = useRoute()
const router = useRouter()
const session = ref(null)
const loading = ref(true)
const error = ref('')
const creating = ref(false)
const variantMsg = ref('')
const statusClass = computed(() => session.value?.status === 'active' ? 'active' : session.value?.outcome || 'not_tried')
const statusLabel = computed(() => {
if (!session.value || session.value.status === 'active') return i18n.t('inProgress')
return outcomeLabel(session.value.outcome)
})
function outcomeLabel(outcome) {
if (outcome === 'won') return i18n.t('won')
if (outcome === 'lost') return i18n.t('lost')
return i18n.t('inProgress')
}
function fieldLabel(key) {
const labels = {
pains: 'ปัญหา (Pain)', income: 'รายได้', personality: 'บุคลิก', budget: 'งบประมาณ',
negotiation_levers: 'สิ่งที่ใช้ต่อรอง', opener: 'บทเปิดบทสนทนา', background: 'ภูมิหลัง',
}
return labels[key] || key
}
function formatValue(value) {
if (Array.isArray(value)) {
// Arrays may hold plain strings OR objects (e.g. pains [{title,description}]).
// Never let join() stringify an object into "[object Object]".
return value
.map((item) => {
if (item && typeof item === 'object') {
const readable = item.description ?? item.title ?? item.text
return readable && typeof readable === 'string' && readable.trim()
? readable.trim()
: JSON.stringify(item)
}
return String(item)
})
.join(', ')
}
if (value && typeof value === 'object') return JSON.stringify(value)
return String(value == null ? '—' : value)
}
async function makeVariant() {
if (!session.value?.group_id || !session.value?.persona_id) return
creating.value = true
variantMsg.value = ''
try {
const created = await api.createPersonaVariant(session.value.group_id, session.value.persona_id)
const target = created && typeof created === 'object' && created.group_id
? created.group_id
: session.value.group_id
// Trainee variants land in the private group; admin variants in the shared group.
await router.push(`/groups/${target}/personas`)
} catch (e) {
variantMsg.value = i18n.t('requestFailed')
} finally {
creating.value = false
}
}
onMounted(async () => {
try {
session.value = (await api.getSession(route.params.sid)).session
} catch (e) {
error.value = i18n.t('requestFailed')
} finally {
loading.value = false
}
})
</script>
<style scoped>
.thread { background:#eceff4; border:1px solid var(--border); border-radius:var(--radius); padding:16px; min-height:220px; max-height:60vh; overflow-y:auto; display:flex; flex-direction:column; gap:8px; }
.bubble { max-width:72%; padding:10px 14px; white-space:pre-wrap; word-break:break-word; }
.msg-system { align-self:center; background:#fef3c7; color:#92400e; font-size:12px; max-width:88%; border-radius:999px; }
.debrief { margin-top:16px; }
.variant-btn { margin-top:14px; }
.reveal-grid { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:8px; }
.rev { display:flex; flex-direction:column; background:#f8fafc; border:1px solid var(--border); border-radius:8px; padding:8px 10px; }
.rk { font-size:12px; color:var(--muted); }
.rv { font-size:13px; color:var(--ink); margin-top:2px; }
.badge.active { background:#e0e7ff; color:#3730a3; }
@media (max-width:640px) { .reveal-grid { grid-template-columns:1fr; } }
</style>