Added Notification Management Component

This commit is contained in:
Aiman Fakhrullah Mantasan 2025-05-27 10:04:59 +08:00
parent 96fafd33d7
commit 33982fb093
12 changed files with 4458 additions and 17 deletions

View File

@ -0,0 +1,795 @@
<template>
<div class="notification-node-configuration">
<!-- Step 1: Basic Configuration -->
<div class="mb-6 bg-gray-50 p-4 rounded-md border border-gray-200">
<div class="flex items-center mb-3">
<div class="w-6 h-6 rounded-full bg-blue-100 flex items-center justify-center mr-2">
<span class="text-xs font-semibold text-blue-600">1</span>
</div>
<h4 class="font-medium">Basic Configuration</h4>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Notification Name -->
<div>
<label for="nodeLabel" class="block text-sm font-medium text-gray-700 mb-1">Notification Name</label>
<input
id="nodeLabel"
v-model="localNodeData.label"
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 a descriptive name"
@blur="saveChanges"
/>
<p class="mt-1 text-xs text-gray-500">
A clear name helps identify this notification in the process flow
</p>
</div>
<!-- Description -->
<div>
<label for="nodeDescription" class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea
id="nodeDescription"
v-model="localNodeData.description"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
placeholder="Describe what this notification does"
rows="2"
@blur="saveChanges"
></textarea>
<p class="mt-1 text-xs text-gray-500">
Optional description to explain this notification's purpose
</p>
</div>
</div>
</div>
<!-- Step 2: Notification Type -->
<div class="mb-6 bg-gray-50 p-4 rounded-md border border-gray-200">
<div class="flex items-center mb-3">
<div class="w-6 h-6 rounded-full bg-blue-100 flex items-center justify-center mr-2">
<span class="text-xs font-semibold text-blue-600">2</span>
</div>
<h4 class="font-medium">Notification Type</h4>
</div>
<div class="grid grid-cols-1 gap-4">
<div class="flex flex-wrap gap-3 justify-center">
<div
v-for="type in notificationTypes"
:key="type.value"
@click="selectNotificationType(type.value)"
class="notification-type-card cursor-pointer p-3 border rounded-md flex items-center"
:class="localNodeData.notificationType === type.value ? 'bg-blue-50 border-blue-300' : 'bg-white border-gray-200 hover:bg-gray-50'"
>
<div :class="`text-${type.color}-500 mr-3`">
<Icon :name="type.icon" class="text-xl" />
</div>
<div>
<h5 class="font-medium text-sm">{{ type.label }}</h5>
<p class="text-xs text-gray-500">{{ type.description }}</p>
</div>
</div>
</div>
</div>
</div>
<!-- Step 3: Recipients -->
<div class="mb-6 bg-gray-50 p-4 rounded-md border border-gray-200">
<div class="flex items-center mb-3">
<div class="w-6 h-6 rounded-full bg-blue-100 flex items-center justify-center mr-2">
<span class="text-xs font-semibold text-blue-600">3</span>
</div>
<h4 class="font-medium">Recipients</h4>
</div>
<div class="grid grid-cols-1 gap-4">
<!-- Recipient Type Selection -->
<div class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-2">Recipient Type</label>
<div class="flex flex-wrap gap-2">
<label class="inline-flex items-center">
<input
type="radio"
v-model="localNodeData.recipientType"
value="user"
class="form-radio"
@change="saveChanges"
/>
<span class="ml-2">User</span>
</label>
<label class="inline-flex items-center">
<input
type="radio"
v-model="localNodeData.recipientType"
value="role"
class="form-radio"
@change="saveChanges"
/>
<span class="ml-2">Role</span>
</label>
<label class="inline-flex items-center">
<input
type="radio"
v-model="localNodeData.recipientType"
value="group"
class="form-radio"
@change="saveChanges"
/>
<span class="ml-2">Group</span>
</label>
<label class="inline-flex items-center">
<input
type="radio"
v-model="localNodeData.recipientType"
value="variable"
class="form-radio"
@change="saveChanges"
/>
<span class="ml-2">Process Variable</span>
</label>
<label class="inline-flex items-center">
<input
type="radio"
v-model="localNodeData.recipientType"
value="email"
class="form-radio"
@change="saveChanges"
/>
<span class="ml-2">Email Address</span>
</label>
</div>
</div>
<!-- Dynamic Recipient Input based on type -->
<div>
<div v-if="localNodeData.recipientType === 'user'">
<label class="block text-sm font-medium text-gray-700 mb-1">Select User</label>
<select
v-model="localNodeData.recipientUser"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
@change="saveChanges"
>
<option value="">Select a user</option>
<option v-for="user in mockUsers" :key="user.id" :value="user.id">
{{ user.name }}
</option>
</select>
</div>
<div v-if="localNodeData.recipientType === 'role'">
<label class="block text-sm font-medium text-gray-700 mb-1">Select Role</label>
<select
v-model="localNodeData.recipientRole"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
@change="saveChanges"
>
<option value="">Select a role</option>
<option v-for="role in mockRoles" :key="role.id" :value="role.id">
{{ role.name }}
</option>
</select>
</div>
<div v-if="localNodeData.recipientType === 'group'">
<label class="block text-sm font-medium text-gray-700 mb-1">Select Group</label>
<select
v-model="localNodeData.recipientGroup"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
@change="saveChanges"
>
<option value="">Select a group</option>
<option v-for="group in mockGroups" :key="group.id" :value="group.id">
{{ group.name }}
</option>
</select>
</div>
<div v-if="localNodeData.recipientType === 'variable'">
<label class="block text-sm font-medium text-gray-700 mb-1">Select Variable</label>
<select
v-model="localNodeData.recipientVariable"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
@change="saveChanges"
>
<option value="">Select a variable</option>
<option v-for="variable in availableVariables" :key="variable.name" :value="variable.name">
{{ variable.label || variable.name }}
</option>
</select>
<p class="mt-1 text-xs text-gray-500">
Variable should contain a user ID, role ID, or email address
</p>
</div>
<div v-if="localNodeData.recipientType === 'email'">
<label class="block text-sm font-medium text-gray-700 mb-1">Email Address</label>
<input
v-model="localNodeData.recipientEmail"
type="email"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
placeholder="Enter email address"
@blur="saveChanges"
/>
</div>
</div>
</div>
</div>
<!-- Step 4: Notification Content -->
<div class="mb-6 bg-gray-50 p-4 rounded-md border border-gray-200">
<div class="flex items-center mb-3">
<div class="w-6 h-6 rounded-full bg-blue-100 flex items-center justify-center mr-2">
<span class="text-xs font-semibold text-blue-600">4</span>
</div>
<h4 class="font-medium">Notification Content</h4>
</div>
<div class="grid grid-cols-1 gap-4">
<!-- Subject -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Subject</label>
<input
v-model="localNodeData.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"
@blur="saveChanges"
/>
<p class="mt-1 text-xs text-gray-500">
You can use variable placeholders with {variableName} syntax
</p>
</div>
<!-- Message Body -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Message</label>
<!-- Message Format Selector -->
<div class="flex items-center mb-2 space-x-4">
<label class="inline-flex items-center">
<input
type="radio"
v-model="localNodeData.messageFormat"
value="text"
class="form-radio"
@change="saveChanges"
/>
<span class="ml-2 text-sm">Plain Text</span>
</label>
<label class="inline-flex items-center">
<input
type="radio"
v-model="localNodeData.messageFormat"
value="richtext"
class="form-radio"
@change="saveChanges"
/>
<span class="ml-2 text-sm">Rich Text</span>
</label>
<label class="inline-flex items-center">
<input
type="radio"
v-model="localNodeData.messageFormat"
value="html"
class="form-radio"
@change="saveChanges"
/>
<span class="ml-2 text-sm">HTML</span>
</label>
</div>
<!-- Plain Text Editor -->
<textarea
v-if="localNodeData.messageFormat === 'text'"
v-model="localNodeData.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"
@blur="saveChanges"
></textarea>
<!-- Rich Text Editor -->
<div v-if="localNodeData.messageFormat === 'richtext'" class="border rounded-md">
<ClientOnly>
<QuillEditor
v-model:content="localNodeData.richTextMessage"
contentType="html"
theme="snow"
toolbar="essential"
@update:content="saveChanges"
placeholder="Enter rich text notification message"
/>
</ClientOnly>
<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 content
</p>
</div>
</div>
<!-- Rich Text Preview -->
<div v-if="localNodeData.messageFormat === 'richtext'" class="mt-2">
<div class="flex justify-between items-center mb-2">
<label class="block text-sm font-medium text-gray-700">Rich Text Preview</label>
<button
@click="showRichTextPreview = !showRichTextPreview"
class="text-xs text-blue-600 hover:text-blue-800"
>
{{ showRichTextPreview ? 'Hide Preview' : 'Show Preview' }}
</button>
</div>
<div v-if="showRichTextPreview" class="border rounded-md p-3 bg-white html-preview">
<div v-html="localNodeData.richTextMessage"></div>
</div>
</div>
<!-- HTML Editor -->
<div v-if="localNodeData.messageFormat === '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 Editor</span>
<div class="flex space-x-2">
<select
v-model="htmlTemplate"
class="text-xs p-1 border rounded"
@change="applyHtmlTemplate"
>
<option value="">Select Template</option>
<option value="basic">Basic Template</option>
<option value="card">Card Template</option>
<option value="email">Email Template</option>
</select>
<button
class="text-xs px-2 py-1 bg-blue-50 text-blue-600 border border-blue-200 rounded hover:bg-blue-100"
@click="showHtmlHelpers = !showHtmlHelpers"
>
{{ showHtmlHelpers ? 'Hide Helpers' : 'Show Helpers' }}
</button>
</div>
</div>
<!-- HTML Helpers -->
<div v-if="showHtmlHelpers" class="bg-blue-50 p-2 border-b border-blue-200">
<div class="text-xs font-medium mb-1 text-blue-700">Insert HTML Elements:</div>
<div class="flex flex-wrap gap-1">
<button
v-for="(snippet, key) in htmlSnippets"
:key="key"
@click="insertHtmlSnippet(snippet.code)"
class="text-xs px-2 py-1 bg-white border border-blue-200 rounded hover:bg-blue-100"
:title="snippet.description"
>
{{ snippet.label }}
</button>
</div>
</div>
<ClientOnly>
<rs-code-mirror
v-model="localNodeData.htmlMessage"
mode="html"
height="250px"
@update:modelValue="saveChanges"
/>
</ClientOnly>
</div>
<p class="mt-1 text-xs text-gray-500">
You can use variable placeholders with {variableName} syntax
</p>
</div>
<!-- HTML Preview -->
<div v-if="localNodeData.messageFormat === 'html'" class="mt-2">
<div class="flex justify-between items-center mb-2">
<label class="block text-sm font-medium text-gray-700">HTML 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 html-preview">
<div v-html="localNodeData.htmlMessage"></div>
</div>
</div>
<!-- Variable Preview -->
<div v-if="hasVariablePlaceholders" class="bg-white 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 flex items-center"
>
<span>{{ variable }}</span>
<span
v-if="!isValidVariable(variable)"
class="ml-1 text-red-500"
title="This variable is not defined in the process"
>
<Icon name="material-symbols:warning" class="text-xs" />
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Delivery Options -->
<div class="mb-6 bg-gray-50 p-4 rounded-md border border-gray-200">
<div class="flex items-center mb-3">
<div class="w-6 h-6 rounded-full bg-blue-100 flex items-center justify-center mr-2">
<span class="text-xs font-semibold text-blue-600">5</span>
</div>
<h4 class="font-medium">Delivery Options</h4>
</div>
<div class="grid grid-cols-1 gap-4">
<div class="space-y-2">
<label class="inline-flex items-center">
<input
type="checkbox"
v-model="localNodeData.deliveryOptions.inApp"
class="form-checkbox"
@change="saveChanges"
/>
<span class="ml-2">In-app notification</span>
</label>
<label class="inline-flex items-center">
<input
type="checkbox"
v-model="localNodeData.deliveryOptions.email"
class="form-checkbox"
@change="saveChanges"
/>
<span class="ml-2">Email notification</span>
</label>
<p v-if="localNodeData.messageFormat === 'html' || localNodeData.messageFormat === 'richtext'" class="mt-1 text-xs text-blue-600 bg-blue-50 p-2 rounded">
<Icon name="material-symbols:info-outline" class="inline-block mr-1" />
{{ localNodeData.messageFormat === 'html' ? 'HTML' : 'Rich Text' }} formatting is only supported for email notifications
</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { useVariableStore } from '~/stores/variableStore';
const props = defineProps({
nodeData: {
type: Object,
required: true
},
availableVariables: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['update']);
// Initialize local data with defaults
const localNodeData = ref({
label: props.nodeData.label || 'Notification',
description: props.nodeData.description || '',
notificationType: props.nodeData.notificationType || 'info',
recipientType: props.nodeData.recipientType || 'user',
recipientUser: props.nodeData.recipientUser || '',
recipientRole: props.nodeData.recipientRole || '',
recipientGroup: props.nodeData.recipientGroup || '',
recipientVariable: props.nodeData.recipientVariable || '',
recipientEmail: props.nodeData.recipientEmail || '',
subject: props.nodeData.subject || '',
message: props.nodeData.message || '',
priority: props.nodeData.priority || 'medium',
deliveryOptions: props.nodeData.deliveryOptions || {
inApp: true,
email: false,
sms: false
},
expiration: props.nodeData.expiration || {
enabled: false,
value: 24,
unit: 'hours'
},
messageFormat: props.nodeData.messageFormat || 'text',
htmlMessage: props.nodeData.htmlMessage || '',
richTextMessage: props.nodeData.richTextMessage || ''
});
// Preview state
const showHtmlPreview = ref(false);
const htmlTemplate = ref('');
const showHtmlHelpers = ref(false);
const showRichTextPreview = ref(false);
// HTML Templates
const templates = {
basic: `<div style="font-family: Arial, sans-serif; padding: 15px;">
<h2 style="color: #3b82f6;">Notification Title</h2>
<p>Hello {userName},</p>
<p>This is a basic notification message.</p>
<p>Thank you,<br>Process Maker</p>
</div>`,
card: `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden;">
<div style="background-color: #3b82f6; color: white; padding: 15px;">
<h2 style="margin: 0;">Important Notification</h2>
</div>
<div style="padding: 20px;">
<p>Hello {userName},</p>
<p>This is an important notification regarding {processName}.</p>
<p>Please review the details below:</p>
<ul>
<li>Item 1: {item1}</li>
<li>Item 2: {item2}</li>
</ul>
<div style="margin-top: 20px; padding: 10px; background-color: #f3f4f6; border-radius: 4px;">
<p style="margin: 0; font-size: 14px;">Reference ID: {referenceId}</p>
</div>
</div>
<div style="background-color: #f3f4f6; padding: 15px; text-align: center; font-size: 14px;">
<p>This is an automated notification from Process Maker.</p>
</div>
</div>`,
email: `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Process Notification</title>
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; margin: 0; padding: 0; background-color: #f9fafb;">
<table width="100%" cellpadding="0" cellspacing="0" style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden;">
<tr>
<td style="background-color: #3b82f6; padding: 20px; text-align: center;">
<h1 style="color: white; margin: 0;">Process Notification</h1>
</td>
</tr>
<tr>
<td style="padding: 20px;">
<p>Hello {userName},</p>
<p>You have a new notification regarding process <strong>{processName}</strong>.</p>
<p>Status: <span style="color: #10b981; font-weight: bold;">{status}</span></p>
<p>Please take appropriate action by clicking the button below:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{actionUrl}" style="background-color: #3b82f6; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: bold;">View Details</a>
</div>
<p>If you have any questions, please contact the system administrator.</p>
<p>Thank you,<br>Process Maker Team</p>
</td>
</tr>
<tr>
<td style="background-color: #f3f4f6; padding: 15px; text-align: center; font-size: 14px; color: #6b7280;">
<p>This is an automated email. Please do not reply to this message.</p>
<p>© 2023 Process Maker. All rights reserved.</p>
</td>
</tr>
</table>
</body>
</html>`
};
// Apply HTML template
const applyHtmlTemplate = () => {
if (htmlTemplate.value && templates[htmlTemplate.value]) {
localNodeData.value.htmlMessage = templates[htmlTemplate.value];
saveChanges();
// Reset the select after applying
htmlTemplate.value = '';
}
};
// Watch for changes to nodeData prop
watch(() => props.nodeData, (value) => {
// Merge incoming props with defaults for any missing values
localNodeData.value = {
label: value.label || localNodeData.value.label,
description: value.description || localNodeData.value.description,
notificationType: value.notificationType || localNodeData.value.notificationType,
recipientType: value.recipientType || localNodeData.value.recipientType,
recipientUser: value.recipientUser || localNodeData.value.recipientUser,
recipientRole: value.recipientRole || localNodeData.value.recipientRole,
recipientGroup: value.recipientGroup || localNodeData.value.recipientGroup,
recipientVariable: value.recipientVariable || localNodeData.value.recipientVariable,
recipientEmail: value.recipientEmail || localNodeData.value.recipientEmail,
subject: value.subject || localNodeData.value.subject,
message: value.message || localNodeData.value.message,
priority: value.priority || localNodeData.value.priority,
deliveryOptions: value.deliveryOptions || localNodeData.value.deliveryOptions,
expiration: value.expiration || localNodeData.value.expiration,
messageFormat: value.messageFormat || localNodeData.value.messageFormat,
htmlMessage: value.htmlMessage || localNodeData.value.htmlMessage,
richTextMessage: value.richTextMessage || localNodeData.value.richTextMessage
};
}, { deep: true });
// Mock data for demonstration
const mockUsers = [
{ id: 'user1', name: 'John Doe' },
{ id: 'user2', name: 'Jane Smith' },
{ id: 'user3', name: 'Mike Johnson' }
];
const mockRoles = [
{ id: 'role1', name: 'Administrator' },
{ id: 'role2', name: 'Manager' },
{ id: 'role3', name: 'Approver' },
{ id: 'role4', name: 'User' }
];
const mockGroups = [
{ id: 'group1', name: 'Marketing Team' },
{ id: 'group2', name: 'Finance Team' },
{ id: 'group3', name: 'Operations Team' }
];
// Notification types
const notificationTypes = [
{
value: 'info',
label: 'Information',
icon: 'material-symbols:info-outline',
color: 'blue',
description: 'General information notifications'
},
{
value: 'success',
label: 'Success',
icon: 'material-symbols:check-circle-outline',
color: 'green',
description: 'Success or completion notifications'
},
{
value: 'warning',
label: 'Warning',
icon: 'material-symbols:warning-outline',
color: 'yellow',
description: 'Warning notifications requiring attention'
},
{
value: 'error',
label: 'Error',
icon: 'material-symbols:error-outline',
color: 'red',
description: 'Error notifications requiring action'
},
{
value: 'reminder',
label: 'Reminder',
icon: 'material-symbols:notifications-active',
color: 'purple',
description: 'Reminders about pending tasks or deadlines'
},
{
value: 'status',
label: 'Status Update',
icon: 'material-symbols:update',
color: 'indigo',
description: 'Updates about process or task status changes'
}
];
// 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'
}
];
// Methods
const saveChanges = () => {
emit('update', localNodeData.value);
};
const selectNotificationType = (type) => {
localNodeData.value.notificationType = type;
saveChanges();
};
// 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
};
// Check if a variable exists in available variables
const isValidVariable = (variableName) => {
return props.availableVariables.some(v => v.name === variableName);
};
// Computed properties
const usedVariables = computed(() => {
const subjectVars = extractVariables(localNodeData.value.subject || '');
const messageVars = extractVariables(localNodeData.value.message || '');
const htmlVars = extractVariables(localNodeData.value.htmlMessage || '');
const richTextVars = extractVariables(localNodeData.value.richTextMessage || '');
return [...new Set([...subjectVars, ...messageVars, ...htmlVars, ...richTextVars])];
});
const hasVariablePlaceholders = computed(() => {
return usedVariables.value.length > 0;
});
// HTML Helpers
const htmlSnippets = [
{ label: 'h1', code: '<h1>Heading 1</h1>', description: 'Adds a level 1 heading' },
{ label: 'h2', code: '<h2>Heading 2</h2>', description: 'Adds a level 2 heading' },
{ label: 'h3', code: '<h3>Heading 3</h3>', description: 'Adds a level 3 heading' },
{ label: 'p', code: '<p>Paragraph</p>', description: 'Adds a paragraph' },
{ label: 'a', code: '<a href="#">Link</a>', description: 'Adds a clickable link' },
{ label: 'img', code: '<img src="https://via.placeholder.com/150" alt="Placeholder">', description: 'Adds an image' },
{ label: 'ul', code: '<ul><li>Item 1</li><li>Item 2</li></ul>', description: 'Adds an unordered list' },
{ label: 'ol', code: '<ol><li>Item 1</li><li>Item 2</li></ol>', description: 'Adds an ordered list' },
{ label: 'div', code: '<div>Divider</div>', description: 'Adds a divider' },
{ label: 'span', code: '<span>Highlight</span>', description: 'Adds a highlighted text' }
];
const insertHtmlSnippet = (code) => {
// Simply append the snippet to the end of the current content
localNodeData.value.htmlMessage = (localNodeData.value.htmlMessage || '') + '\n' + code;
saveChanges();
};
// Watch for message format changes to enable email delivery for HTML and Rich Text
watch(() => localNodeData.value.messageFormat, (newFormat) => {
if ((newFormat === 'html' || newFormat === 'richtext') && !localNodeData.value.deliveryOptions.email) {
// Automatically enable email delivery when HTML or Rich Text format is selected
localNodeData.value.deliveryOptions.email = true;
saveChanges();
}
});
</script>
<style scoped>
.notification-type-card {
min-width: 200px;
flex: 1 0 200px;
max-width: 300px;
height: 100px;
display: flex;
align-items: center;
}
/* HTML Editor Styles */
:deep(.cm-editor) {
font-family: monospace;
font-size: 14px;
}
:deep(.cm-content) {
padding: 8px;
}
/* Preview Styles */
.html-preview {
max-height: 300px;
overflow: auto;
}
</style>

