1358 lines
48 KiB
Vue
1358 lines
48 KiB
Vue
<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>
|
|
{{ isEditing ? "Edit Template" : "Create Template" }}
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<p class="mb-4">
|
|
{{
|
|
isEditing
|
|
? "Edit and update your notification template. Modify content, settings, and channel configurations to improve effectiveness."
|
|
: "Create a new notification template for multiple channels. Design reusable templates with dynamic content and variable placeholders for personalized communications."
|
|
}}
|
|
</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">
|
|
{{
|
|
isEditing ? "Loading template details..." : "Initializing template creator..."
|
|
}}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Main Form Card -->
|
|
<rs-card v-else>
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-xl font-semibold">
|
|
{{ isEditing ? "Edit" : "Create" }} Notification Template
|
|
</h2>
|
|
<div class="flex gap-3">
|
|
<rs-button @click="$router.push('/notification/templates')" variant="outline">
|
|
<Icon name="ic:outline-arrow-back" class="mr-1"></Icon>
|
|
Back to Templates
|
|
</rs-button>
|
|
<rs-button
|
|
@click="previewTemplate"
|
|
variant="info-outline"
|
|
:disabled="!templateForm.content || isSubmitting"
|
|
>
|
|
<Icon name="material-symbols:preview-outline" class="mr-1"></Icon>
|
|
Preview
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<div class="pt-2">
|
|
<FormKit
|
|
type="form"
|
|
@submit="submitTemplate"
|
|
:actions="false"
|
|
class="w-full max-w-6xl mx-auto"
|
|
>
|
|
<!-- Basic Information Section -->
|
|
<div class="space-y-6 mb-8">
|
|
<div class="border-b border-gray-200 dark:border-gray-700 pb-4">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1">
|
|
Basic Information
|
|
</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
Configure the basic template settings and metadata
|
|
</p>
|
|
</div>
|
|
|
|
<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="Template Name"
|
|
placeholder="e.g., Welcome Email Template"
|
|
validation="required"
|
|
v-model="templateForm.title"
|
|
help="Internal name for identifying this template"
|
|
/>
|
|
|
|
<FormKit
|
|
type="textarea"
|
|
name="description"
|
|
label="Description"
|
|
placeholder="Brief description of template purpose and usage"
|
|
v-model="templateForm.description"
|
|
rows="3"
|
|
help="Optional description to help team members understand the template's use"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
name="category"
|
|
label="Category"
|
|
placeholder="Select category"
|
|
:options="categoryOptions"
|
|
validation="required"
|
|
v-model="templateForm.category"
|
|
help="Organize templates by category for better management"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Right Column -->
|
|
<div class="space-y-4">
|
|
<FormKit
|
|
type="checkbox"
|
|
name="channels"
|
|
label="Supported Channels"
|
|
:options="channelOptions"
|
|
validation="required|min:1"
|
|
v-model="templateForm.channels"
|
|
decorator-icon="material-symbols:check"
|
|
options-class="grid grid-cols-2 gap-x-3 gap-y-2 pt-2"
|
|
help="Select which channels this template supports"
|
|
/>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormKit
|
|
type="select"
|
|
name="status"
|
|
label="Status"
|
|
placeholder="Select status"
|
|
:options="statusOptions"
|
|
validation="required"
|
|
v-model="templateForm.status"
|
|
help="Template status - only active templates can be used"
|
|
outer-class="mb-0"
|
|
/>
|
|
|
|
<FormKit
|
|
type="text"
|
|
name="version"
|
|
label="Version"
|
|
placeholder="1.0"
|
|
v-model="templateForm.version"
|
|
help="Version number for tracking template changes"
|
|
outer-class="mb-0"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content Configuration Section -->
|
|
<div class="space-y-6 mb-8">
|
|
<div class="border-b border-gray-200 dark:border-gray-700 pb-4">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1">
|
|
Content Configuration
|
|
</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
Define subject lines and content for different channels
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Subject/Title Section -->
|
|
<div class="space-y-4">
|
|
<FormKit
|
|
type="text"
|
|
name="subject"
|
|
label="Subject Line / Notification Title"
|
|
placeholder="e.g., Welcome to {{company_name}}, {{first_name}}!"
|
|
validation="required"
|
|
v-model="templateForm.subject"
|
|
help="Subject for emails or title for push notifications. Use {{variable}} for dynamic content"
|
|
/>
|
|
|
|
<FormKit
|
|
type="text"
|
|
name="preheader"
|
|
label="Email Preheader Text (Optional)"
|
|
placeholder="Additional preview text for emails"
|
|
v-model="templateForm.preheader"
|
|
help="Preview text shown in email clients alongside the subject line"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Content Editor Section -->
|
|
<div class="space-y-4">
|
|
<FormKit
|
|
type="textarea"
|
|
name="content"
|
|
label="Template Content"
|
|
validation="required"
|
|
v-model="templateForm.content"
|
|
help="Main content body. Use HTML for rich formatting and {{variable}} for dynamic content"
|
|
>
|
|
<template #input="{ value, onChange }">
|
|
<div
|
|
class="editor-wrapper border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden"
|
|
>
|
|
<!-- Editor Toolbar -->
|
|
<div
|
|
class="menubar bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600"
|
|
>
|
|
<div class="flex items-center justify-between p-3">
|
|
<div class="flex items-center gap-1 flex-wrap">
|
|
<!-- Text Formatting -->
|
|
<div class="flex items-center gap-1 mr-4">
|
|
<button
|
|
type="button"
|
|
class="menu-button"
|
|
:class="{ 'is-active': editor?.isActive('bold') }"
|
|
@click="editor?.chain().focus().toggleBold().run()"
|
|
title="Bold"
|
|
>
|
|
<Icon
|
|
name="material-symbols:format-bold"
|
|
class="w-4 h-4"
|
|
/>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="menu-button"
|
|
:class="{ 'is-active': editor?.isActive('italic') }"
|
|
@click="editor?.chain().focus().toggleItalic().run()"
|
|
title="Italic"
|
|
>
|
|
<Icon
|
|
name="material-symbols:format-italic"
|
|
class="w-4 h-4"
|
|
/>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="menu-button"
|
|
:class="{ 'is-active': editor?.isActive('underline') }"
|
|
@click="editor?.chain().focus().toggleUnderline().run()"
|
|
title="Underline"
|
|
>
|
|
<Icon
|
|
name="material-symbols:format-underlined"
|
|
class="w-4 h-4"
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="w-px h-6 bg-gray-300 dark:bg-gray-600 mx-2"></div>
|
|
|
|
<!-- Headings -->
|
|
<div class="flex items-center gap-1 mr-4">
|
|
<button
|
|
type="button"
|
|
class="menu-button"
|
|
:class="{
|
|
'is-active': editor?.isActive('heading', { level: 1 }),
|
|
}"
|
|
@click="
|
|
editor
|
|
?.chain()
|
|
.focus()
|
|
.toggleHeading({ level: 1 })
|
|
.run()
|
|
"
|
|
title="Heading 1"
|
|
>
|
|
<Icon name="material-symbols:format-h1" class="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="menu-button"
|
|
:class="{
|
|
'is-active': editor?.isActive('heading', { level: 2 }),
|
|
}"
|
|
@click="
|
|
editor
|
|
?.chain()
|
|
.focus()
|
|
.toggleHeading({ level: 2 })
|
|
.run()
|
|
"
|
|
title="Heading 2"
|
|
>
|
|
<Icon name="material-symbols:format-h2" class="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="menu-button"
|
|
:class="{ 'is-active': editor?.isActive('paragraph') }"
|
|
@click="editor?.chain().focus().setParagraph().run()"
|
|
title="Paragraph"
|
|
>
|
|
<Icon
|
|
name="material-symbols:format-paragraph"
|
|
class="w-4 h-4"
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="w-px h-6 bg-gray-300 dark:bg-gray-600 mx-2"></div>
|
|
|
|
<!-- Lists -->
|
|
<div class="flex items-center gap-1 mr-4">
|
|
<button
|
|
type="button"
|
|
class="menu-button"
|
|
:class="{ 'is-active': editor?.isActive('bulletList') }"
|
|
@click="editor?.chain().focus().toggleBulletList().run()"
|
|
title="Bullet List"
|
|
>
|
|
<Icon
|
|
name="material-symbols:format-list-bulleted"
|
|
class="w-4 h-4"
|
|
/>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="menu-button"
|
|
:class="{ 'is-active': editor?.isActive('orderedList') }"
|
|
@click="editor?.chain().focus().toggleOrderedList().run()"
|
|
title="Numbered List"
|
|
>
|
|
<Icon
|
|
name="material-symbols:format-list-numbered"
|
|
class="w-4 h-4"
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="w-px h-6 bg-gray-300 dark:bg-gray-600 mx-2"></div>
|
|
|
|
<!-- Additional Tools -->
|
|
<div class="flex items-center gap-1">
|
|
<button
|
|
type="button"
|
|
class="menu-button"
|
|
@click="addLink"
|
|
title="Add Link"
|
|
>
|
|
<Icon name="material-symbols:link" class="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="menu-button"
|
|
@click="editor?.chain().focus().setHorizontalRule().run()"
|
|
title="Horizontal Line"
|
|
>
|
|
<Icon
|
|
name="material-symbols:horizontal-rule"
|
|
class="w-4 h-4"
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Variables Button -->
|
|
<div class="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
class="flex items-center gap-2 px-3 py-1.5 rounded text-sm bg-primary/10 text-primary hover:bg-primary/20 transition-colors font-medium"
|
|
@click="showVariableHelper = true"
|
|
title="Insert Variable"
|
|
>
|
|
<Icon name="material-symbols:code" class="w-4 h-4" />
|
|
<span class="hidden sm:inline">Insert Variable</span>
|
|
<span class="sm:hidden">Vars</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Editor Content -->
|
|
<editor-content
|
|
:editor="editor"
|
|
class="editor-content min-h-[300px] max-h-[500px] overflow-y-auto p-4 bg-white dark:bg-gray-800 prose prose-sm max-w-none dark:prose-invert focus-within:ring-2 focus-within:ring-primary/20 transition-all"
|
|
@update:model-value="
|
|
(val) => {
|
|
templateForm.content = val;
|
|
onChange(val);
|
|
}
|
|
"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</FormKit>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Channel-Specific Settings -->
|
|
<div class="space-y-6 mb-8" v-if="templateForm.channels.length > 0">
|
|
<div class="border-b border-gray-200 dark:border-gray-700 pb-4">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1">
|
|
Channel-Specific Settings
|
|
</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
Configure settings specific to each selected channel
|
|
</p>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<!-- Email Settings -->
|
|
<div v-if="templateForm.channels.includes('email')" class="space-y-4">
|
|
<div class="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
|
|
<h4
|
|
class="font-semibold text-blue-800 dark:text-blue-200 mb-3 flex items-center"
|
|
>
|
|
<Icon name="material-symbols:mail-outline" class="mr-2" />
|
|
Email Settings
|
|
</h4>
|
|
<div class="space-y-3">
|
|
<FormKit
|
|
type="text"
|
|
name="fromName"
|
|
label="From Name"
|
|
placeholder="Your Company Name"
|
|
v-model="templateForm.fromName"
|
|
outer-class="mb-2"
|
|
/>
|
|
<FormKit
|
|
type="text"
|
|
name="replyTo"
|
|
label="Reply-To Email"
|
|
placeholder="noreply@yourcompany.com"
|
|
v-model="templateForm.replyTo"
|
|
outer-class="mb-2"
|
|
/>
|
|
<FormKit
|
|
type="checkbox"
|
|
name="trackOpens"
|
|
label="Track Email Opens"
|
|
v-model="templateForm.trackOpens"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Push Notification Settings -->
|
|
<div v-if="templateForm.channels.includes('push')" class="space-y-4">
|
|
<div class="bg-green-50 dark:bg-green-900/20 p-4 rounded-lg">
|
|
<h4
|
|
class="font-semibold text-green-800 dark:text-green-200 mb-3 flex items-center"
|
|
>
|
|
<Icon name="material-symbols:notifications-outline" class="mr-2" />
|
|
Push Notification Settings
|
|
</h4>
|
|
<div class="space-y-3">
|
|
<FormKit
|
|
type="text"
|
|
name="pushTitle"
|
|
label="Push Title"
|
|
placeholder="Custom title for push notifications"
|
|
v-model="templateForm.pushTitle"
|
|
outer-class="mb-2"
|
|
/>
|
|
<FormKit
|
|
type="text"
|
|
name="pushIcon"
|
|
label="Push Icon URL"
|
|
placeholder="https://example.com/icon.png"
|
|
v-model="templateForm.pushIcon"
|
|
outer-class="mb-2"
|
|
/>
|
|
<FormKit
|
|
type="text"
|
|
name="pushUrl"
|
|
label="Click Action URL"
|
|
placeholder="https://example.com/action"
|
|
v-model="templateForm.pushUrl"
|
|
outer-class="mb-2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- SMS Settings -->
|
|
<div v-if="templateForm.channels.includes('sms')" class="space-y-4">
|
|
<div class="bg-purple-50 dark:bg-purple-900/20 p-4 rounded-lg">
|
|
<h4
|
|
class="font-semibold text-purple-800 dark:text-purple-200 mb-3 flex items-center"
|
|
>
|
|
<Icon name="material-symbols:sms-outline" class="mr-2" />
|
|
SMS Settings
|
|
</h4>
|
|
<div class="space-y-3">
|
|
<FormKit
|
|
type="textarea"
|
|
name="smsContent"
|
|
label="SMS Content"
|
|
placeholder="SMS version of your message (160 characters max)"
|
|
v-model="templateForm.smsContent"
|
|
rows="3"
|
|
outer-class="mb-2"
|
|
/>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
|
Characters: {{ (templateForm.smsContent || "").length }}/160
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Advanced Settings -->
|
|
<div class="space-y-6 mb-8">
|
|
<div class="border-b border-gray-200 dark:border-gray-700 pb-4">
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1">
|
|
Advanced Settings
|
|
</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
Additional configuration options and metadata
|
|
</p>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<div class="space-y-4">
|
|
<FormKit
|
|
type="text"
|
|
name="tags"
|
|
label="Tags (Optional)"
|
|
placeholder="welcome, onboarding, user-management"
|
|
v-model="templateForm.tags"
|
|
help="Comma-separated tags for better organization"
|
|
/>
|
|
|
|
<FormKit
|
|
type="checkbox"
|
|
name="isPersonal"
|
|
label="Contains Personal Information"
|
|
v-model="templateForm.isPersonal"
|
|
help="Mark if template contains sensitive personal data"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div
|
|
class="flex justify-end items-center gap-4 pt-6 border-t border-gray-200 dark:border-gray-700"
|
|
>
|
|
<rs-button
|
|
type="button"
|
|
variant="outline"
|
|
@click="saveDraft"
|
|
:disabled="!templateForm.title || isSubmitting"
|
|
>
|
|
<Icon
|
|
:name="
|
|
isSubmitting ? 'ic:outline-refresh' : 'material-symbols:save-outline'
|
|
"
|
|
class="mr-1"
|
|
:class="{ 'animate-spin': isSubmitting }"
|
|
/>
|
|
{{ isSubmitting ? "Saving..." : "Save as Draft" }}
|
|
</rs-button>
|
|
<rs-button
|
|
type="button"
|
|
variant="info-outline"
|
|
@click="previewTemplate"
|
|
:disabled="!templateForm.content || isSubmitting"
|
|
>
|
|
<Icon name="material-symbols:preview-outline" class="mr-1" />
|
|
Preview
|
|
</rs-button>
|
|
<button
|
|
type="submit"
|
|
@click="handleCreateClick"
|
|
:disabled="!isFormValid || isSubmitting"
|
|
class="inline-flex items-center px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<Icon
|
|
:name="
|
|
isSubmitting
|
|
? 'ic:outline-refresh'
|
|
: 'material-symbols:check-circle-outline'
|
|
"
|
|
class="mr-1"
|
|
:class="{ 'animate-spin': isSubmitting }"
|
|
/>
|
|
{{
|
|
isSubmitting
|
|
? isEditing
|
|
? "Updating..."
|
|
: "Creating..."
|
|
: isEditing
|
|
? "Update Template"
|
|
: "Create Template"
|
|
}}
|
|
</button>
|
|
</div>
|
|
</FormKit>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
|
|
<!-- Preview Modal -->
|
|
<rs-modal v-model="showPreviewModal" title="Template Preview" size="lg">
|
|
<div class="space-y-6">
|
|
<!-- Preview Options -->
|
|
<div class="flex gap-4 pb-4 border-b border-gray-200 dark:border-gray-700">
|
|
<FormKit
|
|
type="select"
|
|
name="previewChannel"
|
|
label="Channel"
|
|
:options="
|
|
channelOptions.filter((c) => templateForm.channels.includes(c.value))
|
|
"
|
|
v-model="previewChannel"
|
|
outer-class="flex-1 mb-0"
|
|
/>
|
|
<FormKit
|
|
type="select"
|
|
name="previewDevice"
|
|
label="Device"
|
|
:options="deviceOptions"
|
|
v-model="previewDevice"
|
|
outer-class="flex-1 mb-0"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Preview Content -->
|
|
<div class="preview-container">
|
|
<div
|
|
v-if="previewChannel === 'email'"
|
|
class="email-preview border rounded-lg p-4 bg-white dark:bg-gray-800"
|
|
>
|
|
<div
|
|
class="email-header mb-4 pb-3 border-b border-gray-200 dark:border-gray-700"
|
|
>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">
|
|
From: {{ templateForm.fromName || "Your Company" }}
|
|
</div>
|
|
<div class="font-semibold text-lg">
|
|
{{ processTemplate(templateForm.subject) }}
|
|
</div>
|
|
<div
|
|
v-if="templateForm.preheader"
|
|
class="text-sm text-gray-500 dark:text-gray-400"
|
|
>
|
|
{{ processTemplate(templateForm.preheader) }}
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="prose prose-sm max-w-none dark:prose-invert"
|
|
v-html="processTemplate(templateForm.content)"
|
|
></div>
|
|
</div>
|
|
|
|
<div v-else-if="previewChannel === 'push'" class="push-preview">
|
|
<div
|
|
class="bg-white dark:bg-gray-800 border rounded-lg p-4 max-w-sm mx-auto shadow-lg"
|
|
>
|
|
<div class="flex items-start gap-3">
|
|
<div
|
|
class="w-10 h-10 bg-primary rounded-lg flex items-center justify-center flex-shrink-0"
|
|
>
|
|
<Icon name="material-symbols:notifications" class="text-white" />
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="font-semibold text-sm truncate">
|
|
{{ processTemplate(templateForm.pushTitle || templateForm.subject) }}
|
|
</div>
|
|
<div class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
|
{{ processTemplate(stripHtml(templateForm.content)) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else-if="previewChannel === 'sms'" class="sms-preview">
|
|
<div class="bg-white dark:bg-gray-800 border rounded-lg p-4 max-w-sm mx-auto">
|
|
<div class="text-sm">
|
|
{{
|
|
processTemplate(
|
|
templateForm.smsContent || stripHtml(templateForm.content)
|
|
)
|
|
}}
|
|
</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
|
SMS •
|
|
{{
|
|
(templateForm.smsContent || stripHtml(templateForm.content)).length
|
|
}}/160 chars
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sample Variables -->
|
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
|
|
<h4 class="font-semibold text-sm text-gray-700 dark:text-gray-300 mb-2">
|
|
Sample Variables Used:
|
|
</h4>
|
|
<div class="grid grid-cols-2 gap-2 text-xs">
|
|
<div>{{ first_name }}: John</div>
|
|
<div>{{ last_name }}: Doe</div>
|
|
<div>{{ company_name }}: Acme Corp</div>
|
|
<div>{{ email }}: john@example.com</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<div class="flex justify-end gap-3">
|
|
<rs-button @click="showPreviewModal = false" variant="outline">Close</rs-button>
|
|
<rs-button @click="sendTestNotification" variant="info">
|
|
<Icon name="material-symbols:send" class="mr-1" />
|
|
Send Test
|
|
</rs-button>
|
|
</div>
|
|
</template>
|
|
</rs-modal>
|
|
|
|
<!-- Variable Helper Modal -->
|
|
<rs-modal v-model="showVariableHelper" title="Insert Variable" size="md">
|
|
<div class="space-y-4">
|
|
<div class="grid grid-cols-1 gap-2">
|
|
<div
|
|
v-for="variable in commonVariables"
|
|
:key="variable.key"
|
|
class="flex items-center justify-between p-3 border rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800"
|
|
@click="insertVariable(variable.key)"
|
|
>
|
|
<div>
|
|
<div class="font-medium">{{ variable.name }}</div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
|
{{ variable.description }}
|
|
</div>
|
|
</div>
|
|
<code class="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">
|
|
{{ formatVariable(variable.key) }}
|
|
</code>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<template #footer>
|
|
<rs-button @click="showVariableHelper = false">Close</rs-button>
|
|
</template>
|
|
</rs-modal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { useEditor, EditorContent } from "@tiptap/vue-3";
|
|
import StarterKit from "@tiptap/starter-kit";
|
|
import Underline from "@tiptap/extension-underline";
|
|
import { ref, computed, onMounted, watch } from "vue";
|
|
import { useRoute, useRouter } from "vue-router";
|
|
|
|
definePageMeta({
|
|
title: "Create Template",
|
|
middleware: ["auth"],
|
|
requiresAuth: true,
|
|
breadcrumb: [
|
|
{
|
|
name: "Dashboard",
|
|
path: "/dashboard",
|
|
},
|
|
{
|
|
name: "Notification",
|
|
path: "/notification",
|
|
},
|
|
{
|
|
name: "Templates",
|
|
path: "/notification/templates",
|
|
},
|
|
{
|
|
name: "Create",
|
|
path: "/notification/templates/create_template",
|
|
type: "current",
|
|
},
|
|
],
|
|
});
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const { $swal } = useNuxtApp();
|
|
|
|
const isEditing = computed(() => !!route.query.id);
|
|
const templateId = computed(() => (route.query.id ? parseInt(route.query.id) : null));
|
|
const isLoading = ref(false);
|
|
const isSubmitting = ref(false);
|
|
const showPreviewModal = ref(false);
|
|
const showVariableHelper = ref(false);
|
|
const previewChannel = ref("email");
|
|
const previewDevice = ref("desktop");
|
|
|
|
const templateForm = ref({
|
|
title: "",
|
|
description: "",
|
|
subject: "",
|
|
preheader: "",
|
|
category: "",
|
|
channels: [],
|
|
status: "Draft",
|
|
version: "1.0",
|
|
content: "",
|
|
tags: "",
|
|
isPersonal: false,
|
|
// Channel-specific settings
|
|
fromName: "",
|
|
replyTo: "",
|
|
trackOpens: true,
|
|
pushTitle: "",
|
|
pushIcon: "",
|
|
pushUrl: "",
|
|
smsContent: "",
|
|
});
|
|
|
|
// Form validation
|
|
const isFormValid = computed(() => {
|
|
const valid =
|
|
templateForm.value.title &&
|
|
templateForm.value.subject &&
|
|
templateForm.value.content &&
|
|
templateForm.value.category &&
|
|
templateForm.value.channels.length > 0;
|
|
|
|
console.log("Form validation check:", {
|
|
title: !!templateForm.value.title,
|
|
subject: !!templateForm.value.subject,
|
|
content: !!templateForm.value.content,
|
|
category: !!templateForm.value.category,
|
|
channels: templateForm.value.channels.length,
|
|
isValid: valid,
|
|
});
|
|
|
|
return valid;
|
|
});
|
|
|
|
// Options for form fields
|
|
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: "SMS", value: "sms" },
|
|
{ label: "Push Notification", value: "push" },
|
|
];
|
|
|
|
const statusOptions = [
|
|
{ label: "Draft", value: "Draft" },
|
|
{ label: "Active", value: "Active" },
|
|
{ label: "Archived", value: "Archived" },
|
|
];
|
|
|
|
const deviceOptions = [
|
|
{ label: "Desktop", value: "desktop" },
|
|
{ label: "Mobile", value: "mobile" },
|
|
{ label: "Tablet", value: "tablet" },
|
|
];
|
|
|
|
const commonVariables = [
|
|
{ key: "first_name", name: "First Name", description: "User's first name" },
|
|
{ key: "last_name", name: "Last Name", description: "User's last name" },
|
|
{ key: "full_name", name: "Full Name", description: "User's full name" },
|
|
{ key: "email", name: "Email", description: "User's email address" },
|
|
{
|
|
key: "company_name",
|
|
name: "Company Name",
|
|
description: "Company or organization name",
|
|
},
|
|
{ key: "order_id", name: "Order ID", description: "Order number or identifier" },
|
|
{ key: "reset_link", name: "Reset Link", description: "Password reset link" },
|
|
{
|
|
key: "verification_link",
|
|
name: "Verification Link",
|
|
description: "Email verification link",
|
|
},
|
|
{ key: "current_date", name: "Current Date", description: "Today's date" },
|
|
{ key: "current_time", name: "Current Time", description: "Current time" },
|
|
];
|
|
|
|
// TipTap Editor Setup
|
|
const editor = useEditor({
|
|
content: templateForm.value.content,
|
|
extensions: [StarterKit, Underline],
|
|
editorProps: {
|
|
attributes: {
|
|
class: "prose prose-sm dark:prose-invert focus:outline-none max-w-none",
|
|
"data-placeholder":
|
|
"Start typing your template content here... Use {{variable_name}} for dynamic content.",
|
|
},
|
|
},
|
|
onUpdate: ({ editor: currentEditor }) => {
|
|
templateForm.value.content = currentEditor.getHTML();
|
|
},
|
|
});
|
|
|
|
// Helper functions
|
|
const stripHtml = (html) => {
|
|
const tmp = document.createElement("div");
|
|
tmp.innerHTML = html;
|
|
return tmp.textContent || tmp.innerText || "";
|
|
};
|
|
|
|
const formatVariable = (variableKey) => {
|
|
return `{{${variableKey}}}`;
|
|
};
|
|
|
|
const processTemplate = (text) => {
|
|
if (!text) return "";
|
|
return text
|
|
.replace(/\{\{first_name\}\}/g, "John")
|
|
.replace(/\{\{last_name\}\}/g, "Doe")
|
|
.replace(/\{\{full_name\}\}/g, "John Doe")
|
|
.replace(/\{\{email\}\}/g, "john@example.com")
|
|
.replace(/\{\{company_name\}\}/g, "Acme Corp")
|
|
.replace(/\{\{order_id\}\}/g, "#12345")
|
|
.replace(/\{\{current_date\}\}/g, new Date().toLocaleDateString())
|
|
.replace(/\{\{current_time\}\}/g, new Date().toLocaleTimeString());
|
|
};
|
|
|
|
const insertVariable = (variable) => {
|
|
if (editor.value) {
|
|
editor.value.chain().focus().insertContent(`{{${variable}}}`).run();
|
|
}
|
|
showVariableHelper.value = false;
|
|
};
|
|
|
|
const addLink = () => {
|
|
const url = prompt("Enter the URL");
|
|
if (url && editor.value) {
|
|
editor.value.chain().focus().setLink({ href: url }).run();
|
|
}
|
|
};
|
|
|
|
// Button click handler for debugging
|
|
const handleCreateClick = (event) => {
|
|
console.log("Create button clicked!", event);
|
|
console.log("Button disabled state:", !isFormValid.value || isSubmitting.value);
|
|
|
|
// If this is inside a form, prevent default form submission for testing
|
|
// event.preventDefault();
|
|
|
|
// Call submit function directly for testing
|
|
if (!(!isFormValid.value || isSubmitting.value)) {
|
|
console.log("Calling submitTemplate directly from button click");
|
|
event.preventDefault();
|
|
submitTemplate();
|
|
}
|
|
};
|
|
|
|
// Actions
|
|
const previewTemplate = () => {
|
|
if (!templateForm.value.channels.length) {
|
|
$swal.fire("Error", "Please select at least one channel to preview", "error");
|
|
return;
|
|
}
|
|
previewChannel.value = templateForm.value.channels[0];
|
|
showPreviewModal.value = true;
|
|
};
|
|
|
|
const saveDraft = async () => {
|
|
if (!templateForm.value.title) {
|
|
$swal.fire("Error", "Please enter a template name", "error");
|
|
return;
|
|
}
|
|
|
|
templateForm.value.status = "Draft";
|
|
await submitTemplate();
|
|
};
|
|
|
|
const sendTestNotification = async () => {
|
|
// Validate required fields before sending test
|
|
if (
|
|
!templateForm.value.title ||
|
|
!templateForm.value.subject ||
|
|
!templateForm.value.content
|
|
) {
|
|
$swal.fire({
|
|
title: "Missing Information",
|
|
text:
|
|
"Please fill in at least the title, subject, and content before sending a test notification.",
|
|
icon: "warning",
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!templateForm.value.channels.length) {
|
|
$swal.fire({
|
|
title: "No Channels Selected",
|
|
text: "Please select at least one notification channel before sending a test.",
|
|
icon: "warning",
|
|
});
|
|
return;
|
|
}
|
|
|
|
const { value: email } = await $swal.fire({
|
|
title: "Send Test Notification",
|
|
input: "email",
|
|
inputLabel: "Enter email address for test",
|
|
inputPlaceholder: "test@example.com",
|
|
showCancelButton: true,
|
|
inputValidator: (value) => {
|
|
if (!value) {
|
|
return "Please enter an email address";
|
|
}
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(value)) {
|
|
return "Please enter a valid email address";
|
|
}
|
|
},
|
|
});
|
|
|
|
if (email) {
|
|
// Show loading state
|
|
const loadingSwal = $swal.fire({
|
|
title: "Sending Test...",
|
|
text: "Please wait while we send your test notification",
|
|
allowOutsideClick: false,
|
|
allowEscapeKey: false,
|
|
showConfirmButton: false,
|
|
didOpen: () => {
|
|
$swal.showLoading();
|
|
},
|
|
});
|
|
|
|
try {
|
|
// Prepare test data
|
|
const testData = {
|
|
title: templateForm.value.title,
|
|
channels: templateForm.value.channels,
|
|
emailSubject: templateForm.value.subject,
|
|
emailContent: templateForm.value.content,
|
|
pushTitle: templateForm.value.pushTitle || templateForm.value.subject,
|
|
pushBody: templateForm.value.content
|
|
? templateForm.value.content.replace(/<[^>]*>/g, "").substring(0, 300)
|
|
: "",
|
|
callToActionText: "",
|
|
callToActionUrl: "",
|
|
};
|
|
|
|
console.log("Sending test notification with data:", testData);
|
|
|
|
// Call the test API
|
|
const response = await $fetch("/api/notifications/test-send", {
|
|
method: "POST",
|
|
body: {
|
|
email: email,
|
|
testData: testData,
|
|
},
|
|
});
|
|
|
|
console.log("Test API Response:", response);
|
|
|
|
// Close loading modal
|
|
loadingSwal.close();
|
|
|
|
// Show success message with results
|
|
let successMessage = `Test notification sent to ${email}`;
|
|
if (response.data?.results) {
|
|
const results = response.data.results;
|
|
const successfulChannels = results
|
|
.filter((r) => r.status === "sent")
|
|
.map((r) => r.channel);
|
|
const failedChannels = results.filter((r) => r.status === "failed");
|
|
|
|
if (successfulChannels.length > 0) {
|
|
successMessage += `\n\nSuccessfully sent via: ${successfulChannels.join(", ")}`;
|
|
}
|
|
|
|
if (failedChannels.length > 0) {
|
|
successMessage += `\n\nFailed channels: ${failedChannels
|
|
.map((f) => `${f.channel} (${f.message})`)
|
|
.join(", ")}`;
|
|
}
|
|
}
|
|
|
|
await $swal.fire({
|
|
title: "Test Sent!",
|
|
text: successMessage,
|
|
icon: "success",
|
|
timer: 4000,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error sending test notification:", error);
|
|
|
|
// Close loading modal
|
|
loadingSwal.close();
|
|
|
|
// Handle errors
|
|
let errorMessage = "Failed to send test notification. Please try again.";
|
|
if (error.data?.error) {
|
|
errorMessage = error.data.error;
|
|
} else if (error.message) {
|
|
errorMessage = error.message;
|
|
}
|
|
|
|
await $swal.fire({
|
|
title: "Test Failed",
|
|
text: errorMessage,
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const submitTemplate = async () => {
|
|
console.log("submitTemplate function called!");
|
|
console.log("Form validation status:", isFormValid.value);
|
|
console.log("Template form data:", templateForm.value);
|
|
|
|
if (!isFormValid.value) {
|
|
$swal.fire("Error", "Please fill in all required fields", "error");
|
|
return;
|
|
}
|
|
|
|
// Prevent double submission
|
|
if (isSubmitting.value) {
|
|
return;
|
|
}
|
|
|
|
isSubmitting.value = true;
|
|
|
|
// Show loading state
|
|
const loadingSwal = $swal.fire({
|
|
title: isEditing.value ? "Updating Template..." : "Creating Template...",
|
|
text: "Please wait while we process your request",
|
|
allowOutsideClick: false,
|
|
allowEscapeKey: false,
|
|
showConfirmButton: false,
|
|
didOpen: () => {
|
|
$swal.showLoading();
|
|
},
|
|
});
|
|
|
|
try {
|
|
// Prepare the data for API submission
|
|
const apiData = {
|
|
title: templateForm.value.title,
|
|
description: templateForm.value.description || "",
|
|
subject: templateForm.value.subject,
|
|
preheader: templateForm.value.preheader || "",
|
|
category: templateForm.value.category,
|
|
channels: templateForm.value.channels,
|
|
status: templateForm.value.status,
|
|
version: templateForm.value.version,
|
|
content: templateForm.value.content,
|
|
tags: templateForm.value.tags || "",
|
|
isPersonal: templateForm.value.isPersonal,
|
|
// Email specific settings
|
|
fromName: templateForm.value.fromName || "",
|
|
replyTo: templateForm.value.replyTo || "",
|
|
trackOpens: templateForm.value.trackOpens,
|
|
// Push notification specific settings
|
|
pushTitle: templateForm.value.pushTitle || "",
|
|
pushIcon: templateForm.value.pushIcon || "",
|
|
pushUrl: templateForm.value.pushUrl || "",
|
|
// SMS specific settings
|
|
smsContent: templateForm.value.smsContent || "",
|
|
};
|
|
|
|
console.log("Submitting Template Data:", apiData);
|
|
|
|
let response;
|
|
if (isEditing.value) {
|
|
// TODO: Implement update API call when needed
|
|
response = await $fetch(`/api/notifications/templates/${templateId.value}`, {
|
|
method: "PUT",
|
|
body: apiData,
|
|
});
|
|
} else {
|
|
// Create new template
|
|
response = await $fetch("/api/notifications/templates/create", {
|
|
method: "POST",
|
|
body: apiData,
|
|
});
|
|
}
|
|
|
|
console.log("API Response:", response);
|
|
|
|
// Close loading modal
|
|
loadingSwal.close();
|
|
isSubmitting.value = false;
|
|
|
|
// Show success message
|
|
await $swal.fire({
|
|
title: isEditing.value ? "Template Updated!" : "Template Created!",
|
|
text: `Template "${templateForm.value.title}" has been successfully ${
|
|
isEditing.value ? "updated" : "created"
|
|
}.`,
|
|
icon: "success",
|
|
timer: 3000,
|
|
showConfirmButton: false,
|
|
});
|
|
|
|
// Navigate back to templates list
|
|
// router.push("/notification/templates");
|
|
} catch (error) {
|
|
console.error("Error submitting template:", error);
|
|
|
|
// Close loading modal
|
|
loadingSwal.close();
|
|
isSubmitting.value = false;
|
|
|
|
// Handle different types of errors
|
|
let errorMessage = "Failed to save template. Please try again.";
|
|
let errorDetails = "";
|
|
|
|
if (error.data) {
|
|
if (error.data.errors && Array.isArray(error.data.errors)) {
|
|
// Validation errors
|
|
errorMessage = "Please fix the following errors:";
|
|
errorDetails = error.data.errors.join("\n• ");
|
|
} else if (error.data.error) {
|
|
// Single error message
|
|
errorMessage = error.data.error;
|
|
}
|
|
} else if (error.message) {
|
|
errorMessage = error.message;
|
|
}
|
|
|
|
// Show error message
|
|
await $swal.fire({
|
|
title: "Error",
|
|
text: errorMessage,
|
|
html: errorDetails
|
|
? `<p>${errorMessage}</p><ul style="text-align: left; margin-top: 10px;"><li>${errorDetails.replace(
|
|
/\n• /g,
|
|
"</li><li>"
|
|
)}</li></ul>`
|
|
: errorMessage,
|
|
icon: "error",
|
|
confirmButtonText: "OK",
|
|
});
|
|
}
|
|
};
|
|
|
|
// Load template data if editing
|
|
onMounted(async () => {
|
|
if (isEditing.value && templateId.value) {
|
|
isLoading.value = true;
|
|
try {
|
|
console.log("Loading template for editing:", templateId.value);
|
|
|
|
const response = await $fetch(`/api/notifications/templates/${templateId.value}`);
|
|
|
|
if (response.success) {
|
|
const template = response.data.template;
|
|
console.log("Template loaded:", template);
|
|
|
|
// Map API response to form data
|
|
Object.assign(templateForm.value, {
|
|
title: template.title,
|
|
description: template.description,
|
|
subject: template.subject,
|
|
preheader: template.preheader,
|
|
category: template.category,
|
|
channels: template.channels || [],
|
|
status: template.status,
|
|
version: template.version,
|
|
content: template.content,
|
|
tags: template.tags,
|
|
isPersonal: template.isPersonal,
|
|
fromName: template.fromName,
|
|
replyTo: template.replyTo,
|
|
trackOpens: template.trackOpens,
|
|
pushTitle: template.pushTitle,
|
|
pushIcon: template.pushIcon,
|
|
pushUrl: template.pushUrl,
|
|
smsContent: template.smsContent,
|
|
});
|
|
|
|
// Update editor content
|
|
if (editor.value && template.content) {
|
|
editor.value.commands.setContent(template.content);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Error loading template:", error);
|
|
|
|
let errorMessage = "Failed to load template data";
|
|
if (error.data?.error) {
|
|
errorMessage = error.data.error;
|
|
}
|
|
|
|
$swal.fire("Error", errorMessage, "error").then(() => {
|
|
// Redirect back to templates list on error
|
|
router.push("/notification/templates");
|
|
});
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Watch for content changes
|
|
watch(
|
|
() => templateForm.value.content,
|
|
(newContent) => {
|
|
if (editor.value && editor.value.getHTML() !== newContent) {
|
|
editor.value.commands.setContent(newContent, false);
|
|
}
|
|
}
|
|
);
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.editor-wrapper {
|
|
.menu-button {
|
|
@apply flex items-center justify-center w-8 h-8 p-1.5 rounded text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-all duration-200;
|
|
|
|
&:hover {
|
|
@apply bg-gray-200 dark:bg-gray-600 shadow-sm;
|
|
}
|
|
|
|
&.is-active {
|
|
@apply bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary shadow-sm;
|
|
}
|
|
}
|
|
|
|
.menubar {
|
|
@apply sticky top-0 z-10 backdrop-blur-sm;
|
|
}
|
|
}
|
|
|
|
.preview-container {
|
|
@apply max-h-96 overflow-y-auto;
|
|
}
|
|
|
|
.email-preview {
|
|
@apply max-w-2xl mx-auto;
|
|
}
|
|
|
|
:deep(.ProseMirror) {
|
|
&:focus {
|
|
outline: none;
|
|
}
|
|
|
|
p.is-editor-empty:first-child::before {
|
|
color: #adb5bd;
|
|
content: attr(data-placeholder);
|
|
float: left;
|
|
height: 0;
|
|
pointer-events: none;
|
|
font-style: italic;
|
|
}
|
|
}
|
|
|
|
// Responsive adjustments
|
|
@media (max-width: 768px) {
|
|
.editor-wrapper .menubar {
|
|
@apply flex-wrap;
|
|
}
|
|
|
|
.preview-container {
|
|
@apply max-h-80;
|
|
}
|
|
}
|
|
</style>
|