314 lines
15 KiB
Vue
314 lines
15 KiB
Vue
<script setup>
|
|
import { ref, computed } from 'vue';
|
|
|
|
definePageMeta({
|
|
title: "Notification Triggers & Rules",
|
|
middleware: ["auth"],
|
|
requiresAuth: true,
|
|
});
|
|
|
|
const activeTab = ref('triggers'); // 'triggers', 'segments'
|
|
|
|
// Mock data
|
|
const mockEvents = ref([
|
|
{ id: 'user_registered', name: 'User Registered' },
|
|
{ id: 'payment_completed', name: 'Payment Completed' },
|
|
{ id: 'password_reset_request', name: 'Password Reset Request' },
|
|
]);
|
|
|
|
const mockTemplates = ref([
|
|
{ id: 'welcome_email', name: 'Welcome Email' },
|
|
{ id: 'payment_receipt', name: 'Payment Receipt' },
|
|
{ id: 'reset_password_instructions', name: 'Reset Password Instructions' },
|
|
]);
|
|
|
|
const triggers = ref([
|
|
{
|
|
id: 'trg_001',
|
|
name: 'Welcome New Users',
|
|
description: 'Sends a welcome email upon user registration.',
|
|
type: 'event',
|
|
eventType: 'user_registered',
|
|
actionTemplateId: 'welcome_email',
|
|
priority: 'medium',
|
|
status: 'active',
|
|
conditions: [],
|
|
targetSegments: [],
|
|
dependencies: null,
|
|
},
|
|
{
|
|
id: 'trg_002',
|
|
name: 'Daily Sales Summary',
|
|
description: 'Sends a summary of sales daily at 8 AM.',
|
|
type: 'time',
|
|
schedule: '0 8 * * *', // Cron for 8 AM daily
|
|
actionTemplateId: 'payment_receipt', // Placeholder, should be a summary template
|
|
priority: 'low',
|
|
status: 'inactive',
|
|
conditions: [],
|
|
targetSegments: [],
|
|
dependencies: null,
|
|
}
|
|
]);
|
|
|
|
const showAddEditModal = ref(false);
|
|
const isEditing = ref(false);
|
|
const currentTrigger = ref(null);
|
|
|
|
const newTriggerData = ref({
|
|
id: null,
|
|
name: '',
|
|
description: '',
|
|
type: 'event',
|
|
eventType: mockEvents.value.length > 0 ? mockEvents.value[0].id : null,
|
|
schedule: '',
|
|
webhookUrl: 'https://api.example.com/webhook/generated_id', // Placeholder
|
|
actionTemplateId: mockTemplates.value.length > 0 ? mockTemplates.value[0].id : null,
|
|
priority: 'medium',
|
|
status: 'active',
|
|
conditions: [],
|
|
targetSegments: [],
|
|
dependencies: null,
|
|
});
|
|
|
|
const priorities = ['low', 'medium', 'high'];
|
|
const triggerTypes = [
|
|
{ id: 'event', name: 'Event-Based' },
|
|
{ id: 'time', name: 'Time-Based' },
|
|
{ id: 'api', name: 'External API/Webhook' }
|
|
];
|
|
|
|
|
|
const openAddModal = () => {
|
|
isEditing.value = false;
|
|
currentTrigger.value = null;
|
|
newTriggerData.value = {
|
|
id: `trg_${Date.now().toString().slice(-3)}`, // Simple unique ID for mock
|
|
name: '',
|
|
description: '',
|
|
type: 'event',
|
|
eventType: mockEvents.value.length > 0 ? mockEvents.value[0].id : null,
|
|
schedule: '',
|
|
webhookUrl: `https://api.example.com/webhook/trg_${Date.now().toString().slice(-3)}`,
|
|
actionTemplateId: mockTemplates.value.length > 0 ? mockTemplates.value[0].id : null,
|
|
priority: 'medium',
|
|
status: 'active',
|
|
conditions: [],
|
|
targetSegments: [],
|
|
dependencies: null,
|
|
};
|
|
showAddEditModal.value = true;
|
|
};
|
|
|
|
const openEditModal = (trigger) => {
|
|
isEditing.value = true;
|
|
currentTrigger.value = trigger;
|
|
newTriggerData.value = { ...trigger };
|
|
showAddEditModal.value = true;
|
|
};
|
|
|
|
const closeModal = () => {
|
|
showAddEditModal.value = false;
|
|
currentTrigger.value = null;
|
|
};
|
|
|
|
const saveTrigger = () => {
|
|
if (isEditing.value && currentTrigger.value) {
|
|
const index = triggers.value.findIndex(t => t.id === currentTrigger.value.id);
|
|
if (index !== -1) {
|
|
triggers.value[index] = { ...newTriggerData.value };
|
|
}
|
|
} else {
|
|
triggers.value.push({ ...newTriggerData.value, id: newTriggerData.value.id || `trg_${Date.now().toString().slice(-3)}` });
|
|
}
|
|
closeModal();
|
|
};
|
|
|
|
const deleteTrigger = (triggerId) => {
|
|
if (confirm('Are you sure you want to delete this trigger?')) {
|
|
triggers.value = triggers.value.filter(t => t.id !== triggerId);
|
|
}
|
|
};
|
|
|
|
const testTrigger = (trigger) => {
|
|
alert(`Simulating test for trigger: ${trigger.name} (Not implemented yet)`);
|
|
};
|
|
|
|
const getEventName = (eventId) => mockEvents.value.find(e => e.id === eventId)?.name || eventId;
|
|
const getTemplateName = (templateId) => mockTemplates.value.find(t => t.id === templateId)?.name || templateId;
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<LayoutsBreadcrumb />
|
|
<rs-card>
|
|
<template #header>
|
|
<div class="flex justify-between items-center">
|
|
<h1 class="text-xl font-semibold">Notification Triggers & Rules</h1>
|
|
<button @click="openAddModal" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm">
|
|
Add New Trigger
|
|
</button>
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<!-- Tabs (Simplified for now, can be expanded later if needed) -->
|
|
<!-- <div class="mb-4 border-b border-gray-200">
|
|
<nav class="-mb-px flex space-x-8" aria-label="Tabs">
|
|
<button @click="activeTab = 'triggers'"
|
|
:class="['whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm', activeTab === 'triggers' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300']">
|
|
Triggers & Rules
|
|
</button>
|
|
<button @click="activeTab = 'segments'"
|
|
:class="['whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm', activeTab === 'segments' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300']">
|
|
User Segments (Coming Soon)
|
|
</button>
|
|
</nav>
|
|
</div> -->
|
|
|
|
<div v-if="activeTab === 'triggers'">
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Details</th>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Priority</th>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
<tr v-if="triggers.length === 0">
|
|
<td colspan="6" class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-center">No triggers defined yet.</td>
|
|
</tr>
|
|
<tr v-for="trigger in triggers" :key="trigger.id">
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm font-medium text-gray-900">{{ trigger.name }}</div>
|
|
<div class="text-xs text-gray-500">{{ trigger.description }}</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 capitalize">{{ trigger.type }}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
<div v-if="trigger.type === 'event'">Event: {{ getEventName(trigger.eventType) }}</div>
|
|
<div v-if="trigger.type === 'time'">Schedule: {{ trigger.schedule }}</div>
|
|
<div v-if="trigger.type === 'api'" class="truncate max-w-xs" :title="trigger.webhookUrl">Webhook: {{ trigger.webhookUrl }}</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 capitalize">{{ trigger.priority }}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span :class="['px-2 inline-flex text-xs leading-5 font-semibold rounded-full', trigger.status === 'active' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800']">
|
|
{{ trigger.status }}
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
|
|
<button @click="openEditModal(trigger)" class="text-indigo-600 hover:text-indigo-900">Edit</button>
|
|
<button @click="testTrigger(trigger)" class="text-yellow-600 hover:text-yellow-900">Test</button>
|
|
<button @click="deleteTrigger(trigger.id)" class="text-red-600 hover:text-red-900">Delete</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- <div v-if="activeTab === 'segments'">
|
|
<p class="text-gray-600 p-4">User Segments management will be available here soon.</p>
|
|
</div> -->
|
|
|
|
<!-- Add/Edit Modal -->
|
|
<div v-if="showAddEditModal" class="fixed inset-0 z-50 overflow-y-auto bg-gray-600 bg-opacity-50 flex items-center justify-center p-4">
|
|
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="text-lg font-medium leading-6 text-gray-900">{{ isEditing ? 'Edit' : 'Add New' }} Trigger/Rule</h3>
|
|
<button @click="closeModal" class="text-gray-400 hover:text-gray-600">
|
|
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
|
</button>
|
|
</div>
|
|
|
|
<form @submit.prevent="saveTrigger" class="space-y-4">
|
|
<div>
|
|
<label for="triggerName" class="block text-sm font-medium text-gray-700">Name</label>
|
|
<input type="text" v-model="newTriggerData.name" id="triggerName" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
|
</div>
|
|
<div>
|
|
<label for="triggerDescription" class="block text-sm font-medium text-gray-700">Description</label>
|
|
<textarea v-model="newTriggerData.description" id="triggerDescription" rows="2" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"></textarea>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label for="triggerStatus" class="block text-sm font-medium text-gray-700">Status</label>
|
|
<select v-model="newTriggerData.status" id="triggerStatus" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
|
<option value="active">Active</option>
|
|
<option value="inactive">Inactive</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="triggerPriority" class="block text-sm font-medium text-gray-700">Priority</label>
|
|
<select v-model="newTriggerData.priority" id="triggerPriority" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
|
<option v-for="p in priorities" :key="p" :value="p" class="capitalize">{{ p }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="triggerType" class="block text-sm font-medium text-gray-700">Trigger Type</label>
|
|
<select v-model="newTriggerData.type" id="triggerType" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
|
<option v-for="tt in triggerTypes" :key="tt.id" :value="tt.id">{{ tt.name }}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Conditional Fields -->
|
|
<div v-if="newTriggerData.type === 'event'">
|
|
<label for="eventType" class="block text-sm font-medium text-gray-700">Event</label>
|
|
<select v-model="newTriggerData.eventType" id="eventType" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
|
<option v-for="event in mockEvents" :key="event.id" :value="event.id">{{ event.name }}</option>
|
|
</select>
|
|
</div>
|
|
<div v-if="newTriggerData.type === 'time'">
|
|
<label for="triggerSchedule" class="block text-sm font-medium text-gray-700">Schedule (Cron Expression or Description)</label>
|
|
<input type="text" v-model="newTriggerData.schedule" id="triggerSchedule" placeholder="e.g., 0 9 * * * OR Daily at 9 AM" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
|
</div>
|
|
<div v-if="newTriggerData.type === 'api'">
|
|
<label class="block text-sm font-medium text-gray-700">Webhook URL</label>
|
|
<input type="text" :value="newTriggerData.webhookUrl" readonly class="mt-1 block w-full rounded-md border-gray-300 shadow-sm bg-gray-100 sm:text-sm cursor-not-allowed">
|
|
<p class="text-xs text-gray-500 mt-1">This URL is automatically generated. Send a POST request here to trigger.</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="actionTemplate" class="block text-sm font-medium text-gray-700">Action: Send Notification Template</label>
|
|
<select v-model="newTriggerData.actionTemplateId" id="actionTemplate" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
|
<option v-for="template in mockTemplates" :key="template.id" :value="template.id">{{ template.name }}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mt-6 p-3 bg-gray-50 rounded-md">
|
|
<h4 class="text-sm font-medium text-gray-600 mb-2">Advanced Configuration (Coming Soon)</h4>
|
|
<p class="text-xs text-gray-500">- Conditional Logic (IF/THEN Rules)</p>
|
|
<p class="text-xs text-gray-500">- User Segmentation Targeting</p>
|
|
<p class="text-xs text-gray-500">- Rule Dependencies</p>
|
|
<p class="text-xs text-gray-500">- Rule Testing & Simulation</p>
|
|
</div>
|
|
|
|
<div class="pt-5">
|
|
<div class="flex justify-end space-x-3">
|
|
<button type="button" @click="closeModal" class="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">Cancel</button>
|
|
<button type="submit" class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700">
|
|
{{ isEditing ? 'Save Changes' : 'Add Trigger' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* Scoped styles if needed */
|
|
.max-h-\[90vh\] {
|
|
max-height: 90vh;
|
|
}
|
|
</style> |