494 lines
17 KiB
Vue
494 lines
17 KiB
Vue
<template>
|
|
<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>
|
|
definePageMeta({
|
|
title: "Notification Templates",
|
|
middleware: ["auth"],
|
|
requiresAuth: true,
|
|
});
|
|
|
|
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>
|