Compare commits
No commits in common. "08620adaab0a753f999313a86b8935502db74956" and "be224f7508003265f3d913342d2dda6ee1da9431" have entirely different histories.
08620adaab
...
be224f7508
@ -1,133 +0,0 @@
|
|||||||
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
29853
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -46,10 +46,6 @@
|
|||||||
"@prisma/client": "^5.1.1",
|
"@prisma/client": "^5.1.1",
|
||||||
"@shimyshack/uid": "^0.1.7",
|
"@shimyshack/uid": "^0.1.7",
|
||||||
"@sweetalert2/theme-dark": "^5.0.14",
|
"@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",
|
"@vueup/vue-quill": "^1.0.0",
|
||||||
"@vueuse/core": "^9.5.0",
|
"@vueuse/core": "^9.5.0",
|
||||||
"@vueuse/nuxt": "^9.5.0",
|
"@vueuse/nuxt": "^9.5.0",
|
||||||
|
@ -1,380 +0,0 @@
|
|||||||
<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>
|
|
@ -1,493 +1,7 @@
|
|||||||
<template>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup></script>
|
||||||
definePageMeta({
|
|
||||||
title: "Notification Templates",
|
|
||||||
middleware: ["auth"],
|
|
||||||
requiresAuth: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { $swal } = useNuxtApp();
|
<style lang="scss" scoped></style>
|
||||||
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>
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user