Add notification template management features, including creation, editing, and version control. Implement filtering options by category, language, and channel. Introduce a preview functionality and enhance the user interface for better usability.

This commit is contained in:
akmakid 2025-05-30 18:48:03 +08:00
parent b181f99237
commit 76841b5b81
6 changed files with 32629 additions and 1415 deletions

133
docx/overview.docx Normal file
View File

@ -0,0 +1,133 @@
Notification System User Requirements
1. Notification Templates
Functional Requirements
Template Creation: Users must be able to create reusable notification templates with customizable content, subject lines, and formatting
Multi-Channel Support: Templates must support different notification channels (email, SMS, push notifications, in-app notifications)
Dynamic Content: Templates must support placeholder variables and merge fields for personalized content (user name, dates, amounts, etc.)
Rich Content: Email templates must support HTML formatting, images, links, and attachments
Template Versioning: System must maintain version history of templates with ability to revert to previous versions
Template Categories: Users must be able to organize templates by categories or tags for easy management
Preview Functionality: Users must be able to preview templates before saving or sending
Localization Support: Templates must support multiple languages and localization
Non-Functional Requirements
Templates must load within 2 seconds
System must support at least 1000 active templates
Template editor must be responsive and work on mobile devices
Must support templates up to 100KB in size
2. Notification Triggers / Rules
Functional Requirements
Event-Based Triggers: System must trigger notifications based on specific events (user registration, purchase completion, password reset, etc.)
Condition-Based Rules: Users must be able to set complex conditional logic (if/then/else scenarios)
Time-Based Triggers: Support for scheduled notifications, recurring notifications, and time-delayed triggers
User Segmentation: Ability to target specific user groups based on demographics, behavior, or custom attributes
Trigger Prioritization: System must support priority levels for different trigger types
Rule Testing: Users must be able to test rules before activation
Rule Dependencies: Support for chained triggers and dependent rule execution
External API Triggers: Ability to trigger notifications via external API calls or webhooks
Non-Functional Requirements
Rules must be evaluated within 100 milliseconds
System must support at least 500 active rules simultaneously
Rules engine must be fault-tolerant and handle errors gracefully
Must support real-time trigger processing
3. Notification Queue & Scheduler
Functional Requirements
Queue Management: System must queue notifications and process them in order with different priority levels
Batch Processing: Ability to group and send notifications in batches to optimize delivery
Scheduling: Users must be able to schedule notifications for future delivery at specific dates and times
Retry Logic: Failed notifications must be automatically retried with configurable retry intervals
Rate Limiting: System must respect rate limits for different channels and recipients
Queue Monitoring: Real-time visibility into queue status, pending notifications, and processing rates
Dead Letter Queue: Failed notifications after maximum retries must be moved to a separate queue for manual review
Timezone Handling: Proper handling of different timezones for scheduled notifications
Non-Functional Requirements
Queue must process at least 10,000 notifications per minute
System must maintain 99.9% uptime for queue processing
Queue data must be persistent and survive system restarts
Must support horizontal scaling of queue workers
4. Notification Delivery Engine
Functional Requirements
Multi-Channel Delivery: Support for email, SMS, push notifications, and in-app notifications
Delivery Status Tracking: Real-time tracking of delivery status (sent, delivered, failed, bounced)
Fallback Mechanisms: Automatic fallback to alternative channels if primary delivery fails
Delivery Optimization: Intelligent routing and timing optimization for better delivery rates
Bounce Handling: Automatic handling of bounced emails and invalid phone numbers
Delivery Reporting: Comprehensive delivery reports with success rates and failure reasons
Third-Party Integration: Integration with popular email services (SendGrid, AWS SES) and SMS providers
Webhook Support: Delivery status callbacks via webhooks for external systems
Non-Functional Requirements
Delivery engine must process 99.5% of notifications successfully
Average delivery time must be under 30 seconds for high-priority notifications
System must handle peak loads of 50,000 notifications per hour
Must maintain delivery logs for at least 90 days
5. User Preferences & Subscription Settings
Functional Requirements
Preference Management: Users must be able to manage their notification preferences across all channels
Granular Controls: Fine-grained control over notification types, frequency, and timing
Subscription Categories: Ability to subscribe/unsubscribe from different notification categories
Quiet Hours: Users must be able to set "do not disturb" time periods
Channel Preferences: Option to choose preferred notification channels for different types of messages
Frequency Limits: Users can set maximum frequency limits for non-critical notifications
One-Click Unsubscribe: Easy unsubscribe mechanism with single-click options
Preference Import/Export: Ability to backup and restore notification preferences
Non-Functional Requirements
Preference changes must take effect within 5 minutes
Preference interface must load within 3 seconds
System must respect user preferences 100% of the time
Must support preferences for at least 1 million users
6. Notification Logs & Audit Trail
Functional Requirements
Comprehensive Logging: Complete audit trail of all notification activities including creation, modification, and delivery
Search and Filter: Advanced search capabilities with filters by date, user, channel, status, and content
Delivery Analytics: Detailed analytics on delivery rates, open rates, click-through rates, and engagement metrics
Error Logging: Detailed error logs with root cause analysis and troubleshooting information
Export Functionality: Ability to export logs and reports in various formats (CSV, PDF, Excel)
Retention Policies: Configurable data retention policies for different types of logs
Compliance Reporting: Generate compliance reports for regulatory requirements
Real-Time Monitoring: Live dashboard showing current notification activity and system health
Non-Functional Requirements
Log queries must return results within 5 seconds
System must retain logs for minimum 1 year
Logs must be searchable and indexable
Must support concurrent access by multiple administrators
7. Admin Panel / Management Interface
Functional Requirements
Dashboard Overview: Comprehensive dashboard showing key metrics, system health, and recent activity
User Management: Admin tools to manage user accounts, permissions, and notification preferences
System Configuration: Interface to configure system settings, rate limits, and integration parameters
Template Management: Centralized management of all notification templates with approval workflows
Rule Management: Interface to create, modify, and monitor notification rules and triggers
Reporting Tools: Built-in reporting tools with customizable charts and metrics
System Monitoring: Real-time monitoring of system performance, queue status, and delivery rates
Role-Based Access: Different access levels for administrators, managers, and operators
Bulk Operations: Ability to perform bulk operations on users, templates, and notifications
API Management: Interface to manage API keys, webhooks, and external integrations
Non-Functional Requirements
Admin interface must load within 3 seconds
Must support concurrent access by up to 50 administrators
Interface must be responsive and mobile-friendly
Must provide role-based security with audit logging
Dashboard must refresh data in real-time or near real-time
General System Requirements
Security Requirements
All user data must be encrypted at rest and in transit
API endpoints must be secured with authentication and authorization
System must support SSO integration (SAML, OAuth)
Regular security audits and vulnerability assessments
GDPR and privacy regulation compliance
Performance Requirements
System must support 1 million active users
99.9% system uptime requirement
Database queries must execute within 2 seconds
API response times must be under 500ms for 95% of requests
Integration Requirements
RESTful API for external system integration
Webhook support for real-time event notifications
Support for popular third-party services (CRM, marketing automation tools)
Standard data import/export capabilities
Scalability Requirements
System must be horizontally scalable
Support for cloud deployment (AWS, Azure, GCP)
Auto-scaling capabilities based on load
Database clustering and load balancing support

