Update various configuration files, components, and assets; enhance notification system and API endpoints; improve documentation and styles across the application.
This commit is contained in:
661
pages/notification/edit/[id].vue
Normal file
661
pages/notification/edit/[id].vue
Normal file
@@ -0,0 +1,661 @@
|
||||
<template>
|
||||
<div>
|
||||
<LayoutsBreadcrumb />
|
||||
|
||||
<!-- Info Card -->
|
||||
<rs-card class="mb-5">
|
||||
<template #header>
|
||||
<div class="flex">
|
||||
<Icon class="mr-2 flex justify-center" name="ic:outline-info"></Icon>
|
||||
Edit Notification
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="mb-4">
|
||||
Edit and update your notification settings. Modify delivery options, content,
|
||||
and targeting to improve your notification campaign effectiveness.
|
||||
</p>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="text-center py-12">
|
||||
<div class="flex justify-center mb-4">
|
||||
<Icon name="ic:outline-refresh" size="2rem" class="text-primary animate-spin" />
|
||||
</div>
|
||||
<p class="text-gray-600">Loading notification details...</p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<rs-card v-else-if="error" class="mb-5">
|
||||
<template #body>
|
||||
<div class="text-center py-8">
|
||||
<div class="flex justify-center mb-4">
|
||||
<Icon name="ic:outline-error" size="4rem" class="text-red-400" />
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-500 mb-2">
|
||||
Error Loading Notification
|
||||
</h3>
|
||||
<p class="text-gray-500 mb-4">
|
||||
{{ error }}
|
||||
</p>
|
||||
<rs-button @click="$router.push('/notification/list')">
|
||||
<Icon name="ic:outline-arrow-back" class="mr-1"></Icon>
|
||||
Back to List
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Notification Not Found -->
|
||||
<rs-card v-else-if="!notification">
|
||||
<template #body>
|
||||
<div class="text-center py-8">
|
||||
<div class="flex justify-center mb-4">
|
||||
<Icon name="ic:outline-error" size="4rem" class="text-red-400" />
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-500 mb-2">Notification Not Found</h3>
|
||||
<p class="text-gray-500 mb-4">
|
||||
The notification you're trying to edit doesn't exist or has been deleted.
|
||||
</p>
|
||||
<rs-button @click="$router.push('/notification/list')">
|
||||
<Icon name="ic:outline-arrow-back" class="mr-1"></Icon>
|
||||
Back to List
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Edit Form -->
|
||||
<rs-card v-else>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold">Edit Notification</h2>
|
||||
<div class="flex gap-3">
|
||||
<rs-button @click="$router.push('/notification/list')" variant="outline">
|
||||
<Icon name="ic:outline-arrow-back" class="mr-1"></Icon>
|
||||
Back to List
|
||||
</rs-button>
|
||||
<rs-button @click="viewNotification" variant="primary">
|
||||
<Icon name="ic:outline-visibility" class="mr-1"></Icon>
|
||||
View Details
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="pt-2">
|
||||
<FormKit
|
||||
type="form"
|
||||
@submit="handleUpdateNotification"
|
||||
:actions="false"
|
||||
class="w-full"
|
||||
>
|
||||
<!-- Basic Information -->
|
||||
<div class="space-y-6 mb-8">
|
||||
<h3
|
||||
class="text-lg font-semibold text-gray-900 dark:text-gray-100 border-b border-gray-200 dark:border-gray-700 pb-2"
|
||||
>
|
||||
Basic Information
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Left Column -->
|
||||
<div class="space-y-4">
|
||||
<FormKit
|
||||
type="text"
|
||||
name="title"
|
||||
label="Notification Title"
|
||||
placeholder="Enter notification title"
|
||||
validation="required"
|
||||
v-model="notificationForm.title"
|
||||
help="This is for internal identification purposes"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="select"
|
||||
name="type"
|
||||
label="Notification Type"
|
||||
:options="notificationTypes"
|
||||
validation="required"
|
||||
v-model="notificationForm.type"
|
||||
help="Choose between single targeted notification or bulk notification"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="select"
|
||||
name="priority"
|
||||
label="Priority Level"
|
||||
:options="priorityLevels"
|
||||
validation="required"
|
||||
v-model="notificationForm.priority"
|
||||
help="Set the importance level of this notification"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="select"
|
||||
name="category"
|
||||
label="Category"
|
||||
:options="categoryOptions"
|
||||
validation="required"
|
||||
v-model="notificationForm.category"
|
||||
help="Categorize your notification for better organization"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="space-y-4">
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
name="channels"
|
||||
label="Delivery Channels"
|
||||
:options="channelOptions"
|
||||
validation="required|min:1"
|
||||
v-model="notificationForm.channels"
|
||||
decorator-icon="material-symbols:check"
|
||||
options-class="grid grid-cols-1 gap-y-2 pt-1"
|
||||
help="Select one or more delivery channels"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-if="notificationForm.channels.includes('email')"
|
||||
type="text"
|
||||
name="emailSubject"
|
||||
label="Email Subject Line"
|
||||
placeholder="Enter email subject"
|
||||
validation="required"
|
||||
v-model="notificationForm.emailSubject"
|
||||
help="This will be the email subject line"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="datetime-local"
|
||||
name="expiresAt"
|
||||
label="Expiration Date & Time (Optional)"
|
||||
v-model="notificationForm.expiresAt"
|
||||
help="Set when this notification should expire"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scheduling -->
|
||||
<div class="space-y-6 mb-8">
|
||||
<h3
|
||||
class="text-lg font-semibold text-gray-900 dark:text-gray-100 border-b border-gray-200 dark:border-gray-700 pb-2"
|
||||
>
|
||||
Scheduling
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="space-y-4">
|
||||
<FormKit
|
||||
type="radio"
|
||||
name="deliveryType"
|
||||
label="Delivery Schedule"
|
||||
:options="deliveryTypes"
|
||||
validation="required"
|
||||
v-model="notificationForm.deliveryType"
|
||||
decorator-icon="material-symbols:radio-button-checked"
|
||||
options-class="space-y-3 pt-1"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="notificationForm.deliveryType === 'scheduled'"
|
||||
class="space-y-4 p-4 border rounded-lg bg-gray-50 dark:bg-gray-800"
|
||||
>
|
||||
<FormKit
|
||||
type="datetime-local"
|
||||
name="scheduledAt"
|
||||
label="Scheduled Date & Time"
|
||||
validation="required"
|
||||
v-model="notificationForm.scheduledAt"
|
||||
help="When should this notification be sent?"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="select"
|
||||
name="timezone"
|
||||
label="Timezone"
|
||||
:options="timezoneOptions"
|
||||
validation="required"
|
||||
v-model="notificationForm.timezone"
|
||||
help="Select the timezone for scheduling"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="space-y-4"
|
||||
v-if="notificationForm.deliveryType === 'scheduled'"
|
||||
>
|
||||
<div class="p-4 border rounded-lg bg-blue-50 dark:bg-blue-900/20">
|
||||
<h4 class="font-semibold text-blue-800 dark:text-blue-200 mb-2">
|
||||
Scheduling Information
|
||||
</h4>
|
||||
<div class="text-sm text-blue-700 dark:text-blue-300 space-y-1">
|
||||
<p v-if="notificationForm.scheduledAt">
|
||||
<strong>Scheduled for:</strong>
|
||||
{{ formatScheduledTime() }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Timezone:</strong>
|
||||
{{ notificationForm.timezone }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Target Audience -->
|
||||
<div class="space-y-6 mb-8">
|
||||
<h3
|
||||
class="text-lg font-semibold text-gray-900 dark:text-gray-100 border-b border-gray-200 dark:border-gray-700 pb-2"
|
||||
>
|
||||
Target Audience
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="space-y-4">
|
||||
<FormKit
|
||||
type="radio"
|
||||
name="audienceType"
|
||||
label="Target Audience"
|
||||
:options="audienceTypeOptions"
|
||||
validation="required"
|
||||
v-model="notificationForm.audienceType"
|
||||
decorator-icon="material-symbols:radio-button-checked"
|
||||
options-class="space-y-3 pt-1"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="notificationForm.audienceType === 'specific'"
|
||||
class="space-y-4 p-4 border rounded-lg bg-gray-50 dark:bg-gray-800"
|
||||
>
|
||||
<FormKit
|
||||
type="textarea"
|
||||
name="specificUsers"
|
||||
label="Specific Users"
|
||||
placeholder="Enter email addresses or user IDs (one per line)"
|
||||
validation="required"
|
||||
v-model="notificationForm.specificUsers"
|
||||
rows="6"
|
||||
help="Enter one email address or user ID per line"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="p-4 border rounded-lg bg-green-50 dark:bg-green-900/20">
|
||||
<h4 class="font-semibold text-green-800 dark:text-green-200 mb-2">
|
||||
Estimated Reach
|
||||
</h4>
|
||||
<div class="text-2xl font-bold text-green-600 dark:text-green-400">
|
||||
{{ estimatedReach.toLocaleString() }} users
|
||||
</div>
|
||||
<p class="text-sm text-green-700 dark:text-green-300 mt-1">
|
||||
Based on your audience selection
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div
|
||||
class="flex justify-between items-center pt-6 border-t border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div class="flex gap-3">
|
||||
<rs-button @click="handleSaveDraft" variant="outline">
|
||||
<Icon name="material-symbols:save" class="mr-1"></Icon>
|
||||
Save as Draft
|
||||
</rs-button>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<rs-button @click="$router.push('/notification/list')" variant="outline">
|
||||
Cancel
|
||||
</rs-button>
|
||||
<rs-button btnType="submit">
|
||||
<Icon name="material-symbols:update" class="mr-1"></Icon>
|
||||
Update Notification
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</FormKit>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useNotifications } from "~/composables/useNotifications";
|
||||
|
||||
const router = useRouter();
|
||||
const nuxtApp = useNuxtApp();
|
||||
|
||||
definePageMeta({
|
||||
title: "Edit Notification",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/dashboard",
|
||||
},
|
||||
{
|
||||
name: "Notification",
|
||||
path: "/notification",
|
||||
},
|
||||
{
|
||||
name: "List",
|
||||
path: "/notification/list",
|
||||
},
|
||||
{
|
||||
name: "Edit",
|
||||
path: "",
|
||||
type: "current",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Get route params
|
||||
const route = useRoute();
|
||||
const notificationId = route.params.id;
|
||||
|
||||
// Use the notifications composable
|
||||
const {
|
||||
isLoading,
|
||||
error,
|
||||
getNotificationById,
|
||||
updateNotification,
|
||||
saveDraft,
|
||||
testSendNotification,
|
||||
getAudiencePreview,
|
||||
} = useNotifications();
|
||||
|
||||
// Reactive data
|
||||
const notification = ref(null);
|
||||
const testEmail = ref("");
|
||||
const estimatedReach = ref(0);
|
||||
|
||||
// Form data
|
||||
const notificationForm = ref({
|
||||
title: "",
|
||||
type: "single",
|
||||
priority: "medium",
|
||||
category: "",
|
||||
channels: [],
|
||||
emailSubject: "",
|
||||
expiresAt: "",
|
||||
deliveryType: "immediate",
|
||||
scheduledAt: "",
|
||||
timezone: "UTC",
|
||||
audienceType: "all",
|
||||
specificUsers: "",
|
||||
userSegments: [],
|
||||
userStatus: "",
|
||||
registrationPeriod: "",
|
||||
excludeUnsubscribed: true,
|
||||
});
|
||||
|
||||
// Form options
|
||||
const notificationTypes = [
|
||||
{ label: "Single Notification", value: "single" },
|
||||
{ label: "Bulk Notification", value: "bulk" },
|
||||
];
|
||||
|
||||
const priorityLevels = [
|
||||
{ label: "Low", value: "low" },
|
||||
{ label: "Medium", value: "medium" },
|
||||
{ label: "High", value: "high" },
|
||||
{ label: "Critical", value: "critical" },
|
||||
];
|
||||
|
||||
const categoryOptions = [
|
||||
{ label: "User Management", value: "user_management" },
|
||||
{ label: "Orders & Transactions", value: "orders" },
|
||||
{ label: "Security & Authentication", value: "security" },
|
||||
{ label: "Marketing & Promotions", value: "marketing" },
|
||||
{ label: "System Updates", value: "system" },
|
||||
{ label: "General Information", value: "general" },
|
||||
];
|
||||
|
||||
const channelOptions = [
|
||||
{ label: "Email", value: "email" },
|
||||
{ label: "Push Notification", value: "push" },
|
||||
];
|
||||
|
||||
const deliveryTypes = [
|
||||
{ label: "Send Immediately", value: "immediate" },
|
||||
{ label: "Schedule for Later", value: "scheduled" },
|
||||
];
|
||||
|
||||
const timezoneOptions = [
|
||||
{ label: "UTC", value: "UTC" },
|
||||
{ label: "Asia/Kuala_Lumpur", value: "Asia/Kuala_Lumpur" },
|
||||
{ label: "America/New_York", value: "America/New_York" },
|
||||
{ label: "Europe/London", value: "Europe/London" },
|
||||
{ label: "Asia/Tokyo", value: "Asia/Tokyo" },
|
||||
];
|
||||
|
||||
const audienceTypeOptions = [
|
||||
{ label: "All Users", value: "all" },
|
||||
{ label: "Specific Users", value: "specific" },
|
||||
];
|
||||
|
||||
// Methods
|
||||
const formatScheduledTime = () => {
|
||||
if (!notificationForm.value.scheduledAt) return "";
|
||||
return new Date(notificationForm.value.scheduledAt).toLocaleDateString("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
const viewNotification = () => {
|
||||
router.push(`/notification/view/${notificationId}`);
|
||||
};
|
||||
|
||||
const handleSaveDraft = async () => {
|
||||
try {
|
||||
await saveDraft(notificationForm.value);
|
||||
nuxtApp.$swal.fire("Success", "Notification saved as draft", "success");
|
||||
} catch (error) {
|
||||
console.error("Error saving draft:", error);
|
||||
nuxtApp.$swal.fire("Error", "Failed to save draft", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateNotification = async () => {
|
||||
try {
|
||||
// Validate channels data
|
||||
if (
|
||||
!Array.isArray(notificationForm.value.channels) ||
|
||||
notificationForm.value.channels.length === 0
|
||||
) {
|
||||
throw new Error("Please select at least one delivery channel");
|
||||
}
|
||||
|
||||
// Validate that all selected channels are valid
|
||||
const validChannels = channelOptions.map((option) => option.value);
|
||||
const hasInvalidChannel = notificationForm.value.channels.some(
|
||||
(channel) => !validChannels.includes(channel)
|
||||
);
|
||||
if (hasInvalidChannel) {
|
||||
throw new Error("Invalid delivery channel selected");
|
||||
}
|
||||
|
||||
// Show loading indicator
|
||||
const loadingSwal = nuxtApp.$swal.fire({
|
||||
title: "Updating...",
|
||||
text: "Please wait while we update the notification",
|
||||
allowOutsideClick: false,
|
||||
allowEscapeKey: false,
|
||||
showConfirmButton: false,
|
||||
didOpen: () => {
|
||||
nuxtApp.$swal.showLoading();
|
||||
},
|
||||
});
|
||||
|
||||
// Transform data to match API expectations (camelCase to snake_case)
|
||||
const formData = {
|
||||
title: notificationForm.value.title,
|
||||
type: notificationForm.value.type,
|
||||
priority: notificationForm.value.priority,
|
||||
// Send null for category_id instead of the value
|
||||
category_id: null, // We'll omit this field and let the server keep the existing value
|
||||
status: notificationForm.value.status || "active",
|
||||
delivery_type: notificationForm.value.deliveryType,
|
||||
scheduled_at: notificationForm.value.scheduledAt,
|
||||
timezone: notificationForm.value.timezone,
|
||||
expires_at: notificationForm.value.expiresAt,
|
||||
audience_type: notificationForm.value.audienceType,
|
||||
specific_users: notificationForm.value.specificUsers,
|
||||
user_segments: notificationForm.value.userSegments || [],
|
||||
user_status: notificationForm.value.userStatus,
|
||||
registration_period: notificationForm.value.registrationPeriod,
|
||||
exclude_unsubscribed: notificationForm.value.excludeUnsubscribed,
|
||||
email_subject: notificationForm.value.emailSubject,
|
||||
// Transform channels to API format
|
||||
channels: notificationForm.value.channels.map((channel) => ({
|
||||
type: channel,
|
||||
is_enabled: true,
|
||||
})),
|
||||
};
|
||||
|
||||
console.log("Sending update with data:", formData);
|
||||
const response = await updateNotification(notificationId, formData);
|
||||
console.log("Update response:", response);
|
||||
|
||||
// Close loading indicator
|
||||
loadingSwal.close();
|
||||
|
||||
if (response && response.success) {
|
||||
nuxtApp.$swal
|
||||
.fire({
|
||||
title: "Success!",
|
||||
text: "Notification has been updated successfully.",
|
||||
icon: "success",
|
||||
confirmButtonText: "Back to List",
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
router.push("/notification/list");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new Error(response?.message || "Update failed with unknown error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating notification:", error);
|
||||
const errorMessage =
|
||||
error.data?.message || error.message || "Failed to update notification";
|
||||
nuxtApp.$swal.fire("Error", errorMessage, "error");
|
||||
}
|
||||
};
|
||||
|
||||
const loadNotification = async () => {
|
||||
try {
|
||||
const data = await getNotificationById(notificationId);
|
||||
notification.value = data;
|
||||
|
||||
console.log("Retrieved notification data:", data);
|
||||
|
||||
// Transform channels data from backend format to frontend format
|
||||
let channels = [];
|
||||
if (Array.isArray(data.channels)) {
|
||||
channels = data.channels;
|
||||
} else if (Array.isArray(data.notification_channels)) {
|
||||
channels = data.notification_channels.map((c) => c.channel_type);
|
||||
}
|
||||
|
||||
// Populate form with existing data
|
||||
notificationForm.value = {
|
||||
title: data.title,
|
||||
type: data.type || "single",
|
||||
priority: data.priority,
|
||||
category: data.category?.value,
|
||||
channels: channels,
|
||||
emailSubject: data.emailSubject,
|
||||
expiresAt: data.expiresAt,
|
||||
deliveryType: data.deliveryType,
|
||||
scheduledAt: data.scheduledAt,
|
||||
timezone: data.timezone || "UTC",
|
||||
audienceType: data.audienceType,
|
||||
specificUsers: data.specificUsers,
|
||||
userSegments: data.userSegments || [],
|
||||
userStatus: data.userStatus,
|
||||
registrationPeriod: data.registrationPeriod,
|
||||
excludeUnsubscribed: data.excludeUnsubscribed !== false,
|
||||
};
|
||||
|
||||
console.log("Populated form:", notificationForm.value);
|
||||
|
||||
// Update estimated reach
|
||||
estimatedReach.value = data.analytics?.estimatedReach || 0;
|
||||
} catch (error) {
|
||||
console.error("Error loading notification:", error);
|
||||
notification.value = null;
|
||||
nuxtApp.$swal
|
||||
.fire({
|
||||
title: "Error",
|
||||
text: "Failed to load notification details",
|
||||
icon: "error",
|
||||
confirmButtonText: "Back to List",
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
router.push("/notification/list");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for audience type changes to update estimated reach
|
||||
watch(
|
||||
() => [
|
||||
notificationForm.value.audienceType,
|
||||
notificationForm.value.specificUsers,
|
||||
notificationForm.value.userSegments,
|
||||
notificationForm.value.userStatus,
|
||||
notificationForm.value.registrationPeriod,
|
||||
notificationForm.value.excludeUnsubscribed,
|
||||
notificationForm.value.channels,
|
||||
],
|
||||
async () => {
|
||||
try {
|
||||
// Skip if audience type is not set
|
||||
if (!notificationForm.value.audienceType) return;
|
||||
|
||||
const response = await getAudiencePreview({
|
||||
audienceType: notificationForm.value.audienceType,
|
||||
specificUsers: notificationForm.value.specificUsers,
|
||||
userSegments: notificationForm.value.userSegments || [],
|
||||
userStatus: notificationForm.value.userStatus,
|
||||
registrationPeriod: notificationForm.value.registrationPeriod,
|
||||
excludeUnsubscribed: notificationForm.value.excludeUnsubscribed,
|
||||
channels: notificationForm.value.channels,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
estimatedReach.value = response.data.totalCount;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting audience preview:", error);
|
||||
// Don't show error to user for background calculation
|
||||
estimatedReach.value = 0;
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
loadNotification();
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user