Files
Nas-Notification/pages/notification/templates/create_template/index.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>