29853
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -46,6 +46,10 @@
"@prisma/client": "^5.1.1",
"@shimyshack/uid": "^0.1.7",
"@sweetalert2/theme-dark": "^5.0.14",
"@tiptap/extension-underline": "^2.12.0",
"@tiptap/pm": "^2.12.0",
"@tiptap/starter-kit": "^2.12.0",
"@tiptap/vue-3": "^2.12.0",
"@vueup/vue-quill": "^1.0.0",
"@vueuse/core": "^9.5.0",
"@vueuse/nuxt": "^9.5.0",

View File

@ -0,0 +1,380 @@
<template>
<rs-card>
<template #header>
<h2 class="text-xl font-semibold">{{ isEditing ? 'Edit' : 'Create' }} Notification Template</h2>
</template>
<template #body>
<div class="pt-2">
<rs-card class="shadow-sm">
<div class="p-5 sm:p-6">
<FormKit type="form" @submit="submitTemplate" :actions="false" class="w-full max-w-5xl mx-auto">
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-1">
<!-- Left Column -->
<div class="space-y-4">
<FormKit
type="text"
name="title"
label="Template Title"
placeholder="Enter template title"
validation="required"
v-model="templateForm.title"
input-class="formkit-input 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"
/>
<FormKit
type="text"
name="subject"
label="Subject Line / Notification Title"
placeholder="e.g. Welcome {{first_name}}"
validation="required"
v-model="templateForm.subject"
input-class="formkit-input 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"
/>
<FormKit
type="select"
name="category"
label="Category"
placeholder="Select category"
:options="categoryOptions"
validation="required"
v-model="templateForm.category"
input-class="formkit-input 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"
/>
</div>
<!-- Right Column -->
<div class="space-y-4">
<FormKit
type="select"
name="language"
label="Language"
placeholder="Select language"
:options="languageOptions"
validation="required"
v-model="templateForm.language"
input-class="formkit-input 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"
/>
<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-1.5 pt-1"
label-class="mb-0 text-sm font-medium"
wrapper-class="mt-1"
/>
<FormKit
type="select"
name="status"
label="Status"
placeholder="Select status"
:options="statusOptions"
validation="required"
v-model="templateForm.status"
input-class="formkit-input 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"
/>
</div>
</div>
<!-- Template Content Section -->
<div class="mt-5">
<FormKit
type="textarea"
name="content"
label="Template Content"
validation="required"
:validation-messages="{ required: 'Content is required' }"
v-model="templateForm.content"
>
<template #input="{ value, onChange }">
<div class="editor-wrapper border dark:border-gray-600 rounded-lg overflow-hidden mt-1">
<div class="menubar">
<template v-for="(item, index) in menuItems" :key="index">
<span v-if="item.type === 'separator'" class="separator dark:bg-gray-600"></span>
<button
v-else
type="button"
class="menu-button dark:text-gray-300 dark:hover:bg-gray-700 dark:[&.is-active]:bg-gray-600"
:class="{ 'is-active': editor?.isActive(item.name) }"
@click="item.action"
:title="item.label || (item.name ? item.name.charAt(0).toUpperCase() + item.name.slice(1) : '')"
>
<Icon :name="item.icon" />
<span v-if="item.label">{{ item.label }}</span>
</button>
</template>
</div>
<editor-content
:editor="editor"
class="editor-content bg-white dark:bg-gray-800 dark:text-gray-200"
@update:model-value="(val) => { templateForm.content = val; onChange(val); }"
/>
</div>
</template>
</FormKit>
</div>
<!-- Action Buttons -->
<div class="flex justify-end items-center mt-6 gap-4">
<rs-button type="button" variant="outline" @click="previewTemplate" title="Preview Template">
<Icon name="material-symbols:preview-outline" class="mr-1" />
Preview
</rs-button>
<rs-button type="submit" :title="isEditing ? 'Save Changes' : 'Create Template'">
<Icon name="material-symbols:save-rounded" class="mr-1" />
{{ isEditing ? 'Save Changes' : 'Create Template' }}
</rs-button>
</div>
</FormKit>
</div>
</rs-card>
</div>
<!-- Preview Modal -->
<rs-modal v-model="showPreviewModal" title="Template Preview" size="lg">
<div class="space-y-4">
<p><span class="font-semibold">Title:</span> {{ templateForm.title }}</p>
<p><span class="font-semibold">Subject:</span> {{ templateForm.subject }}</p>
<div class="border rounded-lg p-4 bg-gray-50">
<h3 class="font-semibold mb-2">Content Preview:</h3>
<div v-html="templateForm.content"></div>
</div>
</div>
<template #footer>
<rs-button @click="showPreviewModal = false">Close</rs-button>
</template>
</rs-modal>
</template>
</rs-card>
</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';
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 templateForm = ref({
title: '',
subject: '',
category: '',
language: 'en',
channels: [],
status: 'Draft',
content: '',
});
// Options for select/checkbox fields
const categoryOptions = [
{ label: 'User Management', value: 'user_management' },
{ label: 'Orders', value: 'orders' },
{ label: 'Security', value: 'security' },
{ label: 'Marketing', value: 'marketing' },
{ label: 'General', value: 'general' },
];
const languageOptions = [
{ label: 'English', value: 'en' },
{ label: 'Spanish', value: 'es' },
{ label: 'French', value: 'fr' },
{ label: 'German', value: 'de' },
];
const channelOptions = [
{ label: 'Email', value: 'email' },
{ label: 'SMS', value: 'sms' },
{ label: 'Push', value: 'push' },
{ label: 'In-App', value: 'in_app' },
];
const statusOptions = [
{ label: 'Draft', value: 'Draft' },
{ label: 'Active', value: 'Active' },
{ label: 'Archived', value: 'Archived' },
];
// 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 min-h-[200px] px-3 py-2',
},
},
onUpdate: ({ editor: currentEditor }) => {
templateForm.value.content = currentEditor.getHTML();
},
});
watch(() => templateForm.value.content, (newContent) => {
if (editor.value && editor.value.getHTML() !== newContent) {
editor.value.commands.setContent(newContent, false);
}
});
const menuItems = computed(() => [
{ name: 'bold', icon: 'material-symbols:format-bold', action: () => editor.value?.chain().focus().toggleBold().run() },
{ name: 'italic', icon: 'material-symbols:format-italic', action: () => editor.value?.chain().focus().toggleItalic().run() },
{ name: 'underline', icon: 'material-symbols:format-underlined', action: () => editor.value?.chain().focus().toggleUnderline().run() },
{ type: 'separator' },
{ name: 'bulletList', icon: 'material-symbols:list', action: () => editor.value?.chain().focus().toggleBulletList().run() },
{ name: 'orderedList', icon: 'material-symbols:format-list-numbered', action: () => editor.value?.chain().focus().toggleOrderedList().run() },
{ type: 'separator' },
{ name: 'variable', icon: 'material-symbols:code', label: 'Insert Variable', action: insertVariable },
]);
function insertVariable() {
const variables = ['user_name', 'first_name', 'last_name', 'order_id', 'email', 'company_name', 'reset_link'];
const variable = prompt('Select or type a variable:', variables.join(', '));
if (variable && editor.value) {
editor.value.chain().focus().insertContent(`{{${variable.trim()}}}`).run();
}
}
// Load template data if editing
onMounted(() => {
if (isEditing.value && templateId.value) {
// Mock fetching template data - replace with actual API call
const MOCK_TEMPLATES_DB = [
{
id: 1,
title: 'Welcome Email',
subject: 'Welcome to Our Platform, {{first_name}}!',
category: 'user_management',
description: 'Template for new user welcome email',
content: '<p>Hello {{first_name}}, welcome to our platform!</p><p>We are excited to have you.</p>',
channels: ['email', 'in_app'],
language: 'en',
version: '1.2',
status: 'Active',
createdAt: '2024-03-20',
updatedAt: '2024-03-20',
},
// Add other mock templates from index.vue if needed for testing edit mode
];
const existingTemplate = MOCK_TEMPLATES_DB.find(t => t.id === templateId.value);
if (existingTemplate) {
templateForm.value = { ...templateForm.value, ...existingTemplate };
if(editor.value) editor.value.commands.setContent(existingTemplate.content, false);
}
}
// Set placeholder for TipTap if content is empty
if (editor.value && !editor.value.getText()) {
const placeholderExtension = editor.value.extensionManager.extensions.find(ext => ext.name === 'placeholder');
if (placeholderExtension && placeholderExtension.options) {
placeholderExtension.options.placeholder = 'Enter your template content here...';
}
}
});
// Preview Modal
const showPreviewModal = ref(false);
const previewTemplate = () => {
showPreviewModal.value = true;
};
// Form Submission
const submitTemplate = async () => {
// Actual submission logic here
console.log('Submitting Template:', templateForm.value);
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: 2000,
showConfirmButton: false,
});
router.push('/notification/templates');
};
</script>
<style scoped lang="scss">
// Minimal scoped styles, relying on utility classes and global FormKit/component styles.
.editor-wrapper {
/* background-color set by utility classes */
width: 100%;
max-width: 100%;
}
.menubar {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
padding: 0.5rem;
/* background-color by utility: bg-gray-50 dark:bg-gray-700 */
/* border-bottom by utility: border-b dark:border-gray-600 */
/* margin: -1px -1px 0.5rem -1px; */ /* Removed, handled by overflow-hidden and padding on parent */
/* width: calc(100% + 2px); */ /* Removed */
}
.menu-button {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
display: inline-flex;
align-items: center;
gap: 0.25rem;
background: transparent;
border: none;
cursor: pointer;
font-size: 0.875rem;
/* &:hover by utility: dark:hover:bg-gray-700 */
/* &.is-active by utility: dark:[&.is-active]:bg-gray-600 */
}
.separator {
width: 1px;
height: 24px;
/* background-color by utility: dark:bg-gray-600 */
margin: 0 0.5rem;
display: inline-block;
vertical-align: middle;
}
.editor-content {
/* padding handled by editorProps attributes */
/* min-height set in editorProps */
&:focus {
outline: none;
}
}
/* :deep(.ProseMirror) styles are mostly handled by editorProps attributes now */
:deep(.ProseMirror p.is-editor-empty:first-child::before) {
color: #adb5bd; /* Tailwind gray-400 */
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
font-style: italic;
}
/* Ensure FormKit elements don't have excessive default margins if not desired */
:deep(.formkit-outer) {
margin-bottom: 0.75rem; /* Default FormKit-like spacing, adjust as needed */
}
:deep(.formkit-outer:last-child) {
margin-bottom: 0;
}
/* Ensure checkbox grid label is aligned well */
:deep(.formkit-label[for*="channels"]) {
margin-bottom: 0.25rem !important;
}
</style>

