Files
nademitex_user/src/views/reminders/ReminderDetailView.vue
T

403 lines
9.7 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useCrmStore } from '@/stores/crm'
import { useAuthStore } from '@/stores/auth'
import type { CrmCustomer, CrmMeeting, CrmReminder, MeetingInteractionType } from '@/types/crm'
import PageHeader from '@/components/layout/PageHeader.vue'
import PageSkeleton from '@/components/ui/PageSkeleton.vue'
const route = useRoute()
const router = useRouter()
const crm = useCrmStore()
const auth = useAuthStore()
const id = computed(() => String(route.query.id ?? ''))
const reminder = ref<CrmReminder | null>(null)
const customer = ref<CrmCustomer | null>(null)
const history = ref<CrmMeeting[]>([])
const loading = ref(true)
const saving = ref(false)
const error = ref<string | null>(null)
async function load() {
if (!id.value) {
reminder.value = null
loading.value = false
return
}
loading.value = true
error.value = null
try {
const data = (await crm.loadReminderDetail(id.value)) as {
reminder: CrmReminder
customer: CrmCustomer
history: CrmMeeting[]
}
reminder.value = data.reminder
customer.value = data.customer
history.value = data.history
} catch (e) {
reminder.value = null
error.value = e instanceof Error ? e.message : 'یادآور یافت نشد.'
} finally {
loading.value = false
}
}
onMounted(load)
watch(id, load)
function typeLabel(type: MeetingInteractionType) {
if (type === 'in_person') return 'ملاقات حضوری'
if (type === 'call') return 'تماس تلفنی'
if (type === 'message') return 'پیام'
return 'یادداشت'
}
async function complete() {
if (!reminder.value || reminder.value.completed) return
saving.value = true
error.value = null
try {
await crm.markReminderComplete(reminder.value.id)
reminder.value = { ...reminder.value, completed: true }
router.push('/reminders?filter=today')
} catch (e) {
error.value = e instanceof Error ? e.message : 'تکمیل یادآور ناموفق بود.'
} finally {
saving.value = false
}
}
const agentName = computed(() => auth.fullName || 'واسطه')
const interactionCounts = computed(() => {
const list = history.value
return {
total: list.length,
inPerson: list.filter((m) => m.type === 'in_person').length,
call: list.filter((m) => m.type === 'call').length,
other: list.filter((m) => m.type === 'message' || m.type === 'note').length,
}
})
const chartBars = computed(() => {
const bars = Array.from({ length: 6 }, () => ({ a: 0, b: 0, c: 0 }))
history.value.forEach((meeting, index) => {
const bar = bars[index % 6]
if (!bar) return
if (meeting.type === 'in_person') bar.a += 1
else if (meeting.type === 'call') bar.b += 1
else bar.c += 1
})
const max = Math.max(1, ...bars.map((b) => b.a + b.b + b.c))
return bars.map((b) => ({
a: `${(b.a / max) * 100}%`,
b: `${(b.b / max) * 100}%`,
c: `${(b.c / max) * 100}%`,
}))
})
</script>
<template>
<div v-if="reminder && customer" class="page reminder-detail">
<PageHeader title="جزئیات یادآور" show-back centered @back="router.back()" />
<div class="page__scroll detail-scroll">
<p v-if="error" class="form-error">{{ error }}</p>
<div class="layout">
<section class="panel">
<h2>جزئیات یادآور</h2>
<div class="field">
<span>عنوان</span>
<strong>{{ reminder.title }} — {{ reminder.description }}</strong>
</div>
<div class="field">
<span>مشتری</span>
<div class="customer-line">
<div class="avatar" :style="{ background: customer.avatarColor }">
{{ customer.avatarLetter }}
</div>
<div>
<strong>{{ customer.name }}</strong>
<p>{{ customer.contactName }}</p>
</div>
</div>
</div>
<div class="field">
<span>فروشنده حامل حضوری</span>
<strong>{{ agentName }}</strong>
</div>
<div class="field">
<span>تاریخ و زمان</span>
<strong>{{ reminder.date }} — {{ reminder.time }}</strong>
</div>
<div class="field">
<span>یادداشت</span>
<strong>{{ reminder.note || '—' }}</strong>
</div>
<div class="actions">
<button type="button" class="btn-outline" @click="router.back()">ویرایش</button>
<button
type="button"
class="btn-primary"
:disabled="saving || reminder.completed"
@click="complete"
>
{{ reminder.completed ? 'تکمیل‌شده' : saving ? '...' : 'تکمیل شد' }}
</button>
</div>
</section>
<section class="panel">
<h2>تاریخچه تعاملات</h2>
<article v-for="item in history" :key="item.id" class="history-row">
<div>
<strong>{{ typeLabel(item.type) }}</strong>
<p>{{ item.description }}</p>
</div>
<span>{{ item.date }}</span>
</article>
<button
type="button"
class="see-all"
@click="router.push({ path: '/customer', query: { id: customer.id, tab: 'reports' } })"
>
مشاهده همه
</button>
</section>
<section class="panel chart-panel">
<div class="chart-panel__head">
<h2>نمودار فعالیت</h2>
<span>تعاملات ثبت‌شده</span>
</div>
<div class="bars">
<div v-for="(bar, index) in chartBars" :key="index" class="bar">
<span v-if="bar.c !== '0%'" class="seg seg--c" :style="{ height: bar.c }" />
<span v-if="bar.b !== '0%'" class="seg seg--b" :style="{ height: bar.b }" />
<span v-if="bar.a !== '0%'" class="seg seg--a" :style="{ height: bar.a }" />
</div>
</div>
<div class="stats-grid">
<div><span>کل تعاملات</span><strong>{{ interactionCounts.total.toLocaleString('fa-IR') }} مورد</strong></div>
<div><span>ملاقات حضوری</span><strong>{{ interactionCounts.inPerson.toLocaleString('fa-IR') }} مورد</strong></div>
<div><span>تماس تلفنی</span><strong>{{ interactionCounts.call.toLocaleString('fa-IR') }} مورد</strong></div>
<div><span>یادداشت و پیام</span><strong>{{ interactionCounts.other.toLocaleString('fa-IR') }} مورد</strong></div>
</div>
</section>
</div>
</div>
</div>
<div v-else-if="loading" class="page empty">
<PageSkeleton variant="detail" />
</div>
<div v-else class="page empty">
<p>{{ error || 'یادآور یافت نشد.' }}</p>
</div>
</template>
<style scoped>
.reminder-detail {
background: var(--color-bg);
}
.detail-scroll {
padding: 12px var(--page-padding) 24px;
}
.form-error {
margin: 0 0 12px;
color: var(--color-danger, #b91c1c);
font-size: 0.85rem;
}
.layout {
display: flex;
flex-direction: column;
gap: 12px;
}
@media (min-width: 1024px) {
.layout {
display: grid;
grid-template-columns: 1.1fr 1fr 1fr;
align-items: start;
}
}
.panel {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 16px;
}
.panel h2 {
margin: 0 0 14px;
font-size: 1rem;
}
.field {
margin-bottom: 14px;
}
.field > span {
display: block;
color: var(--color-text-muted);
font-size: 0.78rem;
margin-bottom: 4px;
}
.customer-line {
display: flex;
gap: 10px;
align-items: center;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 999px;
display: grid;
place-items: center;
font-weight: 700;
}
.customer-line p {
margin: 2px 0 0;
color: var(--color-text-muted);
font-size: 0.8rem;
}
.actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 8px;
}
.btn-outline,
.btn-primary {
min-height: 44px;
border-radius: 12px;
font-weight: 700;
cursor: pointer;
}
.btn-outline {
border: 1px solid var(--color-border);
background: #fff;
}
.btn-primary {
border: none;
background: var(--color-primary);
color: #fff;
}
.btn-primary:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.history-row {
display: flex;
justify-content: space-between;
gap: 10px;
padding: 10px 0;
border-bottom: 1px solid var(--color-border);
}
.history-row p {
margin: 2px 0 0;
color: var(--color-text-muted);
font-size: 0.78rem;
}
.see-all {
width: 100%;
margin-top: 12px;
border: none;
border-radius: 12px;
background: #eef1ef;
min-height: 44px;
cursor: pointer;
font-weight: 600;
}
.chart-panel__head {
display: flex;
justify-content: space-between;
align-items: center;
}
.chart-panel__head span {
color: var(--color-text-muted);
font-size: 0.8rem;
}
.bars {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 8px;
height: 140px;
align-items: end;
margin: 16px 0;
}
.bar {
display: flex;
flex-direction: column-reverse;
gap: 2px;
height: 100%;
}
.seg {
display: block;
width: 100%;
border-radius: 4px 4px 0 0;
}
.seg--a {
background: var(--color-primary);
}
.seg--b {
background: #3b82f6;
}
.seg--c {
background: #a78bfa;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.stats-grid div {
background: var(--color-bg);
border-radius: 12px;
padding: 10px;
}
.stats-grid span {
display: block;
color: var(--color-text-muted);
font-size: 0.72rem;
}
.stats-grid strong {
display: block;
margin-top: 4px;
}
.empty {
padding: 40px;
text-align: center;
}
</style>