Files
CRM_Montzar/apps/web/src/views/SmsCampaignReportView.vue
T
2026-10-05 11:56:49 +03:30

222 lines
6.0 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import TableColumnsMenu from '@/components/TableColumnsMenu.vue';
import { useRoute, useRouter } from 'vue-router';
import axios from 'axios';
import { api } from '@/lib/api';
import type { SmsCampaignReportDto } from '@mento/shared';
import {
UiButton,
UiCard,
UiEmptyState,
UiPageHeader,
} from '@/components/ui';
import UiIcon from '@/components/UiIcon.vue';
import { formatClockFa, formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
const route = useRoute();
const router = useRouter();
const loading = ref(true);
const error = ref('');
const report = ref<SmsCampaignReportDto | null>(null);
const id = computed(() => String(route.params.id || ''));
function maskMobile(m: string | null) {
if (!m) return '—';
const digits = m.replace(/\D/g, '');
if (digits.length < 6) return m;
return `${digits.slice(0, 4)}…${digits.slice(-2)}`;
}
function clickAt(iso: string | null) {
if (!iso) return '—';
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return '—';
return `${formatJalaliNumeric(d)} ${formatClockFa(d)}`;
}
function openContact(row: SmsCampaignReportDto['clickedUsers'][number]) {
if (row.personId) {
void router.push({ name: 'person-detail', params: { id: row.personId } });
return;
}
if (row.leadId) {
void router.push({ name: 'lead-detail', params: { id: row.leadId } });
}
}
async function load() {
loading.value = true;
error.value = '';
try {
const { data } = await api.get<SmsCampaignReportDto>(
`/sms-campaigns/${id.value}/report`,
);
report.value = data;
} catch (e) {
if (axios.isAxiosError(e) && e.response?.status === 403) {
error.value = 'ماژول پیامک گروهی برای این محیط فعال نیست.';
} else if (axios.isAxiosError(e) && e.response?.status === 404) {
error.value = 'کمپین پیدا نشد.';
} else {
error.value = 'بارگذاری گزارش ناموفق بود.';
}
} finally {
loading.value = false;
}
}
onMounted(() => {
void load();
});
</script>
<template>
<div class="page">
<UiPageHeader
:title="report?.campaign.name || 'گزارش کمپین'"
description="آمار ارسال و کلیک لینک‌های ردیابی‌شده"
>
<template #actions>
<UiButton
variant="secondary"
@click="router.push({ name: 'sms-campaigns' })"
>
بازگشت
</UiButton>
<UiButton variant="secondary" :disabled="loading" @click="load">
<UiIcon name="refresh" />
بروزرسانی
</UiButton>
</template>
</UiPageHeader>
<p v-if="error" class="error">{{ error }}</p>
<p v-if="loading" class="muted">در حال بارگذاری…</p>
<template v-if="report && !loading">
<p class="body muted">{{ report.campaign.body }}</p>
<div class="stats">
<UiCard title="ارسال‌شده">
<p class="stat">{{ toFaDigits(report.totalCount) }}</p>
</UiCard>
<UiCard title="ارسال موفق">
<p class="stat">{{ toFaDigits(report.sentCount) }}</p>
</UiCard>
<UiCard title="کلیک یکتا">
<p class="stat">{{ toFaDigits(report.uniqueClickers) }}</p>
</UiCard>
<UiCard title="نرخ کلیک">
<p class="stat">{{ toFaDigits(report.clickRate) }}٪</p>
</UiCard>
<UiCard title="کل کلیک">
<p class="stat">{{ toFaDigits(report.totalClicks) }}</p>
</UiCard>
<UiCard title="چندبار کلیک">
<p class="stat">{{ toFaDigits(report.multiClickers) }}</p>
</UiCard>
</div>
<UiCard title="کلیک‌کنندگان">
<UiEmptyState
v-if="!report.clickedUsers.length"
title="هنوز کلیکی ثبت نشده"
description="وقتی مخاطب روی لینک داخل پیامک بزند، اینجا دیده می‌شود."
/>
<div v-else class="table-wrap">
<div class="tc-row">
<TableColumnsMenu table-key="sms.campaign.clicks" align="end" />
</div>
<table v-table-cols="'sms.campaign.clicks'" class="table">
<thead>
<tr>
<th>مخاطب</th>
<th>نوع</th>
<th>شماره</th>
<th>اولین کلیک</th>
<th>آخرین کلیک</th>
<th>تعداد</th>
</tr>
</thead>
<tbody>
<tr
v-for="(row, idx) in report.clickedUsers"
:key="`${row.contactType}-${row.personId || row.leadId}-${idx}`"
class="row"
@click="openContact(row)"
>
<td>
<strong>{{ row.name }}</strong>
</td>
<td>
{{ row.contactType === 'lead' ? 'Lead' : 'Customer' }}
</td>
<td>{{ maskMobile(row.mobile) }}</td>
<td>{{ clickAt(row.firstClickedAt) }}</td>
<td>{{ clickAt(row.lastClickedAt) }}</td>
<td>{{ toFaDigits(row.clickCount) }}</td>
</tr>
</tbody>
</table>
</div>
</UiCard>
</template>
</div>
</template>
<style scoped>
.page {
display: grid;
gap: 1rem;
}
.error {
color: #b91c1c;
margin: 0;
}
.muted {
color: #64748b;
margin: 0;
}
.body {
white-space: pre-wrap;
line-height: 1.7;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 0.75rem;
}
.stat {
margin: 0;
font-size: 1.6rem;
font-weight: 800;
color: #0f766e;
}
.table-wrap {
overflow-x: auto;
}
.table {
width: 100%;
border-collapse: collapse;
font-size: 0.88rem;
}
.table th,
.table td {
text-align: right;
padding: 0.55rem 0.4rem;
border-bottom: 1px solid #e2e8f0;
}
.table th {
color: #64748b;
font-weight: 600;
}
.row {
cursor: pointer;
}
.row:hover {
background: #f0fdfa;
}
</style>