View File

@ -1,7 +1,493 @@
<template>
<div></div>
<div>
<LayoutsBreadcrumb />
<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>
Info
</div>
</template>
<template #body>
<p class="mb-4">
Manage your notification templates here. You can create, edit, preview, and manage versions of templates for various channels like Email, SMS, Push, and In-App messages.
</p>
</template>
</rs-card>
<rs-card>
<template #header>
<h2 class="text-xl font-semibold">Notification Templates</h2>
</template>
<template #body>
<div class="pt-2">
<rs-tab fill>
<rs-tab-item title="All Templates">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center gap-4 flex-wrap">
<FormKit
type="select"
name="category"
placeholder="Filter by Category"
:options="categories"
@input="filterByCategory"
input-class="formkit-input 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"
outer-class="flex-grow sm:flex-grow-0 min-w-[180px] mb-0"
/>
<FormKit
type="select"
name="language"
placeholder="Filter by Language"
:options="languages"
@input="filterByLanguage"
input-class="formkit-input 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"
outer-class="flex-grow sm:flex-grow-0 min-w-[180px] mb-0"
/>
<FormKit
type="select"
name="channel"
placeholder="Filter by Channel"
:options="channels"
@input="filterByChannel"
input-class="formkit-input 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"
outer-class="flex-grow sm:flex-grow-0 min-w-[180px] mb-0"
/>
</div>
<rs-button @click="$router.push('/notification/templates/create_template')" class="ml-auto" >
<Icon name="material-symbols:add" class="mr-1"></Icon>
Create Template
</rs-button>
</div>
<rs-table
v-if="templateList && templateList.length > 0"
:data="templateList"
:columns="columns"
:options="{
variant: 'default',
striped: true,
borderless: true,
class: 'align-middle'
}"
advanced
>
<template v-slot:channel="{ value }">
<div class="flex items-center justify-start gap-1 flex-wrap" style="max-width: 100px;">
<template v-if="value.channel && value.channel.length">
<template v-for="channel_item in value.channel" :key="channel_item">
<span :title="channel_item">
<Icon
:name="getChannelIcon(channel_item)"
class="text-gray-700 dark:text-gray-300"
size="18"
/>
</span>
</template>
</template>
<span v-else>-</span>
</div>
</template>
<template v-slot:version="{ text }">
<span class="text-sm text-gray-600 dark:text-gray-400">v{{ text }}</span>
</template>
<template v-slot:status="{ text }">
<span
class="px-2 py-1 rounded-full text-xs font-medium"
:class="getStatusClass(text)"
>
{{ text }}
</span>
</template>
<template v-slot:action="data">
<div class="flex justify-center items-center gap-2">
<span title="Edit">
<Icon
name="material-symbols:edit-outline-rounded"
class="text-primary hover:text-primary/90 cursor-pointer"
size="22"
@click="editTemplate(data.value)"
/>
</span>
<span title="Duplicate">
<Icon
name="material-symbols:content-copy"
class="text-primary hover:text-primary/90 cursor-pointer"
size="22"
@click="duplicateTemplate(data.value)"
/>
</span>
<span title="Version History">
<Icon
name="material-symbols:history"
class="text-primary hover:text-primary/90 cursor-pointer"
size="22"
@click="showVersionHistory(data.value)"
/>
</span>
<span title="Preview">
<Icon
name="material-symbols:preview-outline"
class="text-primary hover:text-primary/90 cursor-pointer"
size="22"
@click="previewTemplate(data.value)"
/>
</span>
<span title="Delete">
<Icon
name="material-symbols:close-rounded"
class="text-red-500 hover:text-red-600 cursor-pointer"
size="22"
@click="openModalDelete(data.value)"
/>
</span>
</div>
</template>
</rs-table>
</rs-tab-item>
</rs-tab>
</div>
</template>
</rs-card>
<!-- Preview Modal -->
<rs-modal v-model="showPreview" title="Template Preview" size="lg">
<div class="space-y-4 p-1">
<div class="flex gap-4 mb-4">
<FormKit
type="select"
name="previewChannel"
label="Preview Channel"
:options="channels"
v-model="previewChannel"
/>
<FormKit
type="select"
name="previewLanguage"
label="Preview Language"
:options="languages"
v-model="previewLanguage"
/>
</div>
<div class="border rounded-lg p-4 bg-gray-50 dark:bg-gray-800">
<h3 class="font-semibold mb-2 text-gray-800 dark:text-gray-200">{{ selectedTemplate?.notificationTitle }}</h3>
<div class="prose prose-sm max-w-none dark:prose-invert" v-html="selectedTemplate?.content"></div>
</div>
</div>
<template #footer>
<rs-button @click="showPreview = false">Close</rs-button>
</template>
</rs-modal>
<!-- Version History Modal -->
<rs-modal v-model="showVersions" title="Version History" size="lg">
<div class="space-y-4 p-1">
<div v-if="versionHistory.length">
<div v-for="version in versionHistory" :key="version.id" class="border rounded-lg p-4 mb-3 last:mb-0 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-150 ease-in-out">
<div class="flex justify-between items-center mb-2">
<span class="font-semibold text-gray-700 dark:text-gray-300">Version {{ version.version }}</span>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500 dark:text-gray-400">{{ version.updatedAt }}</span>
<rs-button size="sm" @click="restoreVersion(version)" variant="outline">Restore</rs-button>
<rs-button size="sm" @click="deleteVersion(version)" variant="danger-outline">Delete</rs-button>
</div>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">{{ version.changeDescription }}</p>
</div>
</div>
<div v-else class="text-center text-gray-500 dark:text-gray-400 py-4">
No version history available for this template.
</div>
</div>
<template #footer>
<rs-button @click="showVersions = false">Close</rs-button>
</template>
</rs-modal>
</div>
</template>
<script setup></script>
<script setup>
definePageMeta({
title: "Notification Templates",
middleware: ["auth"],
requiresAuth: true,
});
<style lang="scss" scoped></style>
const { $swal } = useNuxtApp();
const router = useRouter();
const categories = ref([
{ label: 'All Categories', value: '' },
{ label: 'User Management', value: 'user' },
{ label: 'Orders', value: 'order' },
{ label: 'Security', value: 'security' },
{ label: 'Marketing', value: 'marketing' },
]);
const languages = ref([
{ label: 'All Languages', value: '' },
{ label: 'English', value: 'en' },
{ label: 'Spanish', value: 'es' },
{ label: 'French', value: 'fr' },
{ label: 'German', value: 'de' },
]);
const channels = ref([
{ label: 'All Channels', value: '' },
{ label: 'Email', value: 'email' },
{ label: 'SMS', value: 'sms' },
{ label: 'Push', value: 'push' },
{ label: 'In-App', value: 'in-app' },
]);
const columns = [
{
label: 'Title',
key: 'title',
sortable: true,
},
{
label: 'Category',
key: 'category',
sortable: true,
},
{
label: 'Channels',
sortable: false,
},
{
label: 'Language',
key: 'language',
sortable: true,
},
{
label: 'Version',
key: 'version',
sortable: true,
},
{
label: 'Status',
key: 'status',
sortable: true,
},
{
label: 'Last Updated',
key: 'updatedAt',
sortable: true,
},
{
label: 'Action',
key: 'action',
align: 'center',
},
];
const templateList = ref([
{
id: 1,
title: 'Welcome Email',
category: 'User Management',
description: 'Template for new user welcome email',
notificationTitle: 'Welcome {{first_name}}',
content: 'Hello {{first_name}}, welcome to our platform!',
channel: ['email', 'in-app'],
language: 'en',
version: '1.2',
status: 'Active',
createdAt: '2024-03-20',
updatedAt: '2024-03-20',
action: null,
},
{
id: 2,
title: 'Order Confirmation',
category: 'Orders',
description: 'Template for order confirmation',
notificationTitle: 'Order #{{order_id}} Confirmed',
content: 'Your order #{{order_id}} has been confirmed.',
channel: ['email', 'sms', 'push'],
language: 'en',
version: '2.0',
status: 'Active',
createdAt: '2024-03-19',
updatedAt: '2024-03-19',
action: null,
},
{
id: 3,
title: 'Password Reset',
category: 'Security',
description: 'Template for password reset requests',
notificationTitle: 'Password Reset Request',
content: 'Click the link to reset your password: {{reset_link}}',
channel: ['email'],
language: 'en',
version: '1.0',
status: 'Draft',
createdAt: '2024-03-18',
updatedAt: '2024-03-18',
action: null,
},
]);
const showPreview = ref(false);
const selectedTemplate = ref(null);
const previewChannel = ref('email');
const previewLanguage = ref('en');
const showVersions = ref(false);
const versionHistory = ref([]);
const getChannelIcon = (channel_item) => {
const icons = {
email: 'material-symbols:mail-outline-rounded',
sms: 'material-symbols:sms-outline-rounded',
push: 'material-symbols:notifications-active-outline-rounded',
'in-app': 'material-symbols:chat-bubble-outline-rounded',
};
return icons[channel_item] || 'material-symbols:help-outline-rounded';
};
const getStatusClass = (status) => {
const classes = {
Active: 'bg-green-100 text-green-800 dark:bg-green-700 dark:text-green-100',
Draft: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200',
Archived: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-600 dark:text-yellow-100',
};
return classes[status];
};
const filterByCategory = (event) => {
console.log('Filter by category:', event.target.value);
};
const filterByLanguage = (event) => {
console.log('Filter by language:', event.target.value);
};
const filterByChannel = (event) => {
console.log('Filter by channel:', event.target.value);
};
const editTemplate = (template) => {
router.push(`/notification/templates/create_template?id=${template.id}`);
};
const duplicateTemplate = (template) => {
$swal.fire({
title: 'Duplicate Template?',
text: `Are you sure you want to duplicate "${template.title}"?`,
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Duplicate',
cancelButtonText: 'Cancel',
}).then((result) => {
if (result.isConfirmed) {
console.log('Duplicate template:', template.title);
$swal.fire('Duplicated!', 'Template has been duplicated. (Mock)', 'success');
}
});
};
const previewTemplate = (template) => {
selectedTemplate.value = template;
showPreview.value = true;
};
const showVersionHistory = async (template) => {
versionHistory.value = [
{
id: 1,
version: '1.2',
changeDescription: 'Updated welcome message and added new variables for personalization.',
updatedAt: '2024-03-20 14:30',
},
{
id: 2,
version: '1.1',
changeDescription: 'Fixed formatting issues and typos in the content body.',
updatedAt: '2024-03-19 11:20',
},
{
id: 3,
version: '1.0',
changeDescription: 'Initial version of the template created for welcome emails.',
updatedAt: '2024-03-18 09:15',
},
];
selectedTemplate.value = template;
showVersions.value = true;
};
const restoreVersion = async (version) => {
$swal.fire({
title: 'Restore Version?',
text: `Are you sure you want to restore version ${version.version} for "${selectedTemplate.value?.title}"? Current content will be overwritten.`,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Restore',
cancelButtonText: 'Cancel',
dangerMode: true,
}).then((result) => {
if (result.isConfirmed) {
console.log('Restore version:', version);
$swal.fire('Restored!', `Version ${version.version} has been restored. (Mock)`, 'success');
showVersions.value = false;
}
});
};
const deleteVersion = async (version) => {
const result = await $swal.fire({
title: 'Delete Version?',
text: `Are you sure you want to delete version ${version.version} for "${selectedTemplate.value?.title}"? This action cannot be undone.`,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Yes, delete it!',
cancelButtonText: 'Cancel',
confirmButtonColor: '#d33', // Standard red for delete
cancelButtonColor: '#3085d6',
});
if (result.isConfirmed) {
// Mock deletion: In a real app, you'd call an API here
versionHistory.value = versionHistory.value.filter(v => v.id !== version.id);
$swal.fire(
'Deleted!',
`Version ${version.version} has been deleted. (Mock)`,
'success'
);
// If no versions left, maybe close modal or show a message?
// if (versionHistory.value.length === 0) {
// showVersions.value = false;
// }
}
};
const openModalDelete = async (templateToDelete) => {
const result = await $swal.fire({
title: 'Delete Template',
text: `Are you sure you want to delete "${templateToDelete.title}"? This action cannot be undone.`,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Yes, delete it!',
cancelButtonText: 'Cancel',
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
});
if (result.isConfirmed) {
deleteTemplate(templateToDelete);
}
};
const deleteTemplate = async (templateToDelete) => {
templateList.value = templateList.value.filter(t => t.id !== templateToDelete.id);
$swal.fire({
position: "center",
icon: "success",
title: "Deleted!",
text: `Template "${templateToDelete.title}" has been deleted.`,
timer: 2000,
showConfirmButton: false,
});
};
</script>

3182
yarn.lock

File diff suppressed because it is too large Load Diff