View File

@ -0,0 +1,113 @@
<template>
<RsModal
v-model="showModal"
title="Notification Configuration"
size="xl"
position="center"
:okCallback="saveAndClose"
okTitle="Save"
:cancelCallback="closeModal"
>
<template #body>
<div class="mb-6">
<div class="flex items-start">
<div class="mr-4 text-blue-500 flex-shrink-0 mt-1">
<Icon name="material-symbols:notifications-outline" class="text-2xl" />
</div>
<div>
<h3 class="text-lg font-semibold mb-1">Configure Notification</h3>
<p class="text-sm text-gray-600">
Set up notifications that will be sent to users during the process execution.
Notifications can be delivered via in-app alerts, email, or SMS.
</p>
</div>
</div>
</div>
<!-- Main configuration area -->
<NotificationNodeConfiguration
:nodeData="localNodeData"
:availableVariables="availableVariables"
@update="handleUpdate"
/>
<!-- Quick Reference Guide -->
<div class="mt-6 bg-blue-50 p-4 rounded-md border border-blue-100">
<h4 class="font-medium text-blue-700 mb-2 flex items-center">
<Icon name="material-symbols:info-outline" class="mr-1" />
Quick Reference Guide
</h4>
<div class="text-sm text-blue-700">
<ul class="list-disc list-inside space-y-1">
<li>Select notification type and priority to control how it appears</li>
<li>Choose recipients who will receive the notification</li>
<li>Use variable placeholders with <code class="bg-blue-100 px-1">{variableName}</code> syntax</li>
<li>Configure delivery methods (in-app, email, SMS)</li>
<li>Set expiration time for time-sensitive notifications</li>
</ul>
</div>
</div>
</template>
</RsModal>
</template>
<script setup>
import { ref, watch } from 'vue';
import NotificationNodeConfiguration from './NotificationNodeConfiguration.vue';
import { Icon } from '#components';
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
nodeData: {
type: Object,
required: true
},
availableVariables: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['update:modelValue', 'update']);
const showModal = ref(props.modelValue);
const localNodeData = ref({ ...props.nodeData });
// Watch for changes to modelValue prop to sync modal visibility
watch(() => props.modelValue, (value) => {
showModal.value = value;
});
// Watch for changes to showModal to emit update:modelValue
watch(() => showModal.value, (value) => {
emit('update:modelValue', value);
});
// Watch for changes to nodeData prop
watch(() => props.nodeData, (value) => {
localNodeData.value = { ...value };
}, { deep: true });
function handleUpdate(updatedData) {
localNodeData.value = { ...updatedData };
}
function saveAndClose() {
emit('update', localNodeData.value);
showModal.value = false;
}
function closeModal() {
showModal.value = false;
}
</script>
<style scoped>
code {
font-family: monospace;
border-radius: 0.25rem;
}
</style>

View File

@ -119,22 +119,6 @@ const availableComponents = [
}
}
},
{
type: 'business-rule',
name: 'Business Rule',
category: 'Core',
icon: 'material-symbols:rule',
description: 'Apply business rules to process data',
defaultProps: {
label: 'Business Rule',
data: {
description: 'Applies business rules to process data',
conditions: [],
actions: [],
priority: 'medium'
}
}
},
{
type: 'gateway',
name: 'Decision Point',
@ -149,6 +133,69 @@ const availableComponents = [
defaultPath: 'Default'
}
}
},
{
type: 'notification',
name: 'Notification',
category: 'Core',
icon: 'material-symbols:notifications-outline',
description: 'Send notifications to users',
defaultProps: {
label: 'Notification',
data: {
description: 'Send notification to users',
notificationType: 'info',
recipientType: 'user',
recipientUser: '',
recipientRole: '',
recipientVariable: '',
recipientEmail: '',
subject: '',
message: '',
priority: 'medium',
deliveryOptions: {
inApp: true,
email: false,
sms: false
},
expiration: {
enabled: false,
value: 24,
unit: 'hours'
}
}
}
},
// Advanced components
{
type: 'script',
name: 'Script Task',
category: 'Advanced',
icon: 'material-symbols:code',
description: 'Execute custom code',
defaultProps: {
label: 'Script',
data: {
description: 'Script execution',
language: 'JavaScript',
script: ''
}
}
},
{
type: 'business-rule',
name: 'Business Rule',
category: 'Advanced',
icon: 'material-symbols:rule',
description: 'Apply business rules to data',
defaultProps: {
label: 'Business Rule',
data: {
description: 'Apply business rules',
ruleGroups: [],
priority: 'medium'
}
}
}
];

