222 lines
6.0 KiB
Vue
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>
|