1327 lines
47 KiB
Vue
1327 lines
47 KiB
Vue
<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 delivery options,
|
|
target specific groups, 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 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"
|
|
/>
|
|
|
|
<div class="relative">
|
|
<FormKit
|
|
type="select"
|
|
name="category"
|
|
label="Category"
|
|
:options="categoryOptions"
|
|
validation="required"
|
|
v-model="notificationForm.category"
|
|
help="Categorize your notification for better organization"
|
|
:disabled="isLoadingCategories"
|
|
/>
|
|
<div v-if="isLoadingCategories" class="absolute right-3 top-9">
|
|
<Icon name="ic:outline-refresh" class="animate-spin text-primary" />
|
|
</div>
|
|
</div>
|
|
</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>
|
|
|
|
<!-- Step 2: Scheduling -->
|
|
<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="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">
|
|
<FormKit
|
|
type="checkbox"
|
|
name="enableAbTesting"
|
|
label="Enable A/B Testing"
|
|
v-model="notificationForm.enableAbTesting"
|
|
help="Split your audience to test different variations"
|
|
/>
|
|
|
|
<div
|
|
v-if="notificationForm.enableAbTesting"
|
|
class="space-y-4 p-4 border rounded-lg bg-gray-50 dark:bg-gray-800"
|
|
>
|
|
<FormKit
|
|
type="range"
|
|
name="abTestSplit"
|
|
label="A/B Test Split Percentage"
|
|
:min="10"
|
|
:max="90"
|
|
:step="10"
|
|
v-model="notificationForm.abTestSplit"
|
|
:help="`Split: ${notificationForm.abTestSplit}% vs ${
|
|
100 - notificationForm.abTestSplit
|
|
}%`"
|
|
/>
|
|
|
|
<FormKit
|
|
type="text"
|
|
name="abTestName"
|
|
label="Test Name"
|
|
placeholder="e.g., Subject Line Test"
|
|
v-model="notificationForm.abTestName"
|
|
help="Name your A/B test for tracking"
|
|
/>
|
|
</div>
|
|
|
|
<FormKit
|
|
type="checkbox"
|
|
name="enableTracking"
|
|
label="Enable Analytics Tracking"
|
|
v-model="notificationForm.enableTracking"
|
|
help="Track opens, clicks, and other engagement metrics"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 3: Target Audience -->
|
|
<div v-show="currentStep === 3" 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"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
name="userStatus"
|
|
label="User Status"
|
|
:options="userStatusOptions"
|
|
v-model="notificationForm.userStatus"
|
|
placeholder="All statuses"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
name="registrationPeriod"
|
|
label="Registration Period"
|
|
:options="registrationPeriodOptions"
|
|
v-model="notificationForm.registrationPeriod"
|
|
placeholder="All time"
|
|
/>
|
|
</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>
|
|
<rs-button
|
|
v-if="notificationForm.audienceType !== 'all'"
|
|
@click="previewAudience"
|
|
variant="outline"
|
|
size="sm"
|
|
class="mt-2"
|
|
:disabled="isAudienceLoading"
|
|
>
|
|
<Icon
|
|
:name="
|
|
isAudienceLoading
|
|
? 'ic:outline-refresh'
|
|
: 'material-symbols:preview-outline'
|
|
"
|
|
:class="{ 'animate-spin': isAudienceLoading }"
|
|
class="mr-1"
|
|
/>
|
|
{{ isAudienceLoading ? "Loading..." : "Preview Audience" }}
|
|
</rs-button>
|
|
</div>
|
|
|
|
<FormKit
|
|
type="checkbox"
|
|
name="excludeUnsubscribed"
|
|
label="Exclude Unsubscribed Users"
|
|
v-model="notificationForm.excludeUnsubscribed"
|
|
help="Automatically exclude users who have unsubscribed"
|
|
/>
|
|
|
|
<FormKit
|
|
type="checkbox"
|
|
name="respectDoNotDisturb"
|
|
label="Respect Do Not Disturb Hours"
|
|
v-model="notificationForm.respectDoNotDisturb"
|
|
help="Don't send during user's quiet hours"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 4: Content -->
|
|
<div v-show="currentStep === 4" 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"
|
|
/>
|
|
|
|
<!-- Use Template 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="relative">
|
|
<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 v-if="isLoadingTemplates" class="absolute right-3 top-9">
|
|
<Icon name="ic:outline-refresh" class="animate-spin text-primary" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Template Loading State -->
|
|
<div v-if="isLoadingTemplates" class="text-center py-4">
|
|
<Icon
|
|
name="ic:outline-refresh"
|
|
class="animate-spin text-primary mx-auto mb-2"
|
|
size="24"
|
|
/>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
Loading templates...
|
|
</p>
|
|
</div>
|
|
|
|
<!-- No Templates State -->
|
|
<div v-else-if="templateOptions.length === 0" class="text-center py-4">
|
|
<Icon
|
|
name="ic:outline-inbox"
|
|
class="text-gray-400 mx-auto mb-2"
|
|
size="24"
|
|
/>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
No templates available
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Template Preview -->
|
|
<div
|
|
v-else-if="selectedTemplateData"
|
|
class="border rounded-lg p-4 bg-white dark:bg-gray-700"
|
|
>
|
|
<h4 class="font-semibold mb-2">Template Preview:</h4>
|
|
<div class="space-y-3">
|
|
<div v-if="selectedTemplateData.subject">
|
|
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
Email Subject:
|
|
</p>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
{{ selectedTemplateData.subject }}
|
|
</p>
|
|
</div>
|
|
<div v-if="selectedTemplateData.content">
|
|
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
Email Content:
|
|
</p>
|
|
<div
|
|
class="prose prose-sm max-w-none dark:prose-invert"
|
|
v-html="selectedTemplateData.content"
|
|
></div>
|
|
</div>
|
|
<div v-if="selectedTemplateData.pushTitle">
|
|
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
Push Title:
|
|
</p>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
{{ selectedTemplateData.pushTitle }}
|
|
</p>
|
|
</div>
|
|
<div v-if="selectedTemplateData.pushBody">
|
|
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
Push Body:
|
|
</p>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
{{ selectedTemplateData.pushBody }}
|
|
</p>
|
|
</div>
|
|
</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"
|
|
/>
|
|
|
|
<FormKit
|
|
type="url"
|
|
name="pushImageUrl"
|
|
label="Image URL (Optional)"
|
|
placeholder="https://example.com/image.jpg"
|
|
v-model="notificationForm.pushImageUrl"
|
|
help="URL to an image for rich push notifications"
|
|
/>
|
|
</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>
|
|
|
|
<!-- Personalization Variables -->
|
|
<div class="p-4 border rounded-lg bg-yellow-50 dark:bg-yellow-900/20">
|
|
<h4 class="font-semibold text-yellow-800 dark:text-yellow-200 mb-2">
|
|
<Icon name="material-symbols:lightbulb-outline" class="mr-1" />
|
|
Personalization Variables
|
|
</h4>
|
|
<p class="text-sm text-yellow-700 dark:text-yellow-300 mb-2">
|
|
Use these variables in your content to personalize messages:
|
|
</p>
|
|
<div class="flex flex-wrap gap-2">
|
|
<span
|
|
v-for="variable in personalizationVariables"
|
|
:key="variable"
|
|
class="px-2 py-1 bg-yellow-200 dark:bg-yellow-800 text-yellow-800 dark:text-yellow-200 text-xs rounded cursor-pointer hover:bg-yellow-300 dark:hover:bg-yellow-700"
|
|
@click="insertVariable(variable)"
|
|
>
|
|
{{ variable }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 5: Preview & Send -->
|
|
<div v-show="currentStep === 5" class="space-y-6">
|
|
<!-- Preview Section -->
|
|
<div class="border rounded-lg p-6 bg-gray-50 dark:bg-gray-800">
|
|
<h3 class="font-semibold mb-4">Notification Preview</h3>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<!-- Push Preview -->
|
|
<div
|
|
v-if="notificationForm.channels.includes('push')"
|
|
class="space-y-3"
|
|
>
|
|
<h4 class="font-medium">Push Notification Preview</h4>
|
|
<div class="bg-white dark:bg-gray-700 border rounded-lg p-4 max-w-sm">
|
|
<div class="flex items-start gap-3">
|
|
<div
|
|
class="w-8 h-8 bg-primary rounded-lg flex items-center justify-center"
|
|
>
|
|
<Icon
|
|
name="material-symbols:notifications"
|
|
class="text-white text-sm"
|
|
/>
|
|
</div>
|
|
<div class="flex-1">
|
|
<p class="font-semibold text-sm">
|
|
{{ notificationForm.pushTitle || "Push Title" }}
|
|
</p>
|
|
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
|
{{
|
|
notificationForm.pushBody ||
|
|
"Push message content will appear here..."
|
|
}}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Email Preview -->
|
|
<div
|
|
v-if="notificationForm.channels.includes('email')"
|
|
class="space-y-3"
|
|
>
|
|
<h4 class="font-medium">Email Preview</h4>
|
|
<div class="bg-white dark:bg-gray-700 border rounded-lg p-4">
|
|
<div class="border-b pb-2 mb-3">
|
|
<p class="font-semibold text-sm">
|
|
Subject:
|
|
{{ notificationForm.emailSubject || "Email Subject" }}
|
|
</p>
|
|
</div>
|
|
<div class="prose prose-sm max-w-none dark:prose-invert">
|
|
<div
|
|
v-html="
|
|
notificationForm.emailContent ||
|
|
'Email content will appear here...'
|
|
"
|
|
></div>
|
|
</div>
|
|
<div v-if="notificationForm.callToActionText" class="mt-4">
|
|
<rs-button size="sm" variant="primary">
|
|
{{ notificationForm.callToActionText }}
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Summary Section -->
|
|
<div class="border rounded-lg p-6 bg-blue-50 dark:bg-blue-900/20">
|
|
<h3 class="font-semibold text-blue-800 dark:text-blue-200 mb-4">
|
|
Delivery Summary
|
|
</h3>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
|
|
<div>
|
|
<span class="font-medium text-blue-700 dark:text-blue-300"
|
|
>Type:</span
|
|
>
|
|
<p class="text-blue-600 dark:text-blue-400">
|
|
{{ notificationForm.type }}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<span class="font-medium text-blue-700 dark:text-blue-300"
|
|
>Channels:</span
|
|
>
|
|
<p class="text-blue-600 dark:text-blue-400">
|
|
{{ notificationForm.channels.join(", ") }}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<span class="font-medium text-blue-700 dark:text-blue-300"
|
|
>Delivery:</span
|
|
>
|
|
<p class="text-blue-600 dark:text-blue-400">
|
|
{{
|
|
notificationForm.deliveryType === "immediate"
|
|
? "Send Now"
|
|
: "Scheduled"
|
|
}}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<span class="font-medium text-blue-700 dark:text-blue-300"
|
|
>Estimated Reach:</span
|
|
>
|
|
<p class="text-blue-600 dark:text-blue-400">
|
|
{{ estimatedReach }} users
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Test Notification -->
|
|
<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"
|
|
>
|
|
<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="saveDraft"
|
|
>
|
|
<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="resetForm">
|
|
<Icon name="material-symbols:refresh" class="mr-1" />
|
|
Reset
|
|
</rs-button>
|
|
<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"
|
|
:class="{ 'opacity-50 cursor-not-allowed': !isFormValid }"
|
|
@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>
|
|
|
|
<!-- Audience Preview Modal -->
|
|
<rs-modal v-model="showAudiencePreview" title="Audience Preview" size="lg">
|
|
<div class="space-y-4">
|
|
<div class="flex justify-between items-center">
|
|
<h3 class="font-semibold">Target Audience</h3>
|
|
<span class="text-sm text-gray-600">{{ previewedUsers.length }} users</span>
|
|
</div>
|
|
<div class="max-h-64 overflow-y-auto border rounded-lg">
|
|
<div
|
|
v-for="user in previewedUsers"
|
|
:key="user.id"
|
|
class="flex justify-between items-center p-3 border-b last:border-b-0"
|
|
>
|
|
<div>
|
|
<p class="font-medium">{{ user.name }}</p>
|
|
<p class="text-sm text-gray-600">{{ user.email }}</p>
|
|
</div>
|
|
<span class="text-xs bg-gray-200 dark:bg-gray-700 px-2 py-1 rounded">
|
|
{{ user.segment }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<template #footer>
|
|
<rs-button @click="showAudiencePreview = false">Close</rs-button>
|
|
</template>
|
|
</rs-modal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
definePageMeta({
|
|
title: "Create Notification",
|
|
middleware: ["auth"],
|
|
requiresAuth: true,
|
|
});
|
|
|
|
const { $swal } = useNuxtApp();
|
|
const router = useRouter();
|
|
const route = useRoute();
|
|
|
|
// Step management
|
|
const currentStep = ref(1);
|
|
const totalSteps = ref(5);
|
|
|
|
const steps = [
|
|
{ title: "Basic Settings", key: "basic" },
|
|
{ title: "Scheduling", key: "scheduling" },
|
|
{ title: "Target Audience", key: "audience" },
|
|
{ title: "Content", key: "content" },
|
|
{ title: "Preview & Send", key: "preview" },
|
|
];
|
|
|
|
// Reactive data
|
|
const isEditMode = ref(!!route.query.id);
|
|
const showAudiencePreview = ref(false);
|
|
const testEmail = ref("");
|
|
const previewedUsers = ref([]);
|
|
const estimatedReachCount = ref(0);
|
|
|
|
// Loading states
|
|
const isLoadingCategories = ref(false);
|
|
const isLoadingTemplates = ref(false);
|
|
const isAudienceLoading = ref(false);
|
|
|
|
// Form data
|
|
const notificationForm = ref({
|
|
title: "",
|
|
type: "single",
|
|
priority: "medium",
|
|
category: "",
|
|
channels: [],
|
|
emailSubject: "",
|
|
expiresAt: "",
|
|
deliveryType: "immediate",
|
|
scheduledAt: "",
|
|
timezone: "UTC",
|
|
enableAbTesting: false,
|
|
abTestSplit: 50,
|
|
abTestName: "",
|
|
enableTracking: true,
|
|
audienceType: "all",
|
|
specificUsers: "",
|
|
userSegments: [],
|
|
userStatus: "",
|
|
registrationPeriod: "",
|
|
excludeUnsubscribed: true,
|
|
respectDoNotDisturb: true,
|
|
contentType: "new",
|
|
selectedTemplate: "",
|
|
pushTitle: "",
|
|
pushBody: "",
|
|
pushImageUrl: "",
|
|
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([]);
|
|
const templateOptions = ref([]);
|
|
|
|
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 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" },
|
|
{ label: "High-Value Customers", value: "high_value" },
|
|
{ label: "Mobile App Users", value: "mobile_users" },
|
|
];
|
|
|
|
const userStatusOptions = [
|
|
{ label: "Active", value: "active" },
|
|
{ label: "Inactive", value: "inactive" },
|
|
{ label: "Suspended", value: "suspended" },
|
|
{ label: "Premium", value: "premium" },
|
|
];
|
|
|
|
const registrationPeriodOptions = [
|
|
{ label: "Last 7 days", value: "last_7_days" },
|
|
{ label: "Last 30 days", value: "last_30_days" },
|
|
{ label: "Last 90 days", value: "last_90_days" },
|
|
{ label: "Last year", value: "last_year" },
|
|
];
|
|
|
|
const contentTypes = [
|
|
{ label: "Create New Content", value: "new" },
|
|
{ label: "Use Existing Template", value: "template" },
|
|
];
|
|
|
|
const personalizationVariables = [
|
|
"{{first_name}}",
|
|
"{{last_name}}",
|
|
"{{email}}",
|
|
"{{company_name}}",
|
|
"{{user_id}}",
|
|
"{{current_date}}",
|
|
];
|
|
|
|
// 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 selectedTemplateData = computed(() => {
|
|
if (!notificationForm.value.selectedTemplate) return null;
|
|
return (
|
|
templateOptions.value.find(
|
|
(template) => template.value === notificationForm.value.selectedTemplate
|
|
) || null
|
|
);
|
|
});
|
|
|
|
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)
|
|
);
|
|
case 2: // Scheduling
|
|
return (
|
|
notificationForm.value.deliveryType &&
|
|
(notificationForm.value.deliveryType === "immediate" ||
|
|
(notificationForm.value.scheduledAt && notificationForm.value.timezone))
|
|
);
|
|
case 3: // Target Audience
|
|
return (
|
|
notificationForm.value.audienceType &&
|
|
(notificationForm.value.audienceType !== "specific" ||
|
|
notificationForm.value.specificUsers.trim())
|
|
);
|
|
case 4: // 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))
|
|
);
|
|
case 5: // Preview & Send
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
});
|
|
|
|
// API Methods
|
|
const loadCategories = async () => {
|
|
try {
|
|
isLoadingCategories.value = true;
|
|
const response = await $fetch("/api/notifications/categories");
|
|
if (response.success) {
|
|
categoryOptions.value = response.data;
|
|
}
|
|
} catch (error) {
|
|
console.error("Error loading categories:", error);
|
|
$swal.fire("Error", "Failed to load categories", "error");
|
|
} finally {
|
|
isLoadingCategories.value = false;
|
|
}
|
|
};
|
|
|
|
const loadTemplates = async () => {
|
|
try {
|
|
isLoadingTemplates.value = true;
|
|
const response = await $fetch("/api/notifications/templates");
|
|
if (response.success) {
|
|
templateOptions.value = response.data;
|
|
}
|
|
} catch (error) {
|
|
console.error("Error loading templates:", error);
|
|
$swal.fire("Error", "Failed to load templates", "error");
|
|
} finally {
|
|
isLoadingTemplates.value = false;
|
|
}
|
|
};
|
|
|
|
const calculateEstimatedReach = async () => {
|
|
// Skip calculation for 'all' users to avoid unnecessary API calls
|
|
if (notificationForm.value.audienceType === "all") {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const requestBody = {
|
|
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,
|
|
};
|
|
|
|
const response = await $fetch("/api/notifications/audience-preview", {
|
|
method: "POST",
|
|
body: 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 insertVariable = (variable) => {
|
|
// Helper function to insert personalization variables
|
|
// This would need to be connected to actual text cursor position
|
|
console.log("Insert variable:", variable);
|
|
};
|
|
|
|
const previewAudience = async () => {
|
|
try {
|
|
isAudienceLoading.value = true;
|
|
|
|
const requestBody = {
|
|
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,
|
|
};
|
|
|
|
const response = await $fetch("/api/notifications/audience-preview", {
|
|
method: "POST",
|
|
body: requestBody,
|
|
});
|
|
|
|
if (response.success) {
|
|
previewedUsers.value = response.data.users;
|
|
estimatedReachCount.value = response.data.totalCount;
|
|
showAudiencePreview.value = true;
|
|
}
|
|
} catch (error) {
|
|
console.error("Error previewing audience:", error);
|
|
$swal.fire("Error", "Failed to preview audience", "error");
|
|
} finally {
|
|
isAudienceLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const sendTestNotification = async () => {
|
|
if (!testEmail.value) {
|
|
$swal.fire("Error", "Please enter a test email address", "error");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const 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 $fetch("/api/notifications/test-send", {
|
|
method: "POST",
|
|
body: {
|
|
email: testEmail.value,
|
|
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");
|
|
}
|
|
};
|
|
|
|
const saveDraft = async () => {
|
|
try {
|
|
const response = await $fetch("/api/notifications/draft", {
|
|
method: "POST",
|
|
body: notificationForm.value,
|
|
});
|
|
|
|
if (response.success) {
|
|
$swal.fire("Success", "Notification saved as draft", "success");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error saving draft:", error);
|
|
$swal.fire("Error", "Failed to save draft", "error");
|
|
}
|
|
};
|
|
|
|
const resetForm = () => {
|
|
$swal
|
|
.fire({
|
|
title: "Reset Form?",
|
|
text: "This will clear all entered data and return to step 1. Are you sure?",
|
|
icon: "warning",
|
|
showCancelButton: true,
|
|
confirmButtonText: "Yes, reset it!",
|
|
})
|
|
.then((result) => {
|
|
if (result.isConfirmed) {
|
|
// Reset form to initial state
|
|
Object.keys(notificationForm.value).forEach((key) => {
|
|
if (Array.isArray(notificationForm.value[key])) {
|
|
notificationForm.value[key] = [];
|
|
} else if (typeof notificationForm.value[key] === "boolean") {
|
|
notificationForm.value[key] =
|
|
key === "excludeUnsubscribed" ||
|
|
key === "respectDoNotDisturb" ||
|
|
key === "enableTracking";
|
|
} else if (typeof notificationForm.value[key] === "number") {
|
|
notificationForm.value[key] = key === "abTestSplit" ? 50 : 0;
|
|
} else {
|
|
notificationForm.value[key] = "";
|
|
}
|
|
});
|
|
|
|
// Reset specific defaults
|
|
notificationForm.value.type = "single";
|
|
notificationForm.value.priority = "medium";
|
|
notificationForm.value.deliveryType = "immediate";
|
|
notificationForm.value.timezone = "UTC";
|
|
notificationForm.value.audienceType = "all";
|
|
notificationForm.value.contentType = "new";
|
|
notificationForm.value.enableTracking = true;
|
|
notificationForm.value.excludeUnsubscribed = true;
|
|
notificationForm.value.respectDoNotDisturb = true;
|
|
|
|
currentStep.value = 1;
|
|
$swal.fire("Reset!", "Form has been reset.", "success");
|
|
}
|
|
});
|
|
};
|
|
|
|
const submitNotification = async (formData) => {
|
|
try {
|
|
const response = await $fetch("/api/notifications", {
|
|
method: "POST",
|
|
body: 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?.statusMessage || "Failed to process notification";
|
|
$swal.fire("Error", errorMessage, "error");
|
|
}
|
|
};
|
|
|
|
// Step navigation methods
|
|
const nextStep = () => {
|
|
if (currentStep.value < totalSteps.value && isCurrentStepValid.value) {
|
|
currentStep.value++;
|
|
}
|
|
};
|
|
|
|
const previousStep = () => {
|
|
if (currentStep.value > 1) {
|
|
currentStep.value--;
|
|
}
|
|
};
|
|
|
|
onMounted(async () => {
|
|
// Load dynamic data on component mount
|
|
await Promise.all([loadCategories(), loadTemplates()]);
|
|
|
|
// Load data if editing existing notification
|
|
if (isEditMode.value) {
|
|
// Mock loading existing notification data
|
|
console.log("Loading notification for editing...");
|
|
}
|
|
});
|
|
|
|
// Watchers for automatic estimated reach calculation
|
|
watch(
|
|
() => [
|
|
notificationForm.value.audienceType,
|
|
notificationForm.value.specificUsers,
|
|
notificationForm.value.userSegments,
|
|
notificationForm.value.userStatus,
|
|
notificationForm.value.registrationPeriod,
|
|
notificationForm.value.excludeUnsubscribed,
|
|
notificationForm.value.channels,
|
|
],
|
|
() => {
|
|
// Debounce the calculation to avoid too many API calls
|
|
clearTimeout(window.reachCalculationTimeout);
|
|
window.reachCalculationTimeout = setTimeout(() => {
|
|
calculateEstimatedReach();
|
|
}, 500);
|
|
},
|
|
{ deep: true }
|
|
);
|
|
</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>
|