View File

@ -484,6 +484,87 @@ export const BusinessRuleNode = markRaw({
}
});
// Notification node
export const NotificationNode = markRaw({
props: ['id', 'type', 'label', 'selected', 'data'],
computed: {
nodeLabel() {
// Get label from either prop or data, with fallback
return this.label || (this.data && this.data.label) || 'Notification';
},
notificationType() {
return this.data?.notificationType || 'info';
},
notificationTypeIcon() {
const types = {
info: 'material-symbols:info-outline',
success: 'material-symbols:check-circle-outline',
warning: 'material-symbols:warning-outline',
error: 'material-symbols:error-outline'
};
return types[this.notificationType] || types.info;
},
notificationTypeColor() {
const colors = {
info: 'text-blue-500',
success: 'text-green-500',
warning: 'text-yellow-500',
error: 'text-red-500'
};
return colors[this.notificationType] || colors.info;
},
recipientType() {
return this.data?.recipientType || 'user';
},
recipientLabel() {
const types = {
user: 'User',
role: 'Role',
variable: 'Variable',
email: 'Email'
};
return types[this.recipientType] || 'User';
},
isConfigured() {
// Check if notification has required fields
return !!(this.data?.subject && this.data?.message);
}
},
render() {
return h(CustomNode, {
id: this.id,
type: 'notification',
label: this.nodeLabel,
selected: this.selected,
data: this.data,
onClick: () => this.$emit('node-click', this.id)
}, {
icon: () => h('i', { class: `material-icons ${this.notificationTypeColor}` }, 'notifications'),
default: () => h('div', { class: 'node-details' }, [
h('p', { class: 'node-description' }, this.data?.description || 'Send notification'),
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
h('span', { class: 'node-rule-detail-label' }, 'Type:'),
h('span', {
class: `node-rule-detail-value ml-1 font-medium ${this.notificationTypeColor}`
}, this.notificationType.charAt(0).toUpperCase() + this.notificationType.slice(1))
]),
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
h('span', { class: 'node-rule-detail-label' }, 'Recipient:'),
h('span', {
class: 'node-rule-detail-value ml-1 font-medium text-blue-600'
}, this.recipientLabel)
]),
h('div', { class: 'node-rule-detail flex items-center justify-between text-xs mt-1' }, [
h('span', { class: 'node-rule-detail-label' }, 'Status:'),
h('span', {
class: this.isConfigured ? 'node-rule-detail-value ml-1 font-medium text-green-600' : 'node-rule-detail-value ml-1 font-medium text-red-600'
}, this.isConfigured ? 'Configured' : 'Not configured')
])
])
});
}
});
// Export the node types object to use with Vue Flow
export const nodeTypes = markRaw({
task: TaskNode,
@ -493,7 +574,8 @@ export const nodeTypes = markRaw({
form: FormNode,
script: ScriptNode,
'business-rule': BusinessRuleNode,
api: ApiCallNode
api: ApiCallNode,
notification: NotificationNode
});
// Default CSS for the nodes to be imported where needed

View File

@ -0,0 +1,509 @@
<template>
<div class="notification-logs">
<div class="mb-4 flex justify-between items-center">
<h3 class="text-lg font-medium text-gray-900">Notification Logs</h3>
<div class="flex space-x-2">
<RsButton @click="refreshLogs" variant="tertiary" size="sm">
<Icon name="material-symbols:refresh" class="mr-1" />
Refresh
</RsButton>
<RsButton @click="exportLogs" variant="secondary" size="sm" :disabled="logs.length === 0">
<Icon name="material-symbols:download" class="mr-1" />
Export
</RsButton>
</div>
</div>
<!-- Filter Controls -->
<div class="bg-gray-50 border border-gray-200 rounded-md p-3 mb-4">
<div class="flex flex-wrap gap-3">
<div class="flex-1 min-w-[200px]">
<label class="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select
v-model="filters.status"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
>
<option value="all">All</option>
<option value="delivered">Delivered</option>
<option value="read">Read</option>
<option value="failed">Failed</option>
</select>
</div>
<div class="flex-1 min-w-[200px]">
<label class="block text-sm font-medium text-gray-700 mb-1">Type</label>
<select
v-model="filters.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="all">All</option>
<option value="info">Information</option>
<option value="success">Success</option>
<option value="warning">Warning</option>
<option value="error">Error</option>
</select>
</div>
<div class="flex-1 min-w-[200px]">
<label class="block text-sm font-medium text-gray-700 mb-1">Channel</label>
<select
v-model="filters.channel"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
>
<option value="all">All Channels</option>
<option value="in-app">In-App</option>
<option value="email">Email</option>
<option value="sms">SMS</option>
</select>
</div>
<div class="flex-1 min-w-[200px]">
<label class="block text-sm font-medium text-gray-700 mb-1">Date Range</label>
<div class="flex space-x-2">
<input
v-model="filters.dateFrom"
type="date"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
/>
<input
v-model="filters.dateTo"
type="date"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
/>
</div>
</div>
<div class="flex-1 min-w-[200px]">
<label class="block text-sm font-medium text-gray-700 mb-1">Search</label>
<input
v-model="filters.search"
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="Search by recipient or subject..."
/>
</div>
<div class="flex items-end">
<RsButton @click="resetFilters" variant="tertiary" size="sm">
Reset
</RsButton>
</div>
</div>
</div>
<!-- Log Items Table -->
<div class="overflow-x-auto">
<table class="min-w-full bg-white border border-gray-200 rounded-md">
<thead>
<tr class="bg-gray-50">
<th class="py-2 px-3 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
ID
</th>
<th class="py-2 px-3 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Timestamp
</th>
<th class="py-2 px-3 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Type
</th>
<th class="py-2 px-3 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Recipient
</th>
<th class="py-2 px-3 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Subject
</th>
<th class="py-2 px-3 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Channel
</th>
<th class="py-2 px-3 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th class="py-2 px-3 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr v-if="filteredLogs.length === 0">
<td colspan="8" class="py-4 px-3 text-center text-sm text-gray-500">
No notification logs found matching your filters.
</td>
</tr>
<tr v-for="log in paginatedLogs" :key="log.id" class="hover:bg-gray-50">
<td class="py-2 px-3 text-sm">
<span class="font-mono text-xs">{{ log.id.substring(0, 8) }}</span>
</td>
<td class="py-2 px-3 text-sm">
{{ formatDate(log.timestamp) }}
</td>
<td class="py-2 px-3 text-sm">
<div class="flex items-center">
<div :class="`text-${getTypeColor(log.type)}-500 mr-2`">
<Icon :name="getTypeIcon(log.type)" />
</div>
<span class="capitalize">{{ log.type }}</span>
</div>
</td>
<td class="py-2 px-3 text-sm">
{{ log.recipient }}
</td>
<td class="py-2 px-3 text-sm">
{{ log.subject }}
</td>
<td class="py-2 px-3 text-sm">
<span class="capitalize">{{ log.channel }}</span>
</td>
<td class="py-2 px-3 text-sm">
<span
:class="getStatusClass(log.status)"
class="px-2 py-1 text-xs font-medium rounded-full"
>
{{ log.status }}
</span>
</td>
<td class="py-2 px-3 text-sm">
<div class="flex space-x-2">
<button
@click="viewLog(log)"
class="text-blue-600 hover:text-blue-800"
title="View Details"
>
<Icon name="material-symbols:visibility-outline" />
</button>
<button
v-if="log.status === 'failed'"
@click="resendNotification(log)"
class="text-green-600 hover:text-green-800"
title="Resend"
>
<Icon name="material-symbols:refresh" />
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="mt-4 flex justify-between items-center">
<div class="text-sm text-gray-500">
Showing {{ paginationStart }} to {{ paginationEnd }} of {{ filteredLogs.length }} items
</div>
<div class="flex space-x-2">
<RsButton
@click="prevPage"
variant="tertiary"
size="sm"
:disabled="currentPage === 1"
>
Previous
</RsButton>
<RsButton
@click="nextPage"
variant="tertiary"
size="sm"
:disabled="currentPage >= totalPages"
>
Next
</RsButton>
</div>
</div>
<!-- View Log Modal -->
<RsModal
v-model="showViewModal"
title="Notification Log Details"
size="lg"
position="center"
:cancelCallback="closeViewModal"
>
<div class="p-4">
<div v-if="selectedLog" class="space-y-4">
<div class="flex justify-between items-start">
<div>
<h3 class="text-lg font-medium text-gray-900">{{ selectedLog.subject }}</h3>
<p class="text-sm text-gray-500">{{ formatDate(selectedLog.timestamp) }}</p>
</div>
<span
:class="getStatusClass(selectedLog.status)"
class="px-2 py-1 text-xs font-medium rounded-full"
>
{{ selectedLog.status }}
</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h4 class="text-sm font-medium text-gray-700 mb-1">Type</h4>
<div class="flex items-center">
<div :class="`text-${getTypeColor(selectedLog.type)}-500 mr-2`">
<Icon :name="getTypeIcon(selectedLog.type)" />
</div>
<span class="capitalize">{{ selectedLog.type }}</span>
</div>
</div>
<div>
<h4 class="text-sm font-medium text-gray-700 mb-1">Channel</h4>
<div class="flex items-center">
<div class="text-blue-500 mr-2">
<Icon :name="getChannelIcon(selectedLog.channel)" />
</div>
<span class="capitalize">{{ selectedLog.channel }}</span>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h4 class="text-sm font-medium text-gray-700 mb-1">Recipient</h4>
<p>{{ selectedLog.recipient }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-700 mb-1">Template</h4>
<p>{{ selectedLog.templateName || 'Custom notification' }}</p>
</div>
</div>
<div>
<h4 class="text-sm font-medium text-gray-700 mb-1">Message</h4>
<div class="bg-gray-50 p-3 rounded-md whitespace-pre-wrap">
{{ selectedLog.message }}
</div>
</div>
<div v-if="selectedLog.status === 'failed'">
<h4 class="text-sm font-medium text-gray-700 mb-1">Error</h4>
<div class="bg-red-50 p-3 rounded-md text-red-700 whitespace-pre-wrap">
{{ selectedLog.error || 'Unknown error occurred' }}
</div>
</div>
<div v-if="selectedLog.readAt">
<h4 class="text-sm font-medium text-gray-700 mb-1">Read Information</h4>
<div class="bg-green-50 p-3 rounded-md">
<p class="text-green-700">Read at: {{ formatDate(selectedLog.readAt) }}</p>
</div>
</div>
<div v-if="selectedLog.metadata">
<h4 class="text-sm font-medium text-gray-700 mb-1">Additional Information</h4>
<div class="bg-gray-50 p-3 rounded-md font-mono text-xs">
<pre>{{ JSON.stringify(selectedLog.metadata, null, 2) }}</pre>
</div>
</div>
</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', 'resend', 'export']);
// Log data
const logs = ref(props.modelValue || []);
const showViewModal = ref(false);
const selectedLog = ref(null);
// Filters
const filters = ref({
status: 'all',
type: 'all',
channel: 'all',
dateFrom: '',
dateTo: '',
search: ''
});
// Pagination
const itemsPerPage = 10;
const currentPage = ref(1);
// Watch for changes to modelValue prop
watch(() => props.modelValue, (value) => {
logs.value = value || [];
}, { deep: true });
// Computed properties
const filteredLogs = computed(() => {
let items = [...logs.value];
// Apply status filter
if (filters.value.status !== 'all') {
items = items.filter(item => item.status === filters.value.status);
}
// Apply type filter
if (filters.value.type !== 'all') {
items = items.filter(item => item.type === filters.value.type);
}
// Apply channel filter
if (filters.value.channel !== 'all') {
items = items.filter(item => item.channel === filters.value.channel);
}
// Apply date range filter
if (filters.value.dateFrom) {
const fromDate = new Date(filters.value.dateFrom);
fromDate.setHours(0, 0, 0, 0);
items = items.filter(item => new Date(item.timestamp) >= fromDate);
}
if (filters.value.dateTo) {
const toDate = new Date(filters.value.dateTo);
toDate.setHours(23, 59, 59, 999);
items = items.filter(item => new Date(item.timestamp) <= toDate);
}
// Apply search filter
if (filters.value.search) {
const searchTerm = filters.value.search.toLowerCase();
items = items.filter(item =>
(item.subject && item.subject.toLowerCase().includes(searchTerm)) ||
(item.recipient && item.recipient.toLowerCase().includes(searchTerm)) ||
(item.message && item.message.toLowerCase().includes(searchTerm))
);
}
// Sort by timestamp (newest first)
return items.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
});
const paginatedLogs = computed(() => {
const start = (currentPage.value - 1) * itemsPerPage;
const end = start + itemsPerPage;
return filteredLogs.value.slice(start, end);
});
const totalPages = computed(() => {
return Math.ceil(filteredLogs.value.length / itemsPerPage) || 1;
});
const paginationStart = computed(() => {
if (filteredLogs.value.length === 0) return 0;
return (currentPage.value - 1) * itemsPerPage + 1;
});
const paginationEnd = computed(() => {
if (filteredLogs.value.length === 0) return 0;
return Math.min(currentPage.value * itemsPerPage, filteredLogs.value.length);
});
// Methods
const refreshLogs = () => {
// In a real implementation, this would fetch updated log data from the server
// For this demo, we'll just emit an event that the parent can handle
emit('refresh-logs');
};
const exportLogs = () => {
// In a real implementation, this would trigger an export on the server
// For this demo, we'll just emit an event that the parent can handle
emit('export', filteredLogs.value);
};
const resetFilters = () => {
filters.value = {
status: 'all',
type: 'all',
channel: 'all',
dateFrom: '',
dateTo: '',
search: ''
};
currentPage.value = 1;
};
const viewLog = (log) => {
selectedLog.value = log;
showViewModal.value = true;
};
const closeViewModal = () => {
showViewModal.value = false;
selectedLog.value = null;
};
const resendNotification = (log) => {
// Emit event for resending
emit('resend', log);
};
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--;
}
};
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++;
}
};
// Helper methods
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).format(date);
};
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 getChannelIcon = (channel) => {
const icons = {
'in-app': 'material-symbols:notifications-outline',
'email': 'material-symbols:mail-outline',
'sms': 'material-symbols:smartphone-outline'
};
return icons[channel] || 'material-symbols:notifications-outline';
};
const getStatusClass = (status) => {
const classes = {
delivered: 'bg-green-100 text-green-800',
read: 'bg-blue-100 text-blue-800',
failed: 'bg-red-100 text-red-800',
pending: 'bg-yellow-100 text-yellow-800'
};
return classes[status] || 'bg-gray-100 text-gray-800';
};
</script>
<style scoped>
.notification-logs {
@apply bg-white rounded-md;
}
</style>

