feat: mvp
This commit is contained in:
@@ -1,9 +1,21 @@
|
||||
/* global axios */
|
||||
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class CampaignsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('campaigns', { accountScoped: true });
|
||||
}
|
||||
|
||||
analyticsMetrics(id) {
|
||||
return axios.get(`${this.url}/${id}/analytics/metrics`);
|
||||
}
|
||||
|
||||
analyticsContacts(id, { status, page } = {}) {
|
||||
return axios.get(`${this.url}/${id}/analytics/contacts`, {
|
||||
params: { status, page },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new CampaignsAPI();
|
||||
|
||||
@@ -42,9 +42,13 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
showAnalytics: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['edit', 'delete']);
|
||||
const emit = defineEmits(['edit', 'delete', 'analytics']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -116,7 +120,17 @@ const inboxIcon = computed(() => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end w-20 gap-2">
|
||||
<div class="flex items-center justify-end w-28 gap-2">
|
||||
<Button
|
||||
v-if="showAnalytics"
|
||||
v-tooltip.top="t('CAMPAIGN.WHATSAPP.CARD.ANALYTICS')"
|
||||
variant="faded"
|
||||
size="sm"
|
||||
color="slate"
|
||||
icon="i-lucide-chart-no-axes-column"
|
||||
:title="t('CAMPAIGN.WHATSAPP.CARD.ANALYTICS')"
|
||||
@click="emit('analytics')"
|
||||
/>
|
||||
<Button
|
||||
v-if="isLiveChatType"
|
||||
variant="faded"
|
||||
|
||||
+12
-1
@@ -1,5 +1,7 @@
|
||||
<script setup>
|
||||
import CampaignCard from 'dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue';
|
||||
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||
import { useConfig } from 'dashboard/composables/useConfig';
|
||||
|
||||
defineProps({
|
||||
campaigns: {
|
||||
@@ -12,10 +14,13 @@ defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['edit', 'delete']);
|
||||
const emit = defineEmits(['edit', 'delete', 'analytics']);
|
||||
const STATUS_COMPLETED = 'completed';
|
||||
const { isEnterprise } = useConfig();
|
||||
|
||||
const handleEdit = campaign => emit('edit', campaign);
|
||||
const handleDelete = campaign => emit('delete', campaign);
|
||||
const handleAnalytics = campaign => emit('analytics', campaign);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -31,8 +36,14 @@ const handleDelete = campaign => emit('delete', campaign);
|
||||
:inbox="campaign.inbox"
|
||||
:scheduled-at="campaign.scheduled_at"
|
||||
:is-live-chat-type="isLiveChatType"
|
||||
:show-analytics="
|
||||
isEnterprise &&
|
||||
campaign.inbox?.channel_type === INBOX_TYPES.WHATSAPP &&
|
||||
campaign.campaign_status === STATUS_COMPLETED
|
||||
"
|
||||
@edit="handleEdit(campaign)"
|
||||
@delete="handleDelete(campaign)"
|
||||
@analytics="handleAnalytics(campaign)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -145,6 +145,7 @@
|
||||
"SUBTITLE": "Launch a WhatsApp campaign to reach your customers directly. Send offers or make announcements with ease. Click 'Create campaign' to get started."
|
||||
},
|
||||
"CARD": {
|
||||
"ANALYTICS": "View analytics",
|
||||
"STATUS": {
|
||||
"COMPLETED": "Completed",
|
||||
"SCHEDULED": "Scheduled"
|
||||
@@ -199,6 +200,34 @@
|
||||
"ERROR_MESSAGE": "There was an error. Please try again."
|
||||
}
|
||||
}
|
||||
},
|
||||
"ANALYTICS": {
|
||||
"TITLE": "Campaign analytics",
|
||||
"BACK": "Back to WhatsApp campaigns",
|
||||
"EMPTY": "No delivery records found.",
|
||||
"EMPTY_FILTER": "No {status} delivery records found.",
|
||||
"SUMMARY": "{delivered} of {audience} contacts received this campaign. {skipped} skipped and {failed} failed.",
|
||||
"RATE": "{value} of audience",
|
||||
"PAGE_INFO": "Page {current} of {total}",
|
||||
"FILTERS": {
|
||||
"ALL": "All"
|
||||
},
|
||||
"METRICS": {
|
||||
"AUDIENCE": "Audience",
|
||||
"SENT": "Sent",
|
||||
"DELIVERED": "Delivered",
|
||||
"READ": "Read",
|
||||
"FAILED": "Failed",
|
||||
"SKIPPED": "Skipped"
|
||||
},
|
||||
"TABLE": {
|
||||
"CONTACT": "Contact",
|
||||
"PHONE_NUMBER": "Phone number",
|
||||
"STATUS": "Status",
|
||||
"MESSAGE": "Message",
|
||||
"MESSAGE_NOT_GENERATED": "Not generated",
|
||||
"ERROR_REASON": "Reason"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CONFIRM_DELETE": {
|
||||
|
||||
@@ -4,6 +4,7 @@ import CampaignsPageRouteView from './pages/CampaignsPageRouteView.vue';
|
||||
import LiveChatCampaignsPage from './pages/LiveChatCampaignsPage.vue';
|
||||
import SMSCampaignsPage from './pages/SMSCampaignsPage.vue';
|
||||
import WhatsAppCampaignsPage from './pages/WhatsAppCampaignsPage.vue';
|
||||
import WhatsAppCampaignAnalyticsPage from './pages/WhatsAppCampaignAnalyticsPage.vue';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
|
||||
const meta = {
|
||||
@@ -60,6 +61,15 @@ const campaignsRoutes = {
|
||||
},
|
||||
component: WhatsAppCampaignsPage,
|
||||
},
|
||||
{
|
||||
path: 'whatsapp/:campaignId/analytics',
|
||||
name: 'campaigns_whatsapp_analytics',
|
||||
meta: {
|
||||
...meta,
|
||||
featureFlag: FEATURE_FLAGS.WHATSAPP_CAMPAIGNS,
|
||||
},
|
||||
component: WhatsAppCampaignAnalyticsPage,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
+378
@@ -0,0 +1,378 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useStore, useStoreGetters } from 'dashboard/composables/store';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import CampaignsAPI from 'dashboard/api/campaigns';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const getters = useStoreGetters();
|
||||
|
||||
const state = reactive({
|
||||
metrics: null,
|
||||
contacts: [],
|
||||
meta: {},
|
||||
status: 'all',
|
||||
page: 1,
|
||||
isFetchingMetrics: false,
|
||||
isFetchingContacts: false,
|
||||
});
|
||||
|
||||
const campaignId = computed(() => Number(route.params.campaignId));
|
||||
const campaign = computed(() =>
|
||||
getters['campaigns/getAllCampaigns'].value.find(
|
||||
record => Number(record.id) === campaignId.value
|
||||
)
|
||||
);
|
||||
|
||||
const statusOptions = computed(() => [
|
||||
{ key: 'all', label: t('CAMPAIGN.WHATSAPP.ANALYTICS.FILTERS.ALL') },
|
||||
{ key: 'sent', label: t('CAMPAIGN.WHATSAPP.ANALYTICS.METRICS.SENT') },
|
||||
{
|
||||
key: 'delivered',
|
||||
label: t('CAMPAIGN.WHATSAPP.ANALYTICS.METRICS.DELIVERED'),
|
||||
},
|
||||
{ key: 'read', label: t('CAMPAIGN.WHATSAPP.ANALYTICS.METRICS.READ') },
|
||||
{ key: 'failed', label: t('CAMPAIGN.WHATSAPP.ANALYTICS.METRICS.FAILED') },
|
||||
{ key: 'skipped', label: t('CAMPAIGN.WHATSAPP.ANALYTICS.METRICS.SKIPPED') },
|
||||
]);
|
||||
|
||||
const metricItems = computed(() => [
|
||||
{
|
||||
key: 'audience',
|
||||
label: t('CAMPAIGN.WHATSAPP.ANALYTICS.METRICS.AUDIENCE'),
|
||||
showRate: false,
|
||||
},
|
||||
{
|
||||
key: 'sent',
|
||||
label: t('CAMPAIGN.WHATSAPP.ANALYTICS.METRICS.SENT'),
|
||||
showRate: true,
|
||||
},
|
||||
{
|
||||
key: 'delivered',
|
||||
label: t('CAMPAIGN.WHATSAPP.ANALYTICS.METRICS.DELIVERED'),
|
||||
showRate: true,
|
||||
},
|
||||
{
|
||||
key: 'read',
|
||||
label: t('CAMPAIGN.WHATSAPP.ANALYTICS.METRICS.READ'),
|
||||
showRate: true,
|
||||
},
|
||||
{
|
||||
key: 'failed',
|
||||
label: t('CAMPAIGN.WHATSAPP.ANALYTICS.METRICS.FAILED'),
|
||||
showRate: true,
|
||||
},
|
||||
{
|
||||
key: 'skipped',
|
||||
label: t('CAMPAIGN.WHATSAPP.ANALYTICS.METRICS.SKIPPED'),
|
||||
showRate: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const hasNextPage = computed(() => state.page < Number(state.meta.total_pages));
|
||||
const hasPreviousPage = computed(() => state.page > 1);
|
||||
const audienceCount = computed(() => Number(state.metrics?.audience || 0));
|
||||
|
||||
const insightSummary = computed(() =>
|
||||
t('CAMPAIGN.WHATSAPP.ANALYTICS.SUMMARY', {
|
||||
delivered: state.metrics?.delivered || 0,
|
||||
audience: audienceCount.value,
|
||||
skipped: state.metrics?.skipped || 0,
|
||||
failed: state.metrics?.failed || 0,
|
||||
})
|
||||
);
|
||||
|
||||
const filterLabel = option => {
|
||||
const count =
|
||||
option.key === 'all'
|
||||
? audienceCount.value
|
||||
: Number(state.metrics?.[option.key] || 0);
|
||||
|
||||
return `${option.label} ${count}`;
|
||||
};
|
||||
|
||||
const metricRate = key => {
|
||||
if (!audienceCount.value) return '0%';
|
||||
|
||||
return `${Math.round((Number(state.metrics?.[key] || 0) / audienceCount.value) * 100)}%`;
|
||||
};
|
||||
|
||||
const fetchMetrics = async () => {
|
||||
state.isFetchingMetrics = true;
|
||||
try {
|
||||
const response = await CampaignsAPI.analyticsMetrics(campaignId.value);
|
||||
state.metrics = response.data;
|
||||
} finally {
|
||||
state.isFetchingMetrics = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchContacts = async () => {
|
||||
state.isFetchingContacts = true;
|
||||
try {
|
||||
const response = await CampaignsAPI.analyticsContacts(campaignId.value, {
|
||||
status: state.status === 'all' ? undefined : state.status,
|
||||
page: state.page,
|
||||
});
|
||||
state.contacts = response.data.payload;
|
||||
state.meta = response.data.meta;
|
||||
} finally {
|
||||
state.isFetchingContacts = false;
|
||||
}
|
||||
};
|
||||
|
||||
const setStatus = status => {
|
||||
state.status = status;
|
||||
state.page = 1;
|
||||
};
|
||||
|
||||
const goToPreviousPage = () => {
|
||||
if (!hasPreviousPage.value) return;
|
||||
state.page -= 1;
|
||||
};
|
||||
|
||||
const goToNextPage = () => {
|
||||
if (!hasNextPage.value) return;
|
||||
state.page += 1;
|
||||
};
|
||||
|
||||
const statusLabel = status =>
|
||||
statusOptions.value.find(option => option.key === status)?.label || status;
|
||||
|
||||
const errorReason = delivery =>
|
||||
delivery.error_message || delivery.error_title || delivery.error_code || '-';
|
||||
|
||||
const messageContent = delivery => {
|
||||
if (delivery.message_content) return delivery.message_content;
|
||||
if (delivery.status === 'skipped') {
|
||||
return t('CAMPAIGN.WHATSAPP.ANALYTICS.TABLE.MESSAGE_NOT_GENERATED');
|
||||
}
|
||||
|
||||
return '-';
|
||||
};
|
||||
|
||||
const emptyStateMessage = computed(() => {
|
||||
if (state.status === 'all') return t('CAMPAIGN.WHATSAPP.ANALYTICS.EMPTY');
|
||||
|
||||
return t('CAMPAIGN.WHATSAPP.ANALYTICS.EMPTY_FILTER', {
|
||||
status: statusLabel(state.status).toLowerCase(),
|
||||
});
|
||||
});
|
||||
|
||||
const statusBadgeClass = status =>
|
||||
({
|
||||
sent: 'bg-n-blue-3 text-n-blue-11',
|
||||
delivered: 'bg-n-teal-3 text-n-teal-11',
|
||||
read: 'bg-n-iris-3 text-n-iris-11',
|
||||
failed: 'bg-n-ruby-3 text-n-ruby-11',
|
||||
skipped: 'bg-n-amber-3 text-n-amber-11',
|
||||
})[status] || 'bg-n-slate-3 text-n-slate-11';
|
||||
|
||||
const goBack = () => {
|
||||
router.push({ name: 'campaigns_whatsapp_index' });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('campaigns/get');
|
||||
fetchMetrics();
|
||||
fetchContacts();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [state.status, state.page],
|
||||
() => fetchContacts()
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="flex h-full flex-col overflow-hidden bg-n-surface-1">
|
||||
<header
|
||||
class="sticky top-0 z-10 border-b border-n-weak bg-n-surface-1 px-6"
|
||||
>
|
||||
<div class="mx-auto flex h-20 w-full max-w-7xl items-center gap-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="slate"
|
||||
size="sm"
|
||||
icon="i-lucide-arrow-left"
|
||||
:title="t('CAMPAIGN.WHATSAPP.ANALYTICS.BACK')"
|
||||
@click="goBack"
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<h1 class="text-heading-1 text-n-slate-12">
|
||||
{{ t('CAMPAIGN.WHATSAPP.ANALYTICS.TITLE') }}
|
||||
</h1>
|
||||
<p class="mt-1 truncate text-sm text-n-slate-11">
|
||||
{{ campaign?.title || `#${campaignId}` }}
|
||||
</p>
|
||||
<div
|
||||
v-if="campaign?.inbox?.name"
|
||||
class="mt-1 flex flex-wrap items-center gap-2 text-xs text-n-slate-10"
|
||||
>
|
||||
<span>{{ campaign.inbox.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex-1 overflow-y-auto px-6 py-6">
|
||||
<div class="mx-auto w-full max-w-7xl">
|
||||
<div
|
||||
v-if="state.isFetchingMetrics"
|
||||
class="flex h-24 items-center justify-center text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
<div v-else class="grid grid-cols-2 gap-3 md:grid-cols-6">
|
||||
<div
|
||||
v-for="item in metricItems"
|
||||
:key="item.key"
|
||||
class="rounded-lg border border-n-weak bg-n-alpha-2 p-4"
|
||||
>
|
||||
<div class="text-sm font-medium text-n-slate-11">
|
||||
{{ item.label }}
|
||||
</div>
|
||||
<div class="mt-3 text-2xl font-semibold text-n-slate-12">
|
||||
{{ state.metrics?.[item.key] || 0 }}
|
||||
</div>
|
||||
<div
|
||||
v-if="item.showRate"
|
||||
class="mt-1 text-xs font-medium text-n-slate-10"
|
||||
>
|
||||
{{
|
||||
t('CAMPAIGN.WHATSAPP.ANALYTICS.RATE', {
|
||||
value: metricRate(item.key),
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="state.metrics"
|
||||
class="mt-4 rounded-lg border border-n-weak bg-n-alpha-1 px-4 py-3 text-sm text-n-slate-11"
|
||||
>
|
||||
{{ insightSummary }}
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex flex-wrap gap-2">
|
||||
<Button
|
||||
v-for="option in statusOptions"
|
||||
:key="option.key"
|
||||
size="sm"
|
||||
:variant="state.status === option.key ? 'solid' : 'faded'"
|
||||
:color="state.status === option.key ? 'blue' : 'slate'"
|
||||
:label="filterLabel(option)"
|
||||
@click="setStatus(option.key)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 overflow-hidden rounded-lg border border-n-weak">
|
||||
<table class="w-full table-fixed text-left text-sm">
|
||||
<thead class="bg-n-alpha-2 text-xs font-medium text-n-slate-11">
|
||||
<tr>
|
||||
<th class="w-[15%] px-4 py-3">
|
||||
{{ t('CAMPAIGN.WHATSAPP.ANALYTICS.TABLE.CONTACT') }}
|
||||
</th>
|
||||
<th class="w-[14%] px-4 py-3">
|
||||
{{ t('CAMPAIGN.WHATSAPP.ANALYTICS.TABLE.PHONE_NUMBER') }}
|
||||
</th>
|
||||
<th class="w-[10%] px-4 py-3">
|
||||
{{ t('CAMPAIGN.WHATSAPP.ANALYTICS.TABLE.STATUS') }}
|
||||
</th>
|
||||
<th class="w-[43%] px-4 py-3">
|
||||
{{ t('CAMPAIGN.WHATSAPP.ANALYTICS.TABLE.MESSAGE') }}
|
||||
</th>
|
||||
<th class="w-[18%] px-4 py-3">
|
||||
{{ t('CAMPAIGN.WHATSAPP.ANALYTICS.TABLE.ERROR_REASON') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-n-weak text-n-slate-12">
|
||||
<tr v-if="state.isFetchingContacts">
|
||||
<td colspan="5" class="h-24 text-center text-n-slate-11">
|
||||
<Spinner />
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else-if="state.contacts.length === 0">
|
||||
<td colspan="5" class="px-4 py-8 text-center text-n-slate-11">
|
||||
{{ emptyStateMessage }}
|
||||
</td>
|
||||
</tr>
|
||||
<template v-else>
|
||||
<tr
|
||||
v-for="delivery in state.contacts"
|
||||
:key="delivery.contact.id"
|
||||
>
|
||||
<td class="break-words px-4 py-4 align-top">
|
||||
{{ delivery.contact.name || '-' }}
|
||||
</td>
|
||||
<td class="break-words px-4 py-4 align-top">
|
||||
{{ delivery.contact.phone_number || '-' }}
|
||||
</td>
|
||||
<td class="px-4 py-4 align-top">
|
||||
<span
|
||||
class="inline-flex h-6 items-center rounded-md px-2 text-xs font-medium capitalize"
|
||||
:class="statusBadgeClass(delivery.status)"
|
||||
>
|
||||
{{ statusLabel(delivery.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-pre-wrap break-words px-4 py-4 align-top text-n-slate-11"
|
||||
:class="{
|
||||
'italic text-n-slate-10': !delivery.message_content,
|
||||
}"
|
||||
>
|
||||
{{ messageContent(delivery) }}
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-pre-wrap break-words px-4 py-4 align-top text-n-slate-11"
|
||||
>
|
||||
{{ errorReason(delivery) }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between gap-3">
|
||||
<span class="text-sm text-n-slate-11">
|
||||
{{
|
||||
t('CAMPAIGN.WHATSAPP.ANALYTICS.PAGE_INFO', {
|
||||
current: state.meta.current_page || 1,
|
||||
total: state.meta.total_pages || 1,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
color="slate"
|
||||
variant="faded"
|
||||
icon="i-lucide-chevron-left"
|
||||
:disabled="!hasPreviousPage"
|
||||
@click="goToPreviousPage"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
color="slate"
|
||||
variant="faded"
|
||||
icon="i-lucide-chevron-right"
|
||||
:disabled="!hasNextPage"
|
||||
@click="goToNextPage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</section>
|
||||
</template>
|
||||
@@ -10,9 +10,11 @@ import CampaignList from 'dashboard/components-next/Campaigns/Pages/CampaignPage
|
||||
import WhatsAppCampaignDialog from 'dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue';
|
||||
import ConfirmDeleteCampaignDialog from 'dashboard/components-next/Campaigns/Pages/CampaignPage/ConfirmDeleteCampaignDialog.vue';
|
||||
import WhatsAppCampaignEmptyState from 'dashboard/components-next/Campaigns/EmptyState/WhatsAppCampaignEmptyState.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const { t } = useI18n();
|
||||
const getters = useStoreGetters();
|
||||
const router = useRouter();
|
||||
|
||||
const selectedCampaign = ref(null);
|
||||
const [showWhatsAppCampaignDialog, toggleWhatsAppCampaignDialog] = useToggle();
|
||||
@@ -34,6 +36,13 @@ const handleDelete = campaign => {
|
||||
selectedCampaign.value = campaign;
|
||||
confirmDeleteCampaignDialogRef.value.dialogRef.open();
|
||||
};
|
||||
|
||||
const handleAnalytics = campaign => {
|
||||
router.push({
|
||||
name: 'campaigns_whatsapp_analytics',
|
||||
params: { campaignId: campaign.id },
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -59,6 +68,7 @@ const handleDelete = campaign => {
|
||||
v-else-if="!hasNoWhatsAppCampaigns"
|
||||
:campaigns="WhatsAppCampaigns"
|
||||
@delete="handleDelete"
|
||||
@analytics="handleAnalytics"
|
||||
/>
|
||||
<WhatsAppCampaignEmptyState
|
||||
v-else
|
||||
|
||||
Reference in New Issue
Block a user