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:
898
pages/notification/create/index.vue
Normal file
898
pages/notification/create/index.vue
Normal file
@@ -0,0 +1,898 @@
|
||||
<template>
|
||||
<div>
|
||||
<LayoutsBreadcrumb />
|
||||
|
||||
<!-- Info Card -->
|
||||
<rs-card class="mb-5">
|
||||
<template #header>
|
||||
<div class="flex">
|
||||
<span title="Info"
|
||||
><Icon class="mr-2 flex justify-center" name="ic:outline-info"></Icon
|
||||
></span>
|
||||
Create Notification
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="mb-4">
|
||||
Create and send notifications to your audience. Configure basic settings and
|
||||
choose from multiple channels including email and push notifications.
|
||||
</p>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Main Form Card -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold">
|
||||
{{ isEditMode ? "Edit" : "Create" }} Notification
|
||||
</h2>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Step {{ currentStep }} of {{ totalSteps }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="pt-2">
|
||||
<!-- Step Progress Indicator -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
class="flex items-center"
|
||||
:class="{ 'flex-1': index < steps.length - 1 }"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium"
|
||||
:class="{
|
||||
'bg-primary text-white': index + 1 <= currentStep,
|
||||
'bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-400':
|
||||
index + 1 > currentStep,
|
||||
}"
|
||||
>
|
||||
<Icon
|
||||
v-if="index + 1 < currentStep"
|
||||
name="material-symbols:check"
|
||||
class="text-sm"
|
||||
/>
|
||||
<span v-else>{{ index + 1 }}</span>
|
||||
</div>
|
||||
<span
|
||||
class="ml-2 text-sm font-medium"
|
||||
:class="{
|
||||
'text-primary': index + 1 === currentStep,
|
||||
'text-gray-900 dark:text-gray-100': index + 1 < currentStep,
|
||||
'text-gray-500 dark:text-gray-400': index + 1 > currentStep,
|
||||
}"
|
||||
>
|
||||
{{ step.title }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="index < steps.length - 1"
|
||||
class="flex-1 h-0.5 mx-4"
|
||||
:class="{
|
||||
'bg-primary': index + 1 < currentStep,
|
||||
'bg-gray-200 dark:bg-gray-700': index + 1 >= currentStep,
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormKit
|
||||
type="form"
|
||||
@submit="submitNotification"
|
||||
:actions="false"
|
||||
class="w-full"
|
||||
>
|
||||
<!-- Step 1: Basic Settings -->
|
||||
<div v-show="currentStep === 1" class="space-y-6">
|
||||
<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 notification type"
|
||||
/>
|
||||
|
||||
<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="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"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
v-if="notificationForm.deliveryType === 'scheduled'"
|
||||
type="datetime-local"
|
||||
name="scheduledAt"
|
||||
label="Scheduled Date & Time"
|
||||
validation="required"
|
||||
v-model="notificationForm.scheduledAt"
|
||||
help="When should this notification be sent?"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Target Audience -->
|
||||
<div v-show="currentStep === 2" class="space-y-6">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="space-y-4">
|
||||
<FormKit
|
||||
type="radio"
|
||||
name="audienceType"
|
||||
label="Audience Selection"
|
||||
:options="audienceTypes"
|
||||
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="User IDs or Email Addresses"
|
||||
placeholder="Enter user IDs or emails, one per line"
|
||||
rows="4"
|
||||
v-model="notificationForm.specificUsers"
|
||||
help="Enter user IDs or email addresses, one per line"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="notificationForm.audienceType === 'segmented'"
|
||||
class="space-y-4 p-4 border rounded-lg bg-gray-50 dark:bg-gray-800"
|
||||
>
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
name="userSegments"
|
||||
label="User Segments"
|
||||
:options="userSegmentOptions"
|
||||
v-model="notificationForm.userSegments"
|
||||
decorator-icon="material-symbols:check"
|
||||
options-class="grid grid-cols-1 gap-y-2 pt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="p-4 border rounded-lg bg-blue-50 dark:bg-blue-900/20">
|
||||
<h3 class="font-semibold text-blue-800 dark:text-blue-200 mb-2">
|
||||
Audience Preview
|
||||
</h3>
|
||||
<p class="text-sm text-blue-600 dark:text-blue-300">
|
||||
Estimated reach:
|
||||
<span class="font-bold">{{ estimatedReach }}</span>
|
||||
users
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
name="excludeUnsubscribed"
|
||||
label="Exclude Unsubscribed Users"
|
||||
v-model="notificationForm.excludeUnsubscribed"
|
||||
help="Automatically exclude users who have unsubscribed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Content -->
|
||||
<div v-show="currentStep === 3" class="space-y-6">
|
||||
<FormKit
|
||||
type="radio"
|
||||
name="contentType"
|
||||
label="Content Source"
|
||||
:options="contentTypes"
|
||||
validation="required"
|
||||
v-model="notificationForm.contentType"
|
||||
decorator-icon="material-symbols:radio-button-checked"
|
||||
options-class="flex gap-8 pt-1"
|
||||
/>
|
||||
|
||||
<!-- Content Section -->
|
||||
<div
|
||||
v-if="notificationForm.contentType === 'template'"
|
||||
class="space-y-4 p-4 border rounded-lg bg-gray-50 dark:bg-gray-800"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="font-semibold">Select Notification Template</h3>
|
||||
<rs-button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="$router.push('/notification/templates')"
|
||||
type="button"
|
||||
>
|
||||
<Icon name="material-symbols:library-books-outline" class="mr-1" />
|
||||
Browse All Templates
|
||||
</rs-button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<!-- Template Selection -->
|
||||
<div>
|
||||
<FormKit
|
||||
type="select"
|
||||
name="selectedTemplate"
|
||||
label="Select Template"
|
||||
:options="templateOptions"
|
||||
validation="required"
|
||||
v-model="notificationForm.selectedTemplate"
|
||||
help="Choose from existing notification templates"
|
||||
:disabled="isLoadingTemplates"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create New Content Section -->
|
||||
<div v-if="notificationForm.contentType === 'new'" class="space-y-4">
|
||||
<div
|
||||
v-if="notificationForm.channels.includes('push')"
|
||||
class="space-y-4 p-4 border rounded-lg bg-gray-50 dark:bg-gray-800"
|
||||
>
|
||||
<h3 class="font-semibold">Push Notification Content</h3>
|
||||
<FormKit
|
||||
type="text"
|
||||
name="pushTitle"
|
||||
label="Push Title"
|
||||
placeholder="Enter push notification title"
|
||||
validation="required|length:0,50"
|
||||
v-model="notificationForm.pushTitle"
|
||||
help="Maximum 50 characters"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="textarea"
|
||||
name="pushBody"
|
||||
label="Push Message"
|
||||
placeholder="Enter push notification message"
|
||||
validation="required|length:0,150"
|
||||
rows="3"
|
||||
v-model="notificationForm.pushBody"
|
||||
help="Maximum 150 characters"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="notificationForm.channels.includes('email')"
|
||||
class="space-y-4 p-4 border rounded-lg bg-gray-50 dark:bg-gray-800"
|
||||
>
|
||||
<h3 class="font-semibold">Email Content</h3>
|
||||
<FormKit
|
||||
type="textarea"
|
||||
name="emailContent"
|
||||
label="Email Body"
|
||||
validation="required"
|
||||
v-model="notificationForm.emailContent"
|
||||
rows="8"
|
||||
help="You can use HTML formatting"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
name="callToActionText"
|
||||
label="Call-to-Action Button Text (Optional)"
|
||||
placeholder="e.g., Learn More, Get Started"
|
||||
v-model="notificationForm.callToActionText"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="url"
|
||||
name="callToActionUrl"
|
||||
label="Call-to-Action URL (Optional)"
|
||||
placeholder="https://example.com"
|
||||
v-model="notificationForm.callToActionUrl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="border rounded-lg p-4 bg-gray-50 dark:bg-gray-800">
|
||||
<h4 class="font-semibold mb-3">Send Test Notification</h4>
|
||||
<div class="flex gap-4 items-end">
|
||||
<FormKit
|
||||
type="email"
|
||||
name="testEmail"
|
||||
label="Test Email Address"
|
||||
placeholder="test@example.com"
|
||||
v-model="testEmail"
|
||||
outer-class="flex-1 mb-0"
|
||||
/>
|
||||
<rs-button
|
||||
@click="sendTestNotification"
|
||||
variant="outline"
|
||||
type="button"
|
||||
:disabled="isSending"
|
||||
>
|
||||
<Icon name="material-symbols:send" class="mr-1" />
|
||||
Send Test
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step Navigation -->
|
||||
<div class="flex justify-between items-center mt-8 pt-6 border-t">
|
||||
<div class="flex gap-3">
|
||||
<rs-button
|
||||
v-if="currentStep > 1"
|
||||
type="button"
|
||||
variant="outline"
|
||||
@click="previousStep"
|
||||
>
|
||||
<Icon name="material-symbols:arrow-back" class="mr-1" />
|
||||
Previous
|
||||
</rs-button>
|
||||
<rs-button
|
||||
v-if="currentStep < totalSteps"
|
||||
type="button"
|
||||
variant="outline"
|
||||
@click="saveDraftNotification"
|
||||
:disabled="isSaving"
|
||||
>
|
||||
<Icon name="material-symbols:save-as-outline" class="mr-1" />
|
||||
Save as Draft
|
||||
</rs-button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<rs-button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@click="$router.push('/notification/list')"
|
||||
>
|
||||
Cancel
|
||||
</rs-button>
|
||||
<rs-button
|
||||
v-if="currentStep < totalSteps"
|
||||
type="button"
|
||||
@click="nextStep"
|
||||
:disabled="!isCurrentStepValid"
|
||||
:class="{
|
||||
'opacity-50 cursor-not-allowed': !isCurrentStepValid,
|
||||
}"
|
||||
>
|
||||
Next
|
||||
<Icon name="material-symbols:arrow-forward" class="ml-1" />
|
||||
</rs-button>
|
||||
<rs-button
|
||||
v-if="currentStep === totalSteps"
|
||||
type="submit"
|
||||
:disabled="!isFormValid || isSending"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': !isFormValid || isSending }"
|
||||
@click="submitNotification"
|
||||
>
|
||||
<Icon name="material-symbols:send" class="mr-1" />
|
||||
{{
|
||||
notificationForm.deliveryType === "immediate"
|
||||
? "Send Now"
|
||||
: "Schedule Notification"
|
||||
}}
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</FormKit>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed, watch } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { useNotifications } from "~/composables/useNotifications";
|
||||
import { useDebounceFn } from "~/composables/useDebounceFn";
|
||||
|
||||
definePageMeta({
|
||||
title: "Create Notification",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
});
|
||||
|
||||
const { $swal } = useNuxtApp();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// Initialize notifications composable
|
||||
const {
|
||||
isLoading: notificationLoading,
|
||||
createNotification,
|
||||
updateNotification,
|
||||
getNotificationById,
|
||||
saveDraft,
|
||||
testSendNotification,
|
||||
getAudiencePreview,
|
||||
} = useNotifications();
|
||||
|
||||
// Step management
|
||||
const currentStep = ref(1);
|
||||
const totalSteps = ref(3);
|
||||
|
||||
const steps = [
|
||||
{ title: "Basic Settings", key: "basic" },
|
||||
{ title: "Target Audience", key: "audience" },
|
||||
{ title: "Content", key: "content" },
|
||||
];
|
||||
|
||||
// Reactive data
|
||||
const isEditMode = ref(!!route.query.id);
|
||||
const testEmail = ref("");
|
||||
const estimatedReachCount = ref(0);
|
||||
|
||||
// Loading states
|
||||
const isLoadingTemplates = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const isSending = ref(false);
|
||||
|
||||
// Form data
|
||||
const notificationForm = ref({
|
||||
title: "",
|
||||
type: "single",
|
||||
priority: "medium",
|
||||
category: "",
|
||||
channels: [],
|
||||
emailSubject: "",
|
||||
deliveryType: "immediate",
|
||||
scheduledAt: "",
|
||||
timezone: "UTC",
|
||||
audienceType: "all",
|
||||
specificUsers: "",
|
||||
userSegments: [],
|
||||
excludeUnsubscribed: true,
|
||||
contentType: "new",
|
||||
selectedTemplate: "",
|
||||
pushTitle: "",
|
||||
pushBody: "",
|
||||
emailContent: "",
|
||||
callToActionText: "",
|
||||
callToActionUrl: "",
|
||||
});
|
||||
|
||||
// Options for form fields
|
||||
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" },
|
||||
];
|
||||
|
||||
// Dynamic options loaded from API
|
||||
const categoryOptions = ref([
|
||||
{ label: "System", value: "system" },
|
||||
{ label: "Marketing", value: "marketing" },
|
||||
{ label: "Transactional", value: "transactional" },
|
||||
{ label: "Alerts", value: "alerts" },
|
||||
{ label: "Updates", value: "updates" },
|
||||
]);
|
||||
|
||||
const templateOptions = ref([]);
|
||||
|
||||
const channelOptions = [
|
||||
{ label: "Email", value: "email" },
|
||||
{ label: "Push Notification", value: "push" },
|
||||
{ label: "SMS", value: "sms" },
|
||||
];
|
||||
|
||||
const deliveryTypes = [
|
||||
{ label: "Send Immediately", value: "immediate" },
|
||||
{ label: "Schedule for Later", value: "scheduled" },
|
||||
];
|
||||
|
||||
const audienceTypes = [
|
||||
{ label: "All Users", value: "all" },
|
||||
{ label: "Specific Users", value: "specific" },
|
||||
{ label: "Segmented Users", value: "segmented" },
|
||||
];
|
||||
|
||||
const userSegmentOptions = [
|
||||
{ label: "New Users (< 30 days)", value: "new_users" },
|
||||
{ label: "Active Users", value: "active_users" },
|
||||
{ label: "Premium Subscribers", value: "premium_users" },
|
||||
{ label: "Inactive Users", value: "inactive_users" },
|
||||
];
|
||||
|
||||
const contentTypes = [
|
||||
{ label: "Create New Content", value: "new" },
|
||||
{ label: "Use Existing Template", value: "template" },
|
||||
];
|
||||
|
||||
// Computed properties
|
||||
const estimatedReach = computed(() => {
|
||||
if (estimatedReachCount.value > 0) {
|
||||
return estimatedReachCount.value.toLocaleString();
|
||||
}
|
||||
|
||||
// Fallback calculation while API data loads
|
||||
if (notificationForm.value.audienceType === "all") {
|
||||
return "15,000";
|
||||
} else if (notificationForm.value.audienceType === "specific") {
|
||||
const lines = notificationForm.value.specificUsers
|
||||
.split("\n")
|
||||
.filter((line) => line.trim());
|
||||
return lines.length.toLocaleString();
|
||||
} else if (notificationForm.value.audienceType === "segmented") {
|
||||
return "5,000";
|
||||
}
|
||||
return "0";
|
||||
});
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
return (
|
||||
notificationForm.value.title &&
|
||||
notificationForm.value.type &&
|
||||
notificationForm.value.priority &&
|
||||
notificationForm.value.category &&
|
||||
notificationForm.value.channels.length > 0 &&
|
||||
notificationForm.value.audienceType &&
|
||||
(notificationForm.value.contentType === "template"
|
||||
? notificationForm.value.selectedTemplate
|
||||
: (notificationForm.value.channels.includes("push")
|
||||
? notificationForm.value.pushTitle && notificationForm.value.pushBody
|
||||
: true) &&
|
||||
(notificationForm.value.channels.includes("email")
|
||||
? notificationForm.value.emailSubject && notificationForm.value.emailContent
|
||||
: true))
|
||||
);
|
||||
});
|
||||
|
||||
// Computed properties for step validation
|
||||
const isCurrentStepValid = computed(() => {
|
||||
switch (currentStep.value) {
|
||||
case 1: // Basic Settings
|
||||
return (
|
||||
notificationForm.value.title &&
|
||||
notificationForm.value.type &&
|
||||
notificationForm.value.priority &&
|
||||
notificationForm.value.category &&
|
||||
notificationForm.value.channels.length > 0 &&
|
||||
(!notificationForm.value.channels.includes("email") ||
|
||||
notificationForm.value.emailSubject) &&
|
||||
(notificationForm.value.deliveryType === "immediate" ||
|
||||
notificationForm.value.scheduledAt)
|
||||
);
|
||||
case 2: // Target Audience
|
||||
return (
|
||||
notificationForm.value.audienceType &&
|
||||
(notificationForm.value.audienceType !== "specific" ||
|
||||
notificationForm.value.specificUsers.trim())
|
||||
);
|
||||
case 3: // Content
|
||||
return (
|
||||
notificationForm.value.contentType &&
|
||||
(notificationForm.value.contentType === "template"
|
||||
? notificationForm.value.selectedTemplate
|
||||
: (!notificationForm.value.channels.includes("push") ||
|
||||
(notificationForm.value.pushTitle && notificationForm.value.pushBody)) &&
|
||||
(!notificationForm.value.channels.includes("email") ||
|
||||
notificationForm.value.emailContent))
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// API Methods
|
||||
const loadTemplates = async () => {
|
||||
try {
|
||||
isLoadingTemplates.value = true;
|
||||
const response = await $fetch("/api/notifications/templates");
|
||||
if (response.success) {
|
||||
templateOptions.value = response.data.templates.map((template) => ({
|
||||
label: template.title,
|
||||
value: template.value,
|
||||
id: template.id,
|
||||
subject: template.subject,
|
||||
emailContent: template.email_content,
|
||||
pushTitle: template.push_title,
|
||||
pushBody: template.push_body,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading templates:", error);
|
||||
$swal.fire("Error", "Failed to load templates", "error");
|
||||
} finally {
|
||||
isLoadingTemplates.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Debounced function for audience calculation
|
||||
const debouncedCalculateReach = useDebounceFn(calculateEstimatedReach, 500);
|
||||
|
||||
async function calculateEstimatedReach() {
|
||||
try {
|
||||
const requestBody = {
|
||||
audienceType: notificationForm.value.audienceType,
|
||||
specificUsers: notificationForm.value.specificUsers,
|
||||
userSegments: notificationForm.value.userSegments,
|
||||
excludeUnsubscribed: notificationForm.value.excludeUnsubscribed,
|
||||
};
|
||||
|
||||
const response = await getAudiencePreview(requestBody);
|
||||
|
||||
if (response.success) {
|
||||
estimatedReachCount.value = response.data.totalCount;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error calculating estimated reach:", error);
|
||||
// Don't show error to user for background calculation
|
||||
}
|
||||
}
|
||||
|
||||
// Methods
|
||||
const sendTestNotification = async () => {
|
||||
if (!testEmail.value) {
|
||||
$swal.fire("Error", "Please enter a test email address", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isSending.value = true;
|
||||
const testData = {
|
||||
email: testEmail.value,
|
||||
testData: {
|
||||
title: notificationForm.value.title,
|
||||
channels: notificationForm.value.channels,
|
||||
emailSubject: notificationForm.value.emailSubject,
|
||||
emailContent: notificationForm.value.emailContent,
|
||||
pushTitle: notificationForm.value.pushTitle,
|
||||
pushBody: notificationForm.value.pushBody,
|
||||
callToActionText: notificationForm.value.callToActionText,
|
||||
callToActionUrl: notificationForm.value.callToActionUrl,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await testSendNotification(testData);
|
||||
|
||||
if (response.success) {
|
||||
$swal.fire("Success", `Test notification sent to ${testEmail.value}`, "success");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending test notification:", error);
|
||||
$swal.fire("Error", "Failed to send test notification", "error");
|
||||
} finally {
|
||||
isSending.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveDraftNotification = async () => {
|
||||
try {
|
||||
isSaving.value = true;
|
||||
const response = await saveDraft(notificationForm.value);
|
||||
|
||||
if (response.success) {
|
||||
$swal
|
||||
.fire({
|
||||
title: "Success",
|
||||
text: "Notification saved as draft",
|
||||
icon: "success",
|
||||
confirmButtonText: "Continue Editing",
|
||||
showCancelButton: true,
|
||||
cancelButtonText: "Go to List",
|
||||
})
|
||||
.then((result) => {
|
||||
if (!result.isConfirmed) {
|
||||
router.push("/notification/list");
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving draft:", error);
|
||||
$swal.fire("Error", "Failed to save draft", "error");
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const submitNotification = async () => {
|
||||
if (!isFormValid.value) {
|
||||
$swal.fire("Validation Error", "Please complete all required fields", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isSending.value = true;
|
||||
|
||||
const response = isEditMode.value
|
||||
? await updateNotification(route.query.id, notificationForm.value)
|
||||
: await createNotification(notificationForm.value);
|
||||
|
||||
if (response.success) {
|
||||
const actionText =
|
||||
notificationForm.value.deliveryType === "immediate" ? "sent" : "scheduled";
|
||||
|
||||
$swal
|
||||
.fire({
|
||||
title: "Success!",
|
||||
text: `Notification has been ${actionText} successfully.`,
|
||||
icon: "success",
|
||||
confirmButtonText: "View List",
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
router.push("/notification/list");
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating notification:", error);
|
||||
const errorMessage = error.data?.message || "Failed to process notification";
|
||||
$swal.fire("Error", errorMessage, "error");
|
||||
} finally {
|
||||
isSending.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Load notification data if in edit mode
|
||||
const loadNotificationData = async (id) => {
|
||||
try {
|
||||
const notification = await getNotificationById(id);
|
||||
|
||||
if (notification) {
|
||||
// Map notification data to form
|
||||
notificationForm.value = {
|
||||
title: notification.title,
|
||||
type: notification.type,
|
||||
priority: notification.priority,
|
||||
category: notification.category_value,
|
||||
channels: notification.channels.map((c) => c.channel_type),
|
||||
emailSubject: notification.email_subject,
|
||||
deliveryType: notification.delivery_type,
|
||||
scheduledAt: notification.scheduled_at,
|
||||
timezone: notification.timezone || "UTC",
|
||||
audienceType: notification.audience_type,
|
||||
specificUsers: notification.specific_users,
|
||||
userSegments: notification.user_segments?.map((s) => s.value) || [],
|
||||
excludeUnsubscribed: notification.exclude_unsubscribed,
|
||||
contentType: notification.content_type,
|
||||
selectedTemplate: notification.template_value,
|
||||
pushTitle: notification.push_title,
|
||||
pushBody: notification.push_body,
|
||||
emailContent: notification.email_content,
|
||||
callToActionText: notification.call_to_action_text,
|
||||
callToActionUrl: notification.call_to_action_url,
|
||||
};
|
||||
|
||||
// Update estimated reach
|
||||
estimatedReachCount.value = notification.estimated_reach;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading notification data:", error);
|
||||
$swal.fire("Error", "Failed to load notification data", "error");
|
||||
}
|
||||
};
|
||||
|
||||
// Step navigation methods
|
||||
const nextStep = () => {
|
||||
if (currentStep.value < totalSteps.value && isCurrentStepValid.value) {
|
||||
currentStep.value++;
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
} else if (!isCurrentStepValid.value) {
|
||||
$swal.fire(
|
||||
"Incomplete Information",
|
||||
"Please complete all required fields before proceeding",
|
||||
"warning"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const previousStep = () => {
|
||||
if (currentStep.value > 1) {
|
||||
currentStep.value--;
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for audience changes to calculate estimated reach
|
||||
watch(
|
||||
() => [
|
||||
notificationForm.value.audienceType,
|
||||
notificationForm.value.specificUsers,
|
||||
notificationForm.value.userSegments,
|
||||
notificationForm.value.excludeUnsubscribed,
|
||||
],
|
||||
() => {
|
||||
// Use debounced function to avoid too many API calls
|
||||
debouncedCalculateReach();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
// Load templates
|
||||
await loadTemplates();
|
||||
|
||||
// Load data if editing existing notification
|
||||
if (isEditMode.value && route.query.id) {
|
||||
await loadNotificationData(route.query.id);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add any custom styles if needed */
|
||||
/* .formkit-input {
|
||||
@apply appearance-none bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md py-2 px-3 text-base leading-normal focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-400 dark:focus:border-primary-400;
|
||||
} */
|
||||
</style>
|
||||
Reference in New Issue
Block a user