View File

@ -0,0 +1,746 @@
<template>
<div class="notification-manager">
<div class="mb-6">
<h2 class="text-2xl font-bold text-gray-900">Notification Manager</h2>
<p class="text-gray-600">
Manage notifications, templates, triggers, and preferences in one place.
</p>
</div>
<!-- Tabs Navigation -->
<div class="mb-6 border-b border-gray-200">
<nav class="flex -mb-px">
<button
v-for="tab in tabs"
:key="tab.id"
@click="activeTab = tab.id"
:class="[
'py-3 px-4 text-center border-b-2 font-medium text-sm',
activeTab === tab.id
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
]"
class="mr-6"
>
<div class="flex items-center">
<Icon :name="tab.icon" class="mr-2" />
{{ tab.name }}
</div>
</button>
</nav>
</div>
<!-- Tab Content -->
<div v-if="activeTab === 'templates'" class="tab-content">
<NotificationTemplates
v-model="templates"
@select="handleTemplateSelect"
/>
</div>
<div v-if="activeTab === 'triggers'" class="tab-content">
<NotificationTriggers
v-model="triggers"
:availableTemplates="templates"
:availableVariables="availableVariables"
@select="handleTriggerSelect"
/>
</div>
<div v-if="activeTab === 'queue'" class="tab-content">
<NotificationQueue
v-model="queue"
@process-queue="processQueue"
@refresh-queue="refreshQueue"
@resend="resendNotification"
/>
</div>
<div v-if="activeTab === 'logs'" class="tab-content">
<NotificationLogs
v-model="logs"
@refresh-logs="refreshLogs"
@export="exportLogs"
@resend="resendNotification"
/>
</div>
<div v-if="activeTab === 'preferences'" class="tab-content">
<UserPreferences
v-model="userPreferences"
:notificationTypes="notificationTypes"
@save="saveUserPreferences"
/>
</div>
<!-- Dashboard (Summary Tab) -->
<div v-if="activeTab === 'dashboard'" class="tab-content">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<!-- Summary Cards -->
<div class="bg-white p-4 rounded-lg border border-gray-200 shadow-sm">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center mr-3">
<Icon name="material-symbols:template-outline" class="text-blue-600" />
</div>
<h3 class="text-lg font-medium text-gray-900">Templates</h3>
</div>
<div class="flex justify-between items-center">
<span class="text-3xl font-bold text-gray-800">{{ templates.length }}</span>
<button
@click="activeTab = 'templates'"
class="text-sm text-blue-600 hover:text-blue-800 flex items-center"
>
View All
<Icon name="material-symbols:chevron-right" class="ml-1" />
</button>
</div>
</div>
<div class="bg-white p-4 rounded-lg border border-gray-200 shadow-sm">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-full bg-orange-100 flex items-center justify-center mr-3">
<Icon name="material-symbols:bolt" class="text-orange-600" />
</div>
<h3 class="text-lg font-medium text-gray-900">Triggers</h3>
</div>
<div class="flex justify-between items-center">
<span class="text-3xl font-bold text-gray-800">{{ triggers.length }}</span>
<button
@click="activeTab = 'triggers'"
class="text-sm text-blue-600 hover:text-blue-800 flex items-center"
>
View All
<Icon name="material-symbols:chevron-right" class="ml-1" />
</button>
</div>
</div>
<div class="bg-white p-4 rounded-lg border border-gray-200 shadow-sm">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center mr-3">
<Icon name="material-symbols:notifications-outline" class="text-green-600" />
</div>
<h3 class="text-lg font-medium text-gray-900">Notifications</h3>
</div>
<div class="flex justify-between items-center">
<div>
<span class="text-3xl font-bold text-gray-800">{{ queue.length }}</span>
<span class="text-sm text-gray-500 ml-2">In Queue</span>
</div>
<button
@click="activeTab = 'queue'"
class="text-sm text-blue-600 hover:text-blue-800 flex items-center"
>
View Queue
<Icon name="material-symbols:chevron-right" class="ml-1" />
</button>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="bg-white rounded-lg border border-gray-200 shadow-sm mb-8">
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50">
<h3 class="font-medium text-gray-900">Recent Notifications</h3>
</div>
<div class="p-4">
<div v-if="recentLogs.length === 0" class="text-center py-4">
<p class="text-gray-500">No recent notifications.</p>
</div>
<div v-else class="space-y-3">
<div v-for="log in recentLogs" :key="log.id" class="border-b border-gray-100 pb-3 last:border-b-0 last:pb-0">
<div class="flex items-start">
<div :class="`text-${getTypeColor(log.type)}-500 mr-3 mt-1`">
<Icon :name="getTypeIcon(log.type)" />
</div>
<div class="flex-1">
<div class="flex justify-between items-start">
<h4 class="font-medium text-gray-900">{{ log.subject }}</h4>
<span
:class="getStatusClass(log.status)"
class="px-2 py-1 text-xs font-medium rounded-full"
>
{{ log.status }}
</span>
</div>
<p class="text-sm text-gray-500 mt-1">
{{ log.recipient }} | {{ formatDate(log.timestamp) }}
</p>
</div>
</div>
</div>
</div>
<div class="mt-4 text-center">
<button
@click="activeTab = 'logs'"
class="text-sm text-blue-600 hover:text-blue-800 flex items-center justify-center mx-auto"
>
View All Logs
<Icon name="material-symbols:chevron-right" class="ml-1" />
</button>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="bg-white rounded-lg border border-gray-200 shadow-sm">
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50">
<h3 class="font-medium text-gray-900">Quick Actions</h3>
</div>
<div class="p-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<RsButton @click="activeTab = 'templates'; showNewTemplateModal = true" class="w-full">
<Icon name="material-symbols:add" class="mr-1" />
New Template
</RsButton>
<RsButton @click="activeTab = 'triggers'; showNewTriggerModal = true" class="w-full">
<Icon name="material-symbols:add" class="mr-1" />
New Trigger
</RsButton>
<RsButton @click="activeTab = 'queue'; processQueue()" class="w-full">
<Icon name="material-symbols:play-arrow" class="mr-1" />
Process Queue
</RsButton>
<RsButton @click="activeTab = 'logs'; exportLogs(logs)" class="w-full">
<Icon name="material-symbols:download" class="mr-1" />
Export Logs
</RsButton>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { Icon } from '#components';
import NotificationTemplates from './NotificationTemplates.vue';
import NotificationTriggers from './NotificationTriggers.vue';
import NotificationQueue from './NotificationQueue.vue';
import NotificationLogs from './NotificationLogs.vue';
import UserPreferences from './UserPreferences.vue';
const props = defineProps({
userId: {
type: String,
default: ''
},
availableVariables: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['update']);
// Tab navigation
const tabs = [
{ id: 'dashboard', name: 'Dashboard', icon: 'material-symbols:dashboard' },
{ id: 'templates', name: 'Templates', icon: 'material-symbols:template-outline' },
{ id: 'triggers', name: 'Triggers', icon: 'material-symbols:bolt' },
{ id: 'queue', name: 'Queue', icon: 'material-symbols:queue' },
{ id: 'logs', name: 'Logs', icon: 'material-symbols:history' },
{ id: 'preferences', name: 'Preferences', icon: 'material-symbols:settings' }
];
const activeTab = ref('dashboard');
// Mock data for demo purposes
// In a real implementation, these would be loaded from an API
const templates = ref([
{
id: 'template_1',
name: 'Task Assignment',
description: 'Notify users when they are assigned a task',
type: 'info',
subject: 'New Task Assignment: {taskName}',
message: 'Hello {userName},\n\nYou have been assigned a new task: {taskName}.\n\nDue Date: {dueDate}\nPriority: {priority}\n\nPlease log in to the system to view the details.',
emailFormat: 'html',
htmlEmailContent: `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 5px;">
<div style="background-color: #f0f7ff; padding: 15px; border-radius: 5px 5px 0 0; border-bottom: 2px solid #3b82f6;">
<h2 style="color: #1e40af; margin: 0;">New Task Assignment</h2>
</div>
<div style="padding: 20px;">
<p style="margin-top: 0;">Hello {userName},</p>
<p>You have been assigned a new task:</p>
<div style="background-color: #f9fafb; border-left: 4px solid #3b82f6; padding: 15px; margin: 15px 0;">
<h3 style="margin-top: 0; color: #1e40af;">{taskName}</h3>
<p style="margin-bottom: 5px;"><strong>Due Date:</strong> {dueDate}</p>
<p style="margin-bottom: 0;"><strong>Priority:</strong> {priority}</p>
</div>
<p>Please log in to the system to view the task details and take action.</p>
<div style="text-align: center; margin-top: 25px;">
<a href="#" style="background-color: #3b82f6; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; font-weight: bold;">View Task</a>
</div>
</div>
<div style="background-color: #f9fafb; padding: 15px; border-top: 1px solid #e0e0e0; font-size: 12px; color: #6b7280; border-radius: 0 0 5px 5px;">
<p style="margin: 0;">This is an automated notification from the Process Management System.</p>
</div>
</div>`,
channels: {
inApp: true,
email: true,
sms: false
},
priority: 'medium',
language: 'en',
expiration: {
enabled: true,
value: 48,
unit: 'hours'
}
},
{
id: 'template_2',
name: 'Approval Request',
description: 'Request approval for a document or process',
type: 'warning',
subject: 'Approval Required: {documentName}',
message: 'Hello {userName},\n\nYour approval is required for {documentName} submitted by {requesterName}.\n\nPlease review and approve or reject by {dueDate}.',
emailFormat: 'html',
htmlEmailContent: `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 5px;">
<div style="background-color: #fffbeb; padding: 15px; border-radius: 5px 5px 0 0; border-bottom: 2px solid #f59e0b;">
<h2 style="color: #92400e; margin: 0;">Approval Required</h2>
</div>
<div style="padding: 20px;">
<p style="margin-top: 0;">Hello {userName},</p>
<p>Your approval is required for the following document:</p>
<div style="background-color: #f9fafb; border-left: 4px solid #f59e0b; padding: 15px; margin: 15px 0;">
<h3 style="margin-top: 0; color: #92400e;">{documentName}</h3>
<p style="margin-bottom: 5px;"><strong>Submitted by:</strong> {requesterName}</p>
<p style="margin-bottom: 0;"><strong>Due by:</strong> {dueDate}</p>
</div>
<p>Please review the document and take action by the due date.</p>
<div style="text-align: center; margin-top: 25px; display: flex; justify-content: center; gap: 10px;">
<a href="#" style="background-color: #22c55e; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; font-weight: bold;">Approve</a>
<a href="#" style="background-color: #ef4444; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; font-weight: bold;">Reject</a>
<a href="#" style="background-color: #6b7280; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; font-weight: bold;">Review</a>
</div>
</div>
<div style="background-color: #f9fafb; padding: 15px; border-top: 1px solid #e0e0e0; font-size: 12px; color: #6b7280; border-radius: 0 0 5px 5px;">
<p style="margin: 0;">This approval request requires your attention. Please respond by the due date.</p>
</div>
</div>`,
channels: {
inApp: true,
email: true,
sms: false
},
priority: 'high',
language: 'en',
expiration: {
enabled: false,
value: 24,
unit: 'hours'
}
},
{
id: 'template_3',
name: 'Process Completed',
description: 'Notify when a process is successfully completed',
type: 'success',
subject: 'Process {processName} Completed Successfully',
message: 'Hello {userName},\n\nWe are pleased to inform you that the process {processName} has been completed successfully.\n\nCompletion Date: {completionDate}\n\nThank you for using our system.',
emailFormat: 'html',
htmlEmailContent: `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 5px;">
<div style="background-color: #ecfdf5; padding: 15px; border-radius: 5px 5px 0 0; border-bottom: 2px solid #10b981;">
<h2 style="color: #065f46; margin: 0;">Process Completed Successfully</h2>
</div>
<div style="padding: 20px;">
<p style="margin-top: 0;">Hello {userName},</p>
<p>We are pleased to inform you that the following process has been completed successfully:</p>
<div style="background-color: #f9fafb; border-left: 4px solid #10b981; padding: 15px; margin: 15px 0; text-align: center;">
<h3 style="margin-top: 0; color: #065f46;">{processName}</h3>
<p style="margin-bottom: 0;"><strong>Completion Date:</strong> {completionDate}</p>
</div>
<div style="text-align: center; margin: 25px 0;">
<img src="https://via.placeholder.com/100" alt="Success" style="width: 60px; height: 60px;">
<p style="color: #10b981; font-size: 18px; font-weight: bold; margin-top: 10px;">All tasks completed</p>
</div>
<p style="text-align: center;">Thank you for using our system.</p>
</div>
<div style="background-color: #f9fafb; padding: 15px; border-top: 1px solid #e0e0e0; font-size: 12px; color: #6b7280; border-radius: 0 0 5px 5px; text-align: center;">
<p style="margin: 0;">This is an automated notification from the Process Management System.</p>
</div>
</div>`,
channels: {
inApp: true,
email: true,
sms: false
},
priority: 'medium',
language: 'en',
expiration: {
enabled: false,
value: 24,
unit: 'hours'
}
}
]);
const triggers = ref([
{
id: 'trigger_1',
name: 'Task Assignment Notification',
description: 'Send notification when a task is assigned',
triggerType: 'event',
eventType: 'task_assignment',
conditions: [
{ field: 'priority', operator: 'equals', value: 'high' }
],
templateId: 'template_1',
templateName: 'Task Assignment',
recipientType: 'task_assignee',
enabled: true
},
{
id: 'trigger_2',
name: 'Approval Request Notification',
description: 'Send notification when approval is needed',
triggerType: 'event',
eventType: 'status_change',
fromStatus: 'pending',
toStatus: 'in_progress',
conditions: [],
templateId: 'template_2',
templateName: 'Approval Request',
recipientType: 'role',
recipientRoleId: 'role3',
enabled: true
},
{
id: 'trigger_3',
name: 'Daily Task Reminder',
description: 'Send daily reminders for overdue tasks',
triggerType: 'schedule',
scheduleType: 'recurring',
recurrencePattern: 'daily',
timeOfDay: '09:00',
conditions: [
{ field: 'status', operator: 'not_equals', value: 'completed' },
{ field: 'dueDate', operator: 'less_than', value: 'today' }
],
templateId: 'template_1',
templateName: 'Task Assignment',
recipientType: 'task_assignee',
enabled: true
}
]);
const queue = ref([
{
id: 'notification_1',
type: 'info',
recipient: 'john.doe@example.com',
subject: 'New Task Assignment: Annual Report Review',
message: 'Hello John,\n\nYou have been assigned a new task: Annual Report Review.\n\nDue Date: 2023-12-15\nPriority: High\n\nPlease log in to the system to view the details.',
status: 'pending',
createdAt: '2023-11-25T09:30:00Z',
channels: ['in-app', 'email'],
attempts: 0
},
{
id: 'notification_2',
type: 'warning',
recipient: 'jane.smith@example.com',
subject: 'Approval Required: Q4 Marketing Budget',
message: 'Hello Jane,\n\nYour approval is required for Q4 Marketing Budget submitted by John Doe.\n\nPlease review and approve or reject by 2023-11-30.',
status: 'sent',
createdAt: '2023-11-24T14:15:00Z',
channels: ['in-app', 'email'],
attempts: 1
},
{
id: 'notification_3',
type: 'error',
recipient: 'mike.johnson@example.com',
subject: 'Urgent: System Maintenance Required',
message: 'Hello Mike,\n\nUrgent attention is required for system maintenance.\n\nPlease address this issue as soon as possible.',
status: 'failed',
createdAt: '2023-11-23T18:45:00Z',
channels: ['in-app', 'email', 'sms'],
attempts: 3,
error: 'Failed to deliver SMS: Invalid phone number format',
lastAttempt: '2023-11-23T19:15:00Z'
}
]);
const logs = ref([
{
id: 'log_1',
type: 'info',
recipient: 'john.doe@example.com',
subject: 'New Task Assignment: Annual Report Review',
message: 'Hello John,\n\nYou have been assigned a new task: Annual Report Review.\n\nDue Date: 2023-12-15\nPriority: High\n\nPlease log in to the system to view the details.',
status: 'delivered',
timestamp: '2023-11-20T09:30:00Z',
channel: 'in-app',
templateName: 'Task Assignment'
},
{
id: 'log_2',
type: 'info',
recipient: 'john.doe@example.com',
subject: 'New Task Assignment: Annual Report Review',
message: 'Hello John,\n\nYou have been assigned a new task: Annual Report Review.\n\nDue Date: 2023-12-15\nPriority: High\n\nPlease log in to the system to view the details.',
status: 'delivered',
timestamp: '2023-11-20T09:30:05Z',
channel: 'email',
templateName: 'Task Assignment'
},
{
id: 'log_3',
type: 'warning',
recipient: 'jane.smith@example.com',
subject: 'Approval Required: Q4 Marketing Budget',
message: 'Hello Jane,\n\nYour approval is required for Q4 Marketing Budget submitted by John Doe.\n\nPlease review and approve or reject by 2023-11-30.',
status: 'read',
timestamp: '2023-11-19T14:15:00Z',
readAt: '2023-11-19T15:20:00Z',
channel: 'in-app',
templateName: 'Approval Request'
},
{
id: 'log_4',
type: 'success',
recipient: 'mike.johnson@example.com',
subject: 'Process Quarterly Review Completed Successfully',
message: 'Hello Mike,\n\nWe are pleased to inform you that the process Quarterly Review has been completed successfully.\n\nCompletion Date: 2023-11-18\n\nThank you for using our system.',
status: 'delivered',
timestamp: '2023-11-18T11:30:00Z',
channel: 'in-app',
templateName: 'Process Completed'
},
{
id: 'log_5',
type: 'error',
recipient: '+1234567890',
subject: 'Urgent: System Maintenance Required',
message: 'Hello Mike,\n\nUrgent attention is required for system maintenance.\n\nPlease address this issue as soon as possible.',
status: 'failed',
timestamp: '2023-11-17T18:45:00Z',
channel: 'sms',
error: 'Failed to deliver SMS: Invalid phone number format'
}
]);
const userPreferences = ref({
enabled: true,
channels: {
inApp: true,
email: true,
sms: false
},
email: 'current.user@example.com',
phoneNumber: '',
frequency: 'instant',
digestTime: '08:00',
dnd: {
enabled: true,
startTime: '22:00',
endTime: '07:00'
},
typePreferences: {
template_1: {
enabled: true,
channels: {
inApp: true,
email: true,
sms: false
}
},
template_2: {
enabled: true,
channels: {
inApp: true,
email: true,
sms: false
}
},
template_3: {
enabled: true,
channels: {
inApp: true,
email: false,
sms: false
}
}
}
});
// Notification types for preferences
const notificationTypes = computed(() => {
return templates.value.map(template => ({
id: template.id,
name: template.name,
description: template.description,
icon: getTypeIcon(template.type),
color: getTypeColor(template.type)
}));
});
// Recent logs for dashboard
const recentLogs = computed(() => {
return logs.value.slice(0, 5);
});
// Show modals for new items
const showNewTemplateModal = ref(false);
const showNewTriggerModal = ref(false);
// Watch for changes to showNewTemplateModal
watch(() => showNewTemplateModal.value, (value) => {
if (value) {
// Trigger the createNewTemplate method in the NotificationTemplates component
// This is done by emitting an event that the component will listen for
emit('new-template');
}
});
// Watch for changes to showNewTriggerModal
watch(() => showNewTriggerModal.value, (value) => {
if (value) {
// Trigger the createNewTrigger method in the NotificationTriggers component
// This is done by emitting an event that the component will listen for
emit('new-trigger');
}
});
// Methods
const handleTemplateSelect = (template) => {
console.log('Template selected:', template);
// In a real implementation, this would load the template for editing
};
const handleTriggerSelect = (trigger) => {
console.log('Trigger selected:', trigger);
// In a real implementation, this would load the trigger for editing
};
const processQueue = () => {
console.log('Processing queue...');
// In a real implementation, this would call an API to process the queue
// For demo purposes, we'll update a few items in the queue
const pendingItems = queue.value.filter(item => item.status === 'pending');
pendingItems.forEach(item => {
const index = queue.value.findIndex(i => i.id === item.id);
if (index !== -1) {
queue.value[index] = {
...item,
status: Math.random() > 0.2 ? 'sent' : 'failed',
attempts: item.attempts + 1,
lastAttempt: new Date().toISOString(),
error: Math.random() > 0.8 ? 'Connection timeout' : null
};
// Add to logs for sent items
if (queue.value[index].status === 'sent') {
logs.value.unshift({
id: `log_${Date.now()}_${queue.value[index].id}`,
type: queue.value[index].type,
recipient: queue.value[index].recipient,
subject: queue.value[index].subject,
message: queue.value[index].message,
status: 'delivered',
timestamp: new Date().toISOString(),
channel: queue.value[index].channels[0],
templateName: queue.value[index].templateName || 'Custom Notification'
});
}
}
});
};
const refreshQueue = () => {
console.log('Refreshing queue...');
// In a real implementation, this would fetch updated queue data from the API
};
const refreshLogs = () => {
console.log('Refreshing logs...');
// In a real implementation, this would fetch updated log data from the API
};
const exportLogs = (logsToExport) => {
console.log('Exporting logs:', logsToExport.length);
// In a real implementation, this would trigger an export of logs
};
const resendNotification = (item) => {
console.log('Resending notification:', item);
// In a real implementation, this would call an API to resend the notification
// For demo purposes, we'll add it back to the queue
queue.value.push({
id: `notification_${Date.now()}`,
type: item.type,
recipient: item.recipient,
subject: item.subject,
message: item.message,
status: 'pending',
createdAt: new Date().toISOString(),
channels: [item.channel],
attempts: 0
});
};
const saveUserPreferences = (preferences) => {
console.log('Saving user preferences:', preferences);
// In a real implementation, this would call an API to save the preferences
};
// Helper methods
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).format(date);
};
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 getStatusClass = (status) => {
const classes = {
delivered: 'bg-green-100 text-green-800',
read: 'bg-blue-100 text-blue-800',
failed: 'bg-red-100 text-red-800',
pending: 'bg-yellow-100 text-yellow-800',
sent: 'bg-green-100 text-green-800'
};
return classes[status] || 'bg-gray-100 text-gray-800';
};
</script>
<style scoped>
.notification-manager {
@apply bg-white rounded-md p-6;
}
.tab-content {
@apply min-h-[500px];
}
</style>

