corrad-bp/components/process-flow/notification/NotificationTemplates.vue
2025-05-27 10:04:59 +08:00

579 lines
19 KiB
Vue

<template>
<div class="notification-templates">
<div class="mb-4 flex justify-between items-center">
<h3 class="text-lg font-medium text-gray-900">Notification Templates</h3>
<RsButton @click="showNewTemplateModal = true" variant="primary" size="sm">
<Icon name="material-symbols:add" class="mr-1" />
New Template
</RsButton>
</div>
<!-- Templates List -->
<div v-if="templates.length > 0" class="space-y-3">
<div v-for="template in templates" :key="template.id"
class="border rounded-md p-3 hover:bg-gray-50 cursor-pointer"
@click="selectTemplate(template)">
<div class="flex justify-between items-start">
<div>
<h4 class="font-medium text-gray-800">{{ template.name }}</h4>
<p class="text-sm text-gray-500">{{ template.description }}</p>
</div>
<div :class="`text-${getTypeColor(template.type)}-500`">
<Icon :name="getTypeIcon(template.type)" />
</div>
</div>
<div class="flex items-center mt-2 text-xs text-gray-500 space-x-4">
<span class="flex items-center">
<Icon name="material-symbols:label-outline" class="mr-1" />
{{ template.type }}
</span>
<span class="flex items-center">
<Icon name="material-symbols:device-hub" class="mr-1" />
{{ getChannelsText(template.channels) }}
</span>
<span class="flex items-center">
<Icon name="material-symbols:flag" class="mr-1" />
{{ template.priority }}
</span>
</div>
</div>
</div>
<!-- Empty State -->
<div v-else class="text-center py-8 bg-gray-50 rounded-md">
<Icon name="material-symbols:notifications-outline" class="text-4xl text-gray-400 mb-2" />
<h4 class="text-gray-500 font-medium">No Templates</h4>
<p class="text-sm text-gray-400 mb-4">Create notification templates to reuse across your processes</p>
<RsButton @click="showNewTemplateModal = true" variant="primary" size="sm">
Create Template
</RsButton>
</div>
<!-- Template Modal -->
<RsModal
v-model="showTemplateModal"
:title="editingTemplate ? 'Edit Template' : 'New Template'"
size="lg"
position="center"
:okCallback="saveTemplate"
okTitle="Save Template"
:cancelCallback="closeTemplateModal"
>
<div class="p-4 space-y-4">
<!-- Basic Info -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Template Name</label>
<input
v-model="currentTemplate.name"
type="text"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
placeholder="Enter template name"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Notification Type</label>
<select
v-model="currentTemplate.type"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
>
<option value="info">Information</option>
<option value="success">Success</option>
<option value="warning">Warning</option>
<option value="error">Error</option>
</select>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea
v-model="currentTemplate.description"
rows="2"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
placeholder="Describe this template's purpose"
></textarea>
</div>
<!-- Channels -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Delivery Channels</label>
<div class="space-y-2">
<label class="inline-flex items-center mr-4">
<input
type="checkbox"
v-model="currentTemplate.channels.inApp"
class="form-checkbox"
/>
<span class="ml-2">In-app notification</span>
</label>
<label class="inline-flex items-center mr-4">
<input
type="checkbox"
v-model="currentTemplate.channels.email"
class="form-checkbox"
/>
<span class="ml-2">Email notification</span>
</label>
</div>
</div>
<!-- Content -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Subject</label>
<input
v-model="currentTemplate.subject"
type="text"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
placeholder="Enter notification subject"
/>
<p class="mt-1 text-xs text-gray-500">
You can use variable placeholders with {variableName} syntax
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Message Body</label>
<textarea
v-model="currentTemplate.message"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
placeholder="Enter notification message"
rows="4"
></textarea>
<p class="mt-1 text-xs text-gray-500">
You can use variable placeholders with {variableName} syntax
</p>
</div>
<!-- Email HTML Content -->
<div v-if="currentTemplate.channels.email">
<div class="flex justify-between items-center mb-2">
<label class="block text-sm font-medium text-gray-700">Email Format</label>
<div class="flex items-center space-x-2">
<label class="inline-flex items-center">
<input
type="radio"
v-model="currentTemplate.emailFormat"
value="plain"
class="form-radio"
/>
<span class="ml-1 text-sm">Plain Text</span>
</label>
<label class="inline-flex items-center">
<input
type="radio"
v-model="currentTemplate.emailFormat"
value="html"
class="form-radio"
/>
<span class="ml-1 text-sm">HTML</span>
</label>
</div>
</div>
<div v-if="currentTemplate.emailFormat === 'html'" class="border rounded-md">
<div class="bg-gray-50 border-b p-2 flex justify-between items-center">
<span class="text-sm font-medium">HTML Email Content</span>
<div class="flex space-x-1">
<button class="p-1 hover:bg-gray-200 rounded" title="Bold">
<Icon name="material-symbols:format-bold" />
</button>
<button class="p-1 hover:bg-gray-200 rounded" title="Italic">
<Icon name="material-symbols:format-italic" />
</button>
<button class="p-1 hover:bg-gray-200 rounded" title="Link">
<Icon name="material-symbols:link" />
</button>
<button class="p-1 hover:bg-gray-200 rounded" title="List">
<Icon name="material-symbols:format-list-bulleted" />
</button>
</div>
</div>
<textarea
v-model="currentTemplate.htmlEmailContent"
class="w-full p-2 border-0 focus:ring-0 text-sm font-mono"
placeholder="<div>Enter HTML email content here</div>"
rows="10"
></textarea>
<div class="bg-gray-50 border-t p-2">
<p class="text-xs text-gray-500">
You can use variable placeholders with {variableName} syntax in your HTML
</p>
</div>
</div>
<!-- HTML Preview -->
<div v-if="currentTemplate.emailFormat === 'html'" class="mt-2">
<div class="flex justify-between items-center mb-2">
<label class="block text-sm font-medium text-gray-700">Preview</label>
<button
@click="showHtmlPreview = !showHtmlPreview"
class="text-xs text-blue-600 hover:text-blue-800"
>
{{ showHtmlPreview ? 'Hide Preview' : 'Show Preview' }}
</button>
</div>
<div v-if="showHtmlPreview" class="border rounded-md p-3 bg-white">
<div v-html="currentTemplate.htmlEmailContent"></div>
</div>
</div>
</div>
<!-- Advanced Settings -->
<div>
<div class="flex justify-between items-center mb-2">
<label class="block text-sm font-medium text-gray-700">Advanced Settings</label>
<button
@click="showAdvancedSettings = !showAdvancedSettings"
class="text-sm text-blue-600 hover:text-blue-800"
>
{{ showAdvancedSettings ? 'Hide' : 'Show' }}
</button>
</div>
<div v-if="showAdvancedSettings" class="space-y-4 border-t pt-3 mt-2">
<!-- Priority -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Priority</label>
<div class="flex flex-wrap gap-3">
<label
v-for="priority in priorities"
:key="priority.value"
class="inline-flex items-center cursor-pointer p-2 border rounded-md"
:class="currentTemplate.priority === priority.value ? `bg-${priority.color}-50 border-${priority.color}-300` : 'bg-white border-gray-200 hover:bg-gray-50'"
>
<input
type="radio"
v-model="currentTemplate.priority"
:value="priority.value"
class="form-radio hidden"
/>
<span :class="`text-${priority.color}-500 mr-2`">
<Icon :name="priority.icon" class="text-lg" />
</span>
<span>{{ priority.label }}</span>
</label>
</div>
</div>
<!-- Language Support -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Language</label>
<select
v-model="currentTemplate.language"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
>
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="de">German</option>
<option value="zh">Chinese</option>
</select>
</div>
<!-- Expiration Settings -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Expiration</label>
<div class="flex items-center gap-2">
<input
type="checkbox"
v-model="currentTemplate.expiration.enabled"
class="form-checkbox"
/>
<span>Expire after</span>
<input
v-model.number="currentTemplate.expiration.value"
type="number"
min="1"
class="w-20 p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
:disabled="!currentTemplate.expiration.enabled"
/>
<select
v-model="currentTemplate.expiration.unit"
class="p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
:disabled="!currentTemplate.expiration.enabled"
>
<option value="minutes">Minutes</option>
<option value="hours">Hours</option>
<option value="days">Days</option>
</select>
</div>
</div>
</div>
</div>
<!-- Variable Preview -->
<div v-if="usedVariables.length > 0" class="bg-gray-50 p-3 border rounded-md">
<h5 class="font-medium text-sm mb-2">Variable Placeholders Used</h5>
<div class="flex flex-wrap gap-2">
<div
v-for="variable in usedVariables"
:key="variable"
class="px-2 py-1 bg-blue-100 text-blue-700 text-xs rounded-md"
>
{{ variable }}
</div>
</div>
</div>
</div>
</RsModal>
<!-- Delete Confirmation Modal -->
<RsModal
v-model="showDeleteModal"
title="Delete Template"
size="md"
position="center"
:okCallback="confirmDeleteTemplate"
okTitle="Delete"
:cancelCallback="cancelDeleteTemplate"
>
<div class="p-4">
<div class="flex items-start mb-4">
<div class="mr-4 text-red-500 flex-shrink-0 mt-1">
<Icon name="material-symbols:delete-outline" class="text-2xl" />
</div>
<div>
<h3 class="text-lg font-medium text-gray-900">Delete Template</h3>
<p class="text-sm text-gray-500 mt-1">
Are you sure you want to delete this template? This action cannot be undone.
</p>
</div>
</div>
<div class="bg-gray-50 p-3 rounded-md">
<p class="font-medium">{{ templateToDelete?.name }}</p>
<p class="text-sm text-gray-500">{{ templateToDelete?.description }}</p>
</div>
</div>
</RsModal>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { Icon } from '#components';
const props = defineProps({
modelValue: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['update:modelValue', 'select']);
// Template data
const templates = ref(props.modelValue || []);
const showTemplateModal = ref(false);
const showNewTemplateModal = ref(false);
const showDeleteModal = ref(false);
const showAdvancedSettings = ref(false);
const showHtmlPreview = ref(false);
const editingTemplate = ref(false);
const templateToDelete = ref(null);
// Current template being edited
const currentTemplate = ref({
id: '',
name: '',
description: '',
type: 'info',
subject: '',
message: '',
emailFormat: 'plain',
htmlEmailContent: '',
channels: {
inApp: true,
email: false,
sms: false
},
priority: 'medium',
language: 'en',
expiration: {
enabled: false,
value: 24,
unit: 'hours'
}
});
// Priority options
const priorities = [
{
value: 'low',
label: 'Low',
icon: 'material-symbols:arrow-downward',
color: 'gray'
},
{
value: 'medium',
label: 'Medium',
icon: 'material-symbols:drag-handle',
color: 'blue'
},
{
value: 'high',
label: 'High',
icon: 'material-symbols:arrow-upward',
color: 'red'
}
];
// Watch for changes to modelValue prop
watch(() => props.modelValue, (value) => {
templates.value = value || [];
}, { deep: true });
// Methods
const selectTemplate = (template) => {
editingTemplate.value = true;
currentTemplate.value = JSON.parse(JSON.stringify(template));
// Set default emailFormat if it doesn't exist
if (!currentTemplate.value.emailFormat) {
currentTemplate.value.emailFormat = 'plain';
}
// Set default htmlEmailContent if it doesn't exist
if (!currentTemplate.value.htmlEmailContent) {
currentTemplate.value.htmlEmailContent = '';
}
showTemplateModal.value = true;
emit('select', template);
};
const createNewTemplate = () => {
editingTemplate.value = false;
currentTemplate.value = {
id: `template_${Date.now()}`,
name: '',
description: '',
type: 'info',
subject: '',
message: '',
emailFormat: 'plain',
htmlEmailContent: '',
channels: {
inApp: true,
email: false,
sms: false
},
priority: 'medium',
language: 'en',
expiration: {
enabled: false,
value: 24,
unit: 'hours'
}
};
showTemplateModal.value = true;
};
// Watch for new template modal trigger
watch(() => showNewTemplateModal.value, (value) => {
if (value) {
createNewTemplate();
showNewTemplateModal.value = false;
}
});
const saveTemplate = () => {
if (!currentTemplate.value.name) {
// Show error or validation
return;
}
if (editingTemplate.value) {
// Update existing template
const index = templates.value.findIndex(t => t.id === currentTemplate.value.id);
if (index !== -1) {
templates.value[index] = JSON.parse(JSON.stringify(currentTemplate.value));
}
} else {
// Add new template
templates.value.push(JSON.parse(JSON.stringify(currentTemplate.value)));
}
emit('update:modelValue', templates.value);
showTemplateModal.value = false;
};
const closeTemplateModal = () => {
showTemplateModal.value = false;
};
const deleteTemplate = (template) => {
templateToDelete.value = template;
showDeleteModal.value = true;
};
const confirmDeleteTemplate = () => {
if (templateToDelete.value) {
templates.value = templates.value.filter(t => t.id !== templateToDelete.value.id);
emit('update:modelValue', templates.value);
templateToDelete.value = null;
showDeleteModal.value = false;
}
};
const cancelDeleteTemplate = () => {
templateToDelete.value = null;
showDeleteModal.value = false;
};
// Helper methods
const getTypeIcon = (type) => {
const icons = {
info: 'material-symbols:info-outline',
success: 'material-symbols:check-circle-outline',
warning: 'material-symbols:warning-outline',
error: 'material-symbols:error-outline'
};
return icons[type] || icons.info;
};
const getTypeColor = (type) => {
const colors = {
info: 'blue',
success: 'green',
warning: 'yellow',
error: 'red'
};
return colors[type] || colors.info;
};
const getChannelsText = (channels) => {
const enabledChannels = [];
if (channels.inApp) enabledChannels.push('In-app');
if (channels.email) enabledChannels.push('Email');
if (channels.sms) enabledChannels.push('SMS');
return enabledChannels.length > 0
? enabledChannels.join(', ')
: 'None';
};
// Extract variables from content
const extractVariables = (text) => {
const regex = /{([^}]+)}/g;
const matches = [];
let match;
while ((match = regex.exec(text)) !== null) {
matches.push(match[1]);
}
return [...new Set(matches)]; // Return unique values
};
// Computed properties
const usedVariables = computed(() => {
const subjectVars = extractVariables(currentTemplate.value.subject || '');
const messageVars = extractVariables(currentTemplate.value.message || '');
const htmlVars = extractVariables(currentTemplate.value.htmlEmailContent || '');
return [...new Set([...subjectVars, ...messageVars, ...htmlVars])];
});
</script>
<style scoped>
.notification-templates {
@apply bg-white rounded-md;
}
</style>