View File

@ -0,0 +1,527 @@
<template>
<div class="notification-queue">
<div class="mb-4 flex justify-between items-center">
<h3 class="text-lg font-medium text-gray-900">Notification Queue</h3>
<div class="flex space-x-2">
<RsButton @click="refreshQueue" variant="tertiary" size="sm">
<Icon name="material-symbols:refresh" class="mr-1" />
Refresh
</RsButton>
<RsButton @click="processQueue" variant="primary" size="sm" :disabled="queueItems.length === 0">
<Icon name="material-symbols:play-arrow" class="mr-1" />
Process Queue
</RsButton>
</div>
</div>
<!-- Queue Summary -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
<div class="bg-blue-50 border border-blue-100 rounded-md p-3 flex flex-col items-center">
<span class="text-blue-500 text-2xl font-semibold">{{ queueSummary.total }}</span>
<span class="text-blue-700 text-sm">Total</span>
</div>
<div class="bg-yellow-50 border border-yellow-100 rounded-md p-3 flex flex-col items-center">
<span class="text-yellow-500 text-2xl font-semibold">{{ queueSummary.pending }}</span>
<span class="text-yellow-700 text-sm">Pending</span>
</div>
<div class="bg-green-50 border border-green-100 rounded-md p-3 flex flex-col items-center">
<span class="text-green-500 text-2xl font-semibold">{{ queueSummary.sent }}</span>
<span class="text-green-700 text-sm">Sent</span>
</div>
<div class="bg-red-50 border border-red-100 rounded-md p-3 flex flex-col items-center">
<span class="text-red-500 text-2xl font-semibold">{{ queueSummary.failed }}</span>
<span class="text-red-700 text-sm">Failed</span>
</div>
</div>
<!-- Filter Controls -->
<div class="bg-gray-50 border border-gray-200 rounded-md p-3 mb-4">
<div class="flex flex-wrap gap-3">
<div class="flex-1 min-w-[200px]">
<label class="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select
v-model="filters.status"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
>
<option value="all">All</option>
<option value="pending">Pending</option>
<option value="sent">Sent</option>
<option value="failed">Failed</option>
<option value="scheduled">Scheduled</option>
</select>
</div>
<div class="flex-1 min-w-[200px]">
<label class="block text-sm font-medium text-gray-700 mb-1">Type</label>
<select
v-model="filters.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="all">All</option>
<option value="info">Information</option>
<option value="success">Success</option>
<option value="warning">Warning</option>
<option value="error">Error</option>
</select>
</div>
<div class="flex-1 min-w-[200px]">
<label class="block text-sm font-medium text-gray-700 mb-1">Search</label>
<input
v-model="filters.search"
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="Search by recipient or subject..."
/>
</div>
<div class="flex items-end">
<RsButton @click="resetFilters" variant="tertiary" size="sm">
Reset
</RsButton>
</div>
</div>
</div>
<!-- Queue Items Table -->
<div class="overflow-x-auto">
<table class="min-w-full bg-white border border-gray-200 rounded-md">
<thead>
<tr class="bg-gray-50">
<th class="py-2 px-3 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
ID
</th>
<th class="py-2 px-3 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Type
</th>
<th class="py-2 px-3 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Recipient
</th>
<th class="py-2 px-3 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Subject
</th>
<th class="py-2 px-3 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Created
</th>
<th class="py-2 px-3 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th class="py-2 px-3 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr v-if="filteredQueueItems.length === 0">
<td colspan="7" class="py-4 px-3 text-center text-sm text-gray-500">
No notifications found matching your filters.
</td>
</tr>
<tr v-for="item in filteredQueueItems" :key="item.id" class="hover:bg-gray-50">
<td class="py-2 px-3 text-sm">
<span class="font-mono text-xs">{{ item.id.substring(0, 8) }}</span>
</td>
<td class="py-2 px-3 text-sm">
<div class="flex items-center">
<div :class="`text-${getTypeColor(item.type)}-500 mr-2`">
<Icon :name="getTypeIcon(item.type)" />
</div>
<span class="capitalize">{{ item.type }}</span>
</div>
</td>
<td class="py-2 px-3 text-sm">
{{ item.recipient }}
</td>
<td class="py-2 px-3 text-sm">
{{ item.subject }}
</td>
<td class="py-2 px-3 text-sm">
{{ formatDate(item.createdAt) }}
</td>
<td class="py-2 px-3 text-sm">
<span
:class="getStatusClass(item.status)"
class="px-2 py-1 text-xs font-medium rounded-full"
>
{{ item.status }}
</span>
</td>
<td class="py-2 px-3 text-sm">
<div class="flex space-x-2">
<button
@click="viewItem(item)"
class="text-blue-600 hover:text-blue-800"
title="View Details"
>
<Icon name="material-symbols:visibility-outline" />
</button>
<button
v-if="item.status === 'pending' || item.status === 'failed'"
@click="resendItem(item)"
class="text-green-600 hover:text-green-800"
title="Resend"
>
<Icon name="material-symbols:send" />
</button>
<button
@click="deleteItem(item)"
class="text-red-600 hover:text-red-800"
title="Delete"
>
<Icon name="material-symbols:delete-outline" />
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="mt-4 flex justify-between items-center">
<div class="text-sm text-gray-500">
Showing {{ paginationStart }} to {{ paginationEnd }} of {{ filteredQueueItems.length }} items
</div>
<div class="flex space-x-2">
<RsButton
@click="prevPage"
variant="tertiary"
size="sm"
:disabled="currentPage === 1"
>
Previous
</RsButton>
<RsButton
@click="nextPage"
variant="tertiary"
size="sm"
:disabled="currentPage >= totalPages"
>
Next
</RsButton>
</div>
</div>
<!-- View Item Modal -->
<RsModal
v-model="showViewModal"
title="Notification Details"
size="lg"
position="center"
:cancelCallback="closeViewModal"
>
<div class="p-4">
<div v-if="selectedItem" class="space-y-4">
<div class="flex justify-between items-start">
<div>
<h3 class="text-lg font-medium text-gray-900">{{ selectedItem.subject }}</h3>
<p class="text-sm text-gray-500">{{ formatDate(selectedItem.createdAt) }}</p>
</div>
<span
:class="getStatusClass(selectedItem.status)"
class="px-2 py-1 text-xs font-medium rounded-full"
>
{{ selectedItem.status }}
</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h4 class="text-sm font-medium text-gray-700 mb-1">Type</h4>
<div class="flex items-center">
<div :class="`text-${getTypeColor(selectedItem.type)}-500 mr-2`">
<Icon :name="getTypeIcon(selectedItem.type)" />
</div>
<span class="capitalize">{{ selectedItem.type }}</span>
</div>
</div>
<div>
<h4 class="text-sm font-medium text-gray-700 mb-1">Channels</h4>
<div class="flex items-center space-x-2">
<span v-for="channel in selectedItem.channels" :key="channel"
class="px-2 py-1 bg-blue-100 text-blue-700 text-xs rounded-full">
{{ channel }}
</span>
</div>
</div>
</div>
<div>
<h4 class="text-sm font-medium text-gray-700 mb-1">Recipient</h4>
<p>{{ selectedItem.recipient }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-700 mb-1">Message</h4>
<div class="bg-gray-50 p-3 rounded-md whitespace-pre-wrap">
{{ selectedItem.message }}
</div>
</div>
<div v-if="selectedItem.status === 'failed'">
<h4 class="text-sm font-medium text-gray-700 mb-1">Error</h4>
<div class="bg-red-50 p-3 rounded-md text-red-700 whitespace-pre-wrap">
{{ selectedItem.error || 'Unknown error occurred' }}
</div>
</div>
<div v-if="selectedItem.attempts > 0">
<h4 class="text-sm font-medium text-gray-700 mb-1">Delivery Attempts</h4>
<p>{{ selectedItem.attempts }} attempt(s)</p>
<div v-if="selectedItem.lastAttempt" class="text-xs text-gray-500">
Last attempt: {{ formatDate(selectedItem.lastAttempt) }}
</div>
</div>
</div>
</div>
</RsModal>
<!-- Delete Confirmation Modal -->
<RsModal
v-model="showDeleteModal"
title="Delete Notification"
size="md"
position="center"
:okCallback="confirmDeleteItem"
okTitle="Delete"
:cancelCallback="cancelDeleteItem"
>
<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 Notification</h3>
<p class="text-sm text-gray-500 mt-1">
Are you sure you want to delete this notification from the queue? This action cannot be undone.
</p>
</div>
</div>
<div v-if="itemToDelete" class="bg-gray-50 p-3 rounded-md">
<p class="font-medium">{{ itemToDelete.subject }}</p>
<p class="text-sm text-gray-500">Status: {{ itemToDelete.status }}</p>
<p class="text-sm text-gray-500">Recipient: {{ itemToDelete.recipient }}</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', 'process-queue']);
// Queue data
const queueItems = ref(props.modelValue || []);
const showViewModal = ref(false);
const showDeleteModal = ref(false);
const selectedItem = ref(null);
const itemToDelete = ref(null);
// Filters
const filters = ref({
status: 'all',
type: 'all',
search: ''
});
// Pagination
const itemsPerPage = 10;
const currentPage = ref(1);
// Watch for changes to modelValue prop
watch(() => props.modelValue, (value) => {
queueItems.value = value || [];
}, { deep: true });
// Computed properties
const filteredQueueItems = computed(() => {
let items = [...queueItems.value];
// Apply status filter
if (filters.value.status !== 'all') {
items = items.filter(item => item.status === filters.value.status);
}
// Apply type filter
if (filters.value.type !== 'all') {
items = items.filter(item => item.type === filters.value.type);
}
// Apply search filter
if (filters.value.search) {
const searchTerm = filters.value.search.toLowerCase();
items = items.filter(item =>
item.subject.toLowerCase().includes(searchTerm) ||
item.recipient.toLowerCase().includes(searchTerm) ||
item.message.toLowerCase().includes(searchTerm)
);
}
return items;
});
const paginatedItems = computed(() => {
const start = (currentPage.value - 1) * itemsPerPage;
const end = start + itemsPerPage;
return filteredQueueItems.value.slice(start, end);
});
const totalPages = computed(() => {
return Math.ceil(filteredQueueItems.value.length / itemsPerPage) || 1;
});
const paginationStart = computed(() => {
if (filteredQueueItems.value.length === 0) return 0;
return (currentPage.value - 1) * itemsPerPage + 1;
});
const paginationEnd = computed(() => {
if (filteredQueueItems.value.length === 0) return 0;
return Math.min(currentPage.value * itemsPerPage, filteredQueueItems.value.length);
});
const queueSummary = computed(() => {
const total = queueItems.value.length;
const pending = queueItems.value.filter(item => item.status === 'pending').length;
const sent = queueItems.value.filter(item => item.status === 'sent').length;
const failed = queueItems.value.filter(item => item.status === 'failed').length;
return { total, pending, sent, failed };
});
// Methods
const refreshQueue = () => {
// In a real implementation, this would fetch updated queue data from the server
// For this demo, we'll just emit an event that the parent can handle
emit('refresh-queue');
};
const processQueue = () => {
// In a real implementation, this would trigger queue processing on the server
// For this demo, we'll just emit an event that the parent can handle
emit('process-queue');
};
const resetFilters = () => {
filters.value = {
status: 'all',
type: 'all',
search: ''
};
currentPage.value = 1;
};
const viewItem = (item) => {
selectedItem.value = item;
showViewModal.value = true;
};
const closeViewModal = () => {
showViewModal.value = false;
selectedItem.value = null;
};
const resendItem = (item) => {
// In a real implementation, this would trigger a re-send on the server
// For this demo, we'll just update the item status
const index = queueItems.value.findIndex(i => i.id === item.id);
if (index !== -1) {
const updatedItem = { ...queueItems.value[index] };
updatedItem.status = 'pending';
updatedItem.attempts = (updatedItem.attempts || 0) + 1;
updatedItem.lastAttempt = new Date().toISOString();
queueItems.value[index] = updatedItem;
emit('update:modelValue', queueItems.value);
}
};
const deleteItem = (item) => {
itemToDelete.value = item;
showDeleteModal.value = true;
};
const confirmDeleteItem = () => {
if (itemToDelete.value) {
queueItems.value = queueItems.value.filter(item => item.id !== itemToDelete.value.id);
emit('update:modelValue', queueItems.value);
itemToDelete.value = null;
showDeleteModal.value = false;
}
};
const cancelDeleteItem = () => {
itemToDelete.value = null;
showDeleteModal.value = false;
};
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--;
}
};
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++;
}
};
// Helper methods
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).format(date);
};
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 getStatusClass = (status) => {
const classes = {
pending: 'bg-yellow-100 text-yellow-800',
sent: 'bg-green-100 text-green-800',
failed: 'bg-red-100 text-red-800',
scheduled: 'bg-blue-100 text-blue-800'
};
return classes[status] || 'bg-gray-100 text-gray-800';
};
</script>
<style scoped>
.notification-queue {
@apply bg-white rounded-md;
}
</style>

View File

@ -0,0 +1,579 @@
<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>

View File

@ -0,0 +1,620 @@
<template>
<div class="notification-triggers">
<div class="mb-4 flex justify-between items-center">
<h3 class="text-lg font-medium text-gray-900">Notification Triggers</h3>
<RsButton @click="showNewTriggerModal = true" variant="primary" size="sm">
<Icon name="material-symbols:add" class="mr-1" />
New Trigger
</RsButton>
</div>
<!-- Triggers List -->
<div v-if="triggers.length > 0" class="space-y-3">
<div v-for="trigger in triggers" :key="trigger.id"
class="border rounded-md p-3 hover:bg-gray-50 cursor-pointer"
@click="selectTrigger(trigger)">
<div class="flex justify-between items-start">
<div>
<h4 class="font-medium text-gray-800">{{ trigger.name }}</h4>
<p class="text-sm text-gray-500">{{ trigger.description }}</p>
</div>
<div :class="`text-${getTriggerTypeColor(trigger.triggerType)}-500`">
<Icon :name="getTriggerTypeIcon(trigger.triggerType)" />
</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:trigger" class="mr-1" />
{{ trigger.triggerType }}
</span>
<span class="flex items-center">
<Icon name="material-symbols:template-outline" class="mr-1" />
{{ trigger.templateName || 'No template' }}
</span>
<span v-if="trigger.enabled" class="flex items-center text-green-500">
<Icon name="material-symbols:check-circle-outline" class="mr-1" />
Enabled
</span>
<span v-else class="flex items-center text-gray-400">
<Icon name="material-symbols:disabled-circle-outline" class="mr-1" />
Disabled
</span>
</div>
</div>
</div>
<!-- Empty State -->
<div v-else class="text-center py-8 bg-gray-50 rounded-md">
<Icon name="material-symbols:bolt" class="text-4xl text-gray-400 mb-2" />
<h4 class="text-gray-500 font-medium">No Triggers</h4>
<p class="text-sm text-gray-400 mb-4">Create triggers to define when notifications should be sent</p>
<RsButton @click="showNewTriggerModal = true" variant="primary" size="sm">
Create Trigger
</RsButton>
</div>
<!-- Trigger Modal -->
<RsModal
v-model="showTriggerModal"
:title="editingTrigger ? 'Edit Trigger' : 'New Trigger'"
size="lg"
position="center"
:okCallback="saveTrigger"
okTitle="Save Trigger"
:cancelCallback="closeTriggerModal"
>
<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">Trigger Name</label>
<input
v-model="currentTrigger.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 trigger name"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Trigger Type</label>
<select
v-model="currentTrigger.triggerType"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
>
<option value="event">Event-based</option>
<option value="schedule">Schedule-based</option>
</select>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea
v-model="currentTrigger.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 trigger's purpose"
></textarea>
</div>
<!-- Event-based settings -->
<div v-if="currentTrigger.triggerType === 'event'" class="border-t pt-4">
<h4 class="font-medium text-gray-700 mb-3">Event Settings</h4>
<div class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Event Type</label>
<select
v-model="currentTrigger.eventType"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
>
<option value="status_change">Status Change</option>
<option value="task_assignment">Task Assignment</option>
<option value="form_submission">Form Submission</option>
<option value="process_started">Process Started</option>
<option value="process_completed">Process Completed</option>
<option value="task_deadline_approaching">Task Deadline Approaching</option>
<option value="task_overdue">Task Overdue</option>
</select>
</div>
<!-- Status change specific fields -->
<div v-if="currentTrigger.eventType === 'status_change'" class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">From Status</label>
<select
v-model="currentTrigger.fromStatus"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
>
<option value="any">Any Status</option>
<option value="pending">Pending</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
<option value="rejected">Rejected</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">To Status</label>
<select
v-model="currentTrigger.toStatus"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
>
<option value="pending">Pending</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
<option value="rejected">Rejected</option>
</select>
</div>
</div>
<!-- Task deadline specific fields -->
<div v-if="currentTrigger.eventType === 'task_deadline_approaching'" class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Hours Before Deadline</label>
<input
v-model.number="currentTrigger.hoursBeforeDeadline"
type="number"
min="1"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
placeholder="24"
/>
</div>
</div>
<!-- Schedule-based settings -->
<div v-if="currentTrigger.triggerType === 'schedule'" class="border-t pt-4">
<h4 class="font-medium text-gray-700 mb-3">Schedule Settings</h4>
<div class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Schedule Type</label>
<select
v-model="currentTrigger.scheduleType"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
>
<option value="one_time">One-time</option>
<option value="recurring">Recurring</option>
</select>
</div>
<!-- One-time schedule -->
<div v-if="currentTrigger.scheduleType === 'one_time'" class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Scheduled Date/Time</label>
<input
v-model="currentTrigger.scheduledDateTime"
type="datetime-local"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
/>
</div>
<!-- Recurring schedule -->
<div v-if="currentTrigger.scheduleType === 'recurring'" class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Recurrence Pattern</label>
<select
v-model="currentTrigger.recurrencePattern"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
</select>
</div>
<!-- Weekly options -->
<div v-if="currentTrigger.recurrencePattern === 'weekly'" class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-2">Days of Week</label>
<div class="flex flex-wrap gap-2">
<label v-for="day in weekDays" :key="day.value" class="inline-flex items-center">
<input
type="checkbox"
v-model="currentTrigger.daysOfWeek"
:value="day.value"
class="form-checkbox"
/>
<span class="ml-2">{{ day.label }}</span>
</label>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Time of Day</label>
<input
v-model="currentTrigger.timeOfDay"
type="time"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
/>
</div>
</div>
</div>
<!-- Conditions -->
<div class="border-t pt-4">
<div class="flex justify-between items-center mb-2">
<h4 class="font-medium text-gray-700">Conditions</h4>
<RsButton @click="addCondition" variant="tertiary" size="xs">
<Icon name="material-symbols:add" class="mr-1" />
Add Condition
</RsButton>
</div>
<div v-if="currentTrigger.conditions.length === 0" class="text-center py-4 bg-gray-50 rounded-md text-sm text-gray-500">
No conditions added. Notification will trigger without additional conditions.
</div>
<div v-else class="space-y-3">
<div v-for="(condition, index) in currentTrigger.conditions" :key="index" class="p-3 border rounded-md bg-gray-50">
<div class="flex justify-between items-start">
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 w-full">
<div>
<select
v-model="condition.field"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
>
<option value="">Select field</option>
<option v-for="field in availableFields" :key="field.name" :value="field.name">
{{ field.label }}
</option>
</select>
</div>
<div>
<select
v-model="condition.operator"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
>
<option value="equals">Equals</option>
<option value="not_equals">Not Equals</option>
<option value="contains">Contains</option>
<option value="greater_than">Greater Than</option>
<option value="less_than">Less Than</option>
</select>
</div>
<div>
<input
v-model="condition.value"
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="Value"
/>
</div>
</div>
<button @click="removeCondition(index)" class="ml-2 text-red-500 hover:text-red-700">
<Icon name="material-symbols:delete-outline" />
</button>
</div>
</div>
</div>
</div>
<!-- Notification template -->
<div class="border-t pt-4">
<h4 class="font-medium text-gray-700 mb-3">Notification Settings</h4>
<div class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Select Template</label>
<select
v-model="currentTrigger.templateId"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
>
<option value="">Select a template</option>
<option v-for="template in availableTemplates" :key="template.id" :value="template.id">
{{ template.name }}
</option>
</select>
</div>
<!-- Recipients -->
<div class="mb-3">
<label class="block text-sm font-medium text-gray-700 mb-1">Recipients</label>
<select
v-model="currentTrigger.recipientType"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm mb-2"
>
<option value="user">Specific User</option>
<option value="role">Role</option>
<option value="process_variable">Process Variable</option>
<option value="event_initiator">Event Initiator</option>
<option value="task_assignee">Task Assignee</option>
</select>
<div v-if="currentTrigger.recipientType === 'user'">
<select
v-model="currentTrigger.recipientUserId"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
>
<option value="">Select user</option>
<option v-for="user in mockUsers" :key="user.id" :value="user.id">
{{ user.name }}
</option>
</select>
</div>
<div v-if="currentTrigger.recipientType === 'role'">
<select
v-model="currentTrigger.recipientRoleId"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
>
<option value="">Select role</option>
<option v-for="role in mockRoles" :key="role.id" :value="role.id">
{{ role.name }}
</option>
</select>
</div>
<div v-if="currentTrigger.recipientType === 'process_variable'">
<select
v-model="currentTrigger.recipientVariable"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
>
<option value="">Select variable</option>
<option v-for="variable in availableVariables" :key="variable.name" :value="variable.name">
{{ variable.label }}
</option>
</select>
</div>
</div>
</div>
<!-- Enable/Disable -->
<div class="border-t pt-4">
<label class="inline-flex items-center">
<input
type="checkbox"
v-model="currentTrigger.enabled"
class="form-checkbox"
/>
<span class="ml-2">Enable this trigger</span>
</label>
</div>
</div>
</RsModal>
<!-- Delete Confirmation Modal -->
<RsModal
v-model="showDeleteModal"
title="Delete Trigger"
size="md"
position="center"
:okCallback="confirmDeleteTrigger"
okTitle="Delete"
:cancelCallback="cancelDeleteTrigger"
>
<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 Trigger</h3>
<p class="text-sm text-gray-500 mt-1">
Are you sure you want to delete this trigger? This action cannot be undone.
</p>
</div>
</div>
<div class="bg-gray-50 p-3 rounded-md">
<p class="font-medium">{{ triggerToDelete?.name }}</p>
<p class="text-sm text-gray-500">{{ triggerToDelete?.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: () => []
},
availableTemplates: {
type: Array,
default: () => []
},
availableVariables: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['update:modelValue', 'select']);
// Trigger data
const triggers = ref(props.modelValue || []);
const showTriggerModal = ref(false);
const showNewTriggerModal = ref(false);
const showDeleteModal = ref(false);
const editingTrigger = ref(false);
const triggerToDelete = ref(null);
// Mock data
const mockUsers = [
{ id: 'user1', name: 'John Doe' },
{ id: 'user2', name: 'Jane Smith' },
{ id: 'user3', name: 'Mike Johnson' }
];
const mockRoles = [
{ id: 'role1', name: 'Administrator' },
{ id: 'role2', name: 'Manager' },
{ id: 'role3', name: 'Approver' },
{ id: 'role4', name: 'User' }
];
const weekDays = [
{ value: 'monday', label: 'Mon' },
{ value: 'tuesday', label: 'Tue' },
{ value: 'wednesday', label: 'Wed' },
{ value: 'thursday', label: 'Thu' },
{ value: 'friday', label: 'Fri' },
{ value: 'saturday', label: 'Sat' },
{ value: 'sunday', label: 'Sun' }
];
// Available fields for conditions
const availableFields = [
{ name: 'status', label: 'Status' },
{ name: 'assignee', label: 'Assignee' },
{ name: 'dueDate', label: 'Due Date' },
{ name: 'priority', label: 'Priority' },
{ name: 'formData.firstName', label: 'Form: First Name' },
{ name: 'formData.lastName', label: 'Form: Last Name' },
{ name: 'formData.email', label: 'Form: Email' }
];
// Current trigger being edited
const currentTrigger = ref({
id: '',
name: '',
description: '',
triggerType: 'event',
eventType: 'status_change',
fromStatus: 'any',
toStatus: 'completed',
hoursBeforeDeadline: 24,
scheduleType: 'recurring',
scheduledDateTime: '',
recurrencePattern: 'daily',
daysOfWeek: [],
timeOfDay: '09:00',
conditions: [],
templateId: '',
templateName: '',
recipientType: 'user',
recipientUserId: '',
recipientRoleId: '',
recipientVariable: '',
enabled: true
});
// Watch for changes to modelValue prop
watch(() => props.modelValue, (value) => {
triggers.value = value || [];
}, { deep: true });
// Methods
const selectTrigger = (trigger) => {
editingTrigger.value = true;
currentTrigger.value = JSON.parse(JSON.stringify(trigger));
showTriggerModal.value = true;
emit('select', trigger);
};
const createNewTrigger = () => {
editingTrigger.value = false;
currentTrigger.value = {
id: `trigger_${Date.now()}`,
name: '',
description: '',
triggerType: 'event',
eventType: 'status_change',
fromStatus: 'any',
toStatus: 'completed',
hoursBeforeDeadline: 24,
scheduleType: 'recurring',
scheduledDateTime: '',
recurrencePattern: 'daily',
daysOfWeek: [],
timeOfDay: '09:00',
conditions: [],
templateId: '',
templateName: '',
recipientType: 'user',
recipientUserId: '',
recipientRoleId: '',
recipientVariable: '',
enabled: true
};
showTriggerModal.value = true;
};
// Watch for new trigger modal trigger
watch(() => showNewTriggerModal.value, (value) => {
if (value) {
createNewTrigger();
showNewTriggerModal.value = false;
}
});
const saveTrigger = () => {
if (!currentTrigger.value.name) {
// Show error or validation
return;
}
// Update template name reference for display
if (currentTrigger.value.templateId) {
const selectedTemplate = props.availableTemplates.find(t => t.id === currentTrigger.value.templateId);
if (selectedTemplate) {
currentTrigger.value.templateName = selectedTemplate.name;
}
}
if (editingTrigger.value) {
// Update existing trigger
const index = triggers.value.findIndex(t => t.id === currentTrigger.value.id);
if (index !== -1) {
triggers.value[index] = JSON.parse(JSON.stringify(currentTrigger.value));
}
} else {
// Add new trigger
triggers.value.push(JSON.parse(JSON.stringify(currentTrigger.value)));
}
emit('update:modelValue', triggers.value);
showTriggerModal.value = false;
};
const closeTriggerModal = () => {
showTriggerModal.value = false;
};
const addCondition = () => {
currentTrigger.value.conditions.push({
field: '',
operator: 'equals',
value: ''
});
};
const removeCondition = (index) => {
currentTrigger.value.conditions.splice(index, 1);
};
const deleteTrigger = (trigger) => {
triggerToDelete.value = trigger;
showDeleteModal.value = true;
};
const confirmDeleteTrigger = () => {
if (triggerToDelete.value) {
triggers.value = triggers.value.filter(t => t.id !== triggerToDelete.value.id);
emit('update:modelValue', triggers.value);
triggerToDelete.value = null;
showDeleteModal.value = false;
}
};
const cancelDeleteTrigger = () => {
triggerToDelete.value = null;
showDeleteModal.value = false;
};
// Helper methods
const getTriggerTypeIcon = (type) => {
if (type === 'event') return 'material-symbols:bolt';
if (type === 'schedule') return 'material-symbols:schedule';
return 'material-symbols:bolt';
};
const getTriggerTypeColor = (type) => {
if (type === 'event') return 'orange';
if (type === 'schedule') return 'blue';
return 'gray';
};
</script>
<style scoped>
.notification-triggers {
@apply bg-white rounded-md;
}
</style>

View File

@ -0,0 +1,315 @@
<template>
<div class="user-preferences">
<div class="mb-4 flex justify-between items-center">
<h3 class="text-lg font-medium text-gray-900">Notification Preferences</h3>
<div class="flex space-x-2">
<RsButton @click="savePreferences" variant="primary" size="sm">
<Icon name="material-symbols:save" class="mr-1" />
Save Preferences
</RsButton>
</div>
</div>
<!-- General Preferences -->
<div class="bg-white border border-gray-200 rounded-md mb-4">
<div class="border-b border-gray-200 px-4 py-3 bg-gray-50">
<h4 class="font-medium text-gray-700">General Preferences</h4>
</div>
<div class="p-4 space-y-4">
<div>
<label class="flex items-center">
<input
type="checkbox"
v-model="preferences.enabled"
class="form-checkbox h-4 w-4 text-blue-600"
/>
<span class="ml-2">Enable all notifications</span>
</label>
<p class="mt-1 text-xs text-gray-500 ml-6">
When disabled, you will not receive any notifications regardless of other settings
</p>
</div>
<div v-if="preferences.enabled">
<h5 class="text-sm font-medium text-gray-700 mb-2">Default Delivery Methods</h5>
<div class="space-y-2">
<label class="flex items-center">
<input
type="checkbox"
v-model="preferences.channels.inApp"
class="form-checkbox h-4 w-4 text-blue-600"
/>
<span class="ml-2">In-app notifications</span>
</label>
<label class="flex items-center">
<input
type="checkbox"
v-model="preferences.channels.email"
class="form-checkbox h-4 w-4 text-blue-600"
/>
<span class="ml-2">Email notifications</span>
</label>
</div>
</div>
<div v-if="preferences.enabled && preferences.channels.email">
<label class="block text-sm font-medium text-gray-700 mb-1">Email Address</label>
<input
v-model="preferences.email"
type="email"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
placeholder="your.email@example.com"
/>
</div>
<div v-if="preferences.enabled">
<h5 class="text-sm font-medium text-gray-700 mb-2">Notification Frequency</h5>
<div class="space-y-2">
<label class="flex items-center">
<input
type="radio"
v-model="preferences.frequency"
value="instant"
class="form-radio h-4 w-4 text-blue-600"
/>
<span class="ml-2">Instant (receive notifications immediately)</span>
</label>
<label class="flex items-center">
<input
type="radio"
v-model="preferences.frequency"
value="digest"
class="form-radio h-4 w-4 text-blue-600"
/>
<span class="ml-2">Digest (receive a daily summary)</span>
</label>
</div>
</div>
<div v-if="preferences.enabled && preferences.frequency === 'digest'">
<label class="block text-sm font-medium text-gray-700 mb-1">Digest Time</label>
<select
v-model="preferences.digestTime"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
>
<option value="08:00">Morning (8:00 AM)</option>
<option value="12:00">Noon (12:00 PM)</option>
<option value="17:00">Evening (5:00 PM)</option>
</select>
</div>
<div v-if="preferences.enabled">
<h5 class="text-sm font-medium text-gray-700 mb-2">Do Not Disturb</h5>
<label class="flex items-center mb-2">
<input
type="checkbox"
v-model="preferences.dnd.enabled"
class="form-checkbox h-4 w-4 text-blue-600"
/>
<span class="ml-2">Enable Do Not Disturb hours</span>
</label>
<div v-if="preferences.dnd.enabled" class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Start Time</label>
<input
v-model="preferences.dnd.startTime"
type="time"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">End Time</label>
<input
v-model="preferences.dnd.endTime"
type="time"
class="w-full p-2 border rounded-md shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 text-sm"
/>
</div>
</div>
</div>
</div>
</div>
<!-- Notification Type Preferences -->
<div class="bg-white border border-gray-200 rounded-md">
<div class="border-b border-gray-200 px-4 py-3 bg-gray-50">
<h4 class="font-medium text-gray-700">Notification Type Preferences</h4>
</div>
<div class="p-4">
<p class="text-sm text-gray-500 mb-4">
Customize how you receive different types of notifications.
</p>
<div v-if="notificationTypes.length === 0" class="text-center py-4 bg-gray-50 rounded-md">
<p class="text-gray-500">No notification types available to configure.</p>
</div>
<div v-else class="space-y-4">
<div v-for="type in notificationTypes" :key="type.id" class="border-b border-gray-100 pb-4 last:border-b-0 last:pb-0">
<div class="flex items-start justify-between mb-2">
<div class="flex items-center">
<div :class="`text-${type.color}-500 mr-2`">
<Icon :name="type.icon" class="text-xl" />
</div>
<h5 class="font-medium">{{ type.name }}</h5>
</div>
<label class="inline-flex items-center">
<input
type="checkbox"
v-model="typePreferences[type.id].enabled"
class="form-checkbox h-4 w-4 text-blue-600"
:disabled="!preferences.enabled"
/>
<span class="ml-2 text-sm">Enabled</span>
</label>
</div>
<p class="text-sm text-gray-500 mb-3">{{ type.description }}</p>
<div v-if="typePreferences[type.id].enabled && preferences.enabled" class="bg-gray-50 p-3 rounded-md">
<h6 class="text-sm font-medium mb-2">Delivery Methods</h6>
<div class="space-y-1">
<label class="flex items-center">
<input
type="checkbox"
v-model="typePreferences[type.id].channels.inApp"
class="form-checkbox h-4 w-4 text-blue-600"
:disabled="!preferences.channels.inApp"
/>
<span class="ml-2 text-sm" :class="{'text-gray-400': !preferences.channels.inApp}">
In-app notifications
</span>
</label>
<label class="flex items-center">
<input
type="checkbox"
v-model="typePreferences[type.id].channels.email"
class="form-checkbox h-4 w-4 text-blue-600"
:disabled="!preferences.channels.email"
/>
<span class="ml-2 text-sm" :class="{'text-gray-400': !preferences.channels.email}">
Email notifications
</span>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { Icon } from '#components';
const props = defineProps({
userId: {
type: String,
default: ''
},
modelValue: {
type: Object,
default: () => ({})
},
notificationTypes: {
type: Array,
default: () => []
}
});
const emit = defineEmits(['update:modelValue', 'save']);
// Default preferences
const defaultPreferences = {
enabled: true,
channels: {
inApp: true,
email: false,
sms: false
},
email: '',
phoneNumber: '',
frequency: 'instant',
digestTime: '08:00',
dnd: {
enabled: false,
startTime: '22:00',
endTime: '08:00'
}
};
// User preferences
const preferences = ref({ ...defaultPreferences });
// Type-specific preferences
const typePreferences = ref({});
// Initialize preferences from props
watch(() => props.modelValue, (value) => {
if (value && Object.keys(value).length > 0) {
preferences.value = {
...defaultPreferences,
...value
};
} else {
preferences.value = { ...defaultPreferences };
}
}, { immediate: true, deep: true });
// Initialize type preferences
onMounted(() => {
initializeTypePreferences();
});
watch(() => props.notificationTypes, () => {
initializeTypePreferences();
}, { deep: true });
const initializeTypePreferences = () => {
// Create default preferences for each notification type
props.notificationTypes.forEach(type => {
// If we already have preferences for this type, keep them
if (!typePreferences.value[type.id]) {
typePreferences.value[type.id] = {
enabled: true,
channels: {
inApp: true,
email: false,
sms: false
}
};
}
// If we have saved preferences in the modelValue, use those
if (props.modelValue && props.modelValue.typePreferences && props.modelValue.typePreferences[type.id]) {
typePreferences.value[type.id] = {
...typePreferences.value[type.id],
...props.modelValue.typePreferences[type.id]
};
}
});
};
const savePreferences = () => {
// Combine general preferences with type-specific preferences
const combinedPreferences = {
...preferences.value,
typePreferences: typePreferences.value
};
// Update model value
emit('update:modelValue', combinedPreferences);
// Emit save event
emit('save', combinedPreferences);
};
</script>
<style scoped>
.user-preferences {
@apply bg-white rounded-md;
}
</style>

View File

@ -0,0 +1,75 @@
<script setup>
import { ref, onMounted, computed } from 'vue';
import { useVariableStore } from '~/stores/variableStore';
import NotificationManager from '~/components/process-flow/notification/NotificationManager.vue';
// Define page meta
definePageMeta({
title: "Notification Manager",
description: "Manage notifications, templates, triggers, and user preferences",
layout: "default",
middleware: ["auth"],
requiresAuth: true,
});
// Initialize the store
const variableStore = useVariableStore();
// Get available variables for usage in notifications
const availableVariables = computed(() => {
const processVars = variableStore.getAllVariables.process.map(v => ({
name: v.name || 'unnamed',
label: v?.description
? `${v.description} (${v.name || 'unnamed'}, process)`
: `${v.name || 'unnamed'} (process)` ,
type: v.type || 'string',
scope: 'process'
}));
const globalVars = variableStore.getAllVariables.global.map(v => ({
name: v.name || 'unnamed',
label: v?.description
? `${v.description} (${v.name || 'unnamed'}, global)`
: `${v.name || 'unnamed'} (global)` ,
type: v.type || 'string',
scope: 'global'
}));
return [...processVars, ...globalVars];
});
// Current user ID
const userId = ref('current_user');
// Get notification preferences (in a real implementation, this would be fetched from the API)
onMounted(() => {
// In a real implementation, fetch user data, notification templates, triggers, etc.
console.log('Notification Manager loaded');
});
</script>
<template>
<div>
<div class="bg-white shadow">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<h1 class="text-3xl font-bold text-gray-900">
Notification Center
</h1>
</div>
</div>
<main>
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<NotificationManager
:userId="userId"
:availableVariables="availableVariables"
/>
</div>
</div>
</main>
</div>
</template>
<style scoped>
/* Additional styles if needed */
</style>

View File

@ -17,6 +17,7 @@ import FormNodeConfigurationModal from '~/components/process-flow/FormNodeConfig
import TaskNodeConfiguration from '~/components/process-flow/TaskNodeConfiguration.vue';
import BusinessRuleNodeConfiguration from '~/components/process-flow/BusinessRuleNodeConfiguration.vue';
import BusinessRuleNodeConfigurationModal from '~/components/process-flow/BusinessRuleNodeConfigurationModal.vue';
import NotificationNodeConfigurationModal from '~/components/process-flow/NotificationNodeConfigurationModal.vue';
// Define page meta
definePageMeta({
@ -64,6 +65,7 @@ const showFormConfigModal = ref(false);
const showApiConfigModal = ref(false);
const showGatewayConfigModal = ref(false);
const showBusinessRuleConfigModal = ref(false);
const showNotificationConfigModal = ref(false);
// Component definitions
const components = [
@ -717,6 +719,14 @@ const handleDefaultPathUpdate = (path) => {
updateNodeInStore();
}
};
// Handle notification node update
const handleNotificationNodeUpdate = (updatedData) => {
if (selectedNodeData.value) {
selectedNodeData.value.data = { ...updatedData };
updateNodeInStore();
}
};
</script>
<template>
@ -851,6 +861,13 @@ const handleDefaultPathUpdate = (path) => {
Configure Business Rule
</RsButton>
</div>
<!-- Notification Configuration -->
<div v-if="selectedNodeData.type === 'notification'">
<RsButton @click="showNotificationConfigModal = true" variant="primary" class="w-full">
Configure Notification
</RsButton>
</div>
</div>
</div>
</div>
@ -957,6 +974,15 @@ const handleDefaultPathUpdate = (path) => {
:availableVariables="gatewayAvailableVariables"
@update="handleBusinessRuleUpdate"
/>
<!-- Notification Configuration Modal -->
<NotificationNodeConfigurationModal
v-if="selectedNodeData && selectedNodeData.type === 'notification'"
v-model="showNotificationConfigModal"
:nodeData="selectedNodeData.data"
:availableVariables="gatewayAvailableVariables"
@update="handleNotificationNodeUpdate"
/>
</div>
</template>
@ -1020,6 +1046,13 @@ const handleDefaultPathUpdate = (path) => {
border-left: 4px solid #9333ea; /* Purple border to match our icon color */
}
:deep(.node-notification) {
min-width: 160px;
background: white;
border: 1px solid #ddd;
border-left: 4px solid #3b82f6; /* Blue border to match our icon color */
}
:deep(.node-details) {
margin-top: 8px;
font-size: 0.75rem;