Add user preferences and notification settings management page. Implement CRUD functionality for notification categories, channels, and frequency options. Introduce user preference audit with search capabilities and mock user data handling. Enhance UI with tab navigation and modals for adding/editing categories and frequencies.

This commit is contained in:
amirrulhaiqal98 2025-05-30 19:09:42 +08:00
parent 08620adaab
commit 09ba340ba7
2 changed files with 950 additions and 8 deletions

View File

@ -1,7 +1,642 @@
<script setup>
import { ref, computed } from 'vue';
definePageMeta({
title: "Admin: User Preferences & System Settings",
middleware: ["auth"],
requiresAuth: true,
});
const currentTab = ref('categories'); // categories, channels, frequencies, globalQuietHours, userAudit, bulkOps
// --- Admin: Notification Categories ---
const adminNotificationCategories = ref([
{ id: 'cat_promo', name: 'Promotions & Offers', description: 'Updates on new promotions, discounts, etc.', defaultSubscribed: true, isActive: true },
{ id: 'cat_alerts', name: 'Critical Alerts', description: 'Important security or account issue alerts.', defaultSubscribed: true, isActive: true },
{ id: 'cat_updates', name: 'Product Updates', description: 'New features, improvements, system maintenance.', defaultSubscribed: true, isActive: true },
{ id: 'cat_newsletter', name: 'Newsletter', description: 'Regular news and tips.', defaultSubscribed: false, isActive: true },
{ id: 'cat_surveys', name: 'Feedback Surveys', description: 'Occasional surveys to improve our service.', defaultSubscribed: false, isActive: false },
]);
const showCategoryModal = ref(false);
const editingCategory = ref(null);
const categoryForm = ref({ id: null, name: '', description: '', defaultSubscribed: false, isActive: true });
function openAddCategoryModal() {
editingCategory.value = null;
categoryForm.value = { id: `cat_${Date.now().toString().slice(-4)}`, name: '', description: '', defaultSubscribed: false, isActive: true };
showCategoryModal.value = true;
}
function openEditCategoryModal(category) {
editingCategory.value = { ...category };
categoryForm.value = { ...category };
showCategoryModal.value = true;
}
function saveCategory() {
if (editingCategory.value && editingCategory.value.id) {
const index = adminNotificationCategories.value.findIndex(c => c.id === editingCategory.value.id);
if (index !== -1) {
adminNotificationCategories.value[index] = { ...categoryForm.value };
}
} else {
adminNotificationCategories.value.push({ ...categoryForm.value, id: categoryForm.value.id || `cat_${Date.now().toString().slice(-4)}` });
}
showCategoryModal.value = false;
}
function toggleCategoryStatus(category) {
category.isActive = !category.isActive;
}
// --- Admin: Channels ---
const adminChannels = ref([
{ id: 'email', name: 'Email', isEnabled: true, defaultFrequencyCap: 'No Limit', supportedMessageTypes: ['cat_promo', 'cat_alerts', 'cat_updates', 'cat_newsletter'] },
{ id: 'sms', name: 'SMS', isEnabled: true, defaultFrequencyCap: '5 per day', supportedMessageTypes: ['cat_alerts'] },
{ id: 'push', name: 'Push Notifications', isEnabled: false, defaultFrequencyCap: '10 per day', supportedMessageTypes: ['cat_alerts', 'cat_updates'] },
]);
// --- Admin: Frequencies ---
const adminFrequencies = ref([
{ id: 'freq_immediate', label: 'Immediate', value: 'immediate', isUserSelectable: true, isDefault: true },
{ id: 'freq_hourly', label: 'Hourly Digest', value: 'hourly', isUserSelectable: true, isDefault: false },
{ id: 'freq_daily', label: 'Daily Digest', value: 'daily', isUserSelectable: true, isDefault: false },
{ id: 'freq_weekly', label: 'Weekly Digest', value: 'weekly', isUserSelectable: true, isDefault: false }, // Made user selectable for demo
{ id: 'freq_monthly', label: 'Monthly Summary', value: 'monthly', isUserSelectable: false, isDefault: false },
]);
const showFrequencyModal = ref(false);
const editingFrequency = ref(null);
const frequencyForm = ref({ id: null, label: '', value: '', isUserSelectable: true, isDefault: false });
function openAddFrequencyModal() {
editingFrequency.value = null;
frequencyForm.value = { id: `freq_${Date.now().toString().slice(-4)}`, label: '', value: '', isUserSelectable: true, isDefault: false };
showFrequencyModal.value = true;
}
function openEditFrequencyModal(freq) {
editingFrequency.value = { ...freq };
frequencyForm.value = { ...freq };
showFrequencyModal.value = true;
}
function saveFrequency() {
if (editingFrequency.value && editingFrequency.value.id) {
const index = adminFrequencies.value.findIndex(f => f.id === editingFrequency.value.id);
if (index !== -1) {
adminFrequencies.value[index] = { ...frequencyForm.value };
}
} else {
adminFrequencies.value.push({ ...frequencyForm.value, id: frequencyForm.value.id || `freq_${Date.now().toString().slice(-4)}` });
}
showFrequencyModal.value = false;
}
function deleteFrequency(freqId) {
if (confirm(`Are you sure you want to delete frequency option with ID: ${freqId}?`)) {
adminFrequencies.value = adminFrequencies.value.filter(f => f.id !== freqId);
}
}
// --- Admin: Global Quiet Hours ---
const adminGlobalQuietHours = ref({
enabled: false,
startTime: '22:00',
endTime: '07:00',
allowUserOverride: true,
});
// --- Admin: User Preference Audit ---
const userAuditSearchQuery = ref('');
const searchedUser = ref(null); // Will hold structure like: { id: 'user123', name: 'John Doe', preferences: { defaultChannel: 'email', subscriptions: { 'cat_promo': { subscribed: true, channel: 'email', frequency: 'weekly' } }, quietHours: { enabled: false, startTime: '22:00', endTime: '07:00'} } }
const isSearchingUser = ref(false);
const userPreferencesForm = ref(null); // For editing the searched user's prefs
// Mock user data - in a real app, this would come from an API
const mockUsers = [
{
id: 'user001',
name: 'Alice Wonderland',
email: 'alice@example.com',
preferences: {
defaultPreferredChannel: 'email',
subscriptions: {
'cat_promo': { subscribed: true, channel: 'email', frequency: 'freq_weekly' },
'cat_alerts': { subscribed: true, channel: 'sms', frequency: 'freq_immediate' },
'cat_updates': { subscribed: false, channel: 'email', frequency: 'freq_daily' },
'cat_newsletter': { subscribed: true, channel: 'email', frequency: 'freq_monthly' }
},
quietHours: { enabled: false, startTime: '22:00', endTime: '08:00' }
}
},
{
id: 'user002',
name: 'Bob The Builder',
email: 'bob@example.com',
preferences: {
defaultPreferredChannel: 'sms',
subscriptions: {
'cat_promo': { subscribed: false, channel: 'email', frequency: 'freq_weekly' },
'cat_alerts': { subscribed: true, channel: 'sms', frequency: 'freq_immediate' },
'cat_updates': { subscribed: true, channel: 'push', frequency: 'freq_daily' }, // Assuming push is a configured channel id
'cat_newsletter': { subscribed: false, channel: 'email', frequency: 'freq_monthly' }
},
quietHours: { enabled: true, startTime: '23:00', endTime: '07:30' }
}
}
];
function handleUserSearch() {
if (!userAuditSearchQuery.value.trim()) {
searchedUser.value = null;
userPreferencesForm.value = null;
return;
}
isSearchingUser.value = true;
setTimeout(() => { // Simulate API call
const found = mockUsers.find(u => u.id.includes(userAuditSearchQuery.value.trim()) || u.name.toLowerCase().includes(userAuditSearchQuery.value.trim().toLowerCase()) || u.email.toLowerCase().includes(userAuditSearchQuery.value.trim().toLowerCase()));
if (found) {
searchedUser.value = JSON.parse(JSON.stringify(found)); // Deep copy
// Initialize form data by ensuring all admin-defined categories are present
const prefsCopy = JSON.parse(JSON.stringify(found.preferences));
adminNotificationCategories.value.forEach(adminCat => {
if (!prefsCopy.subscriptions[adminCat.id]) {
prefsCopy.subscriptions[adminCat.id] = { subscribed: false, channel: found.preferences.defaultPreferredChannel, frequency: adminFrequencies.value.find(f=>f.isDefault)?.id || adminFrequencies.value[0]?.id };
}
});
userPreferencesForm.value = prefsCopy;
} else {
searchedUser.value = null;
userPreferencesForm.value = null;
alert('User not found.');
}
isSearchingUser.value = false;
}, 1000);
}
function saveUserPreferences() {
if (!searchedUser.value || !userPreferencesForm.value) return;
// In a real app, send userPreferencesForm.value to the backend to save.
// For this mock, update the mockUsers array or searchedUser directly.
const userIndex = mockUsers.findIndex(u => u.id === searchedUser.value.id);
if (userIndex !== -1) {
mockUsers[userIndex].preferences = JSON.parse(JSON.stringify(userPreferencesForm.value));
}
searchedUser.value.preferences = JSON.parse(JSON.stringify(userPreferencesForm.value)); // Update current view
alert(`Preferences for ${searchedUser.value.name} saved (mock).`);
}
function cancelUserEdit() {
if (searchedUser.value) {
// Re-initialize form from original searchedUser data if needed, or just clear
const prefsCopy = JSON.parse(JSON.stringify(searchedUser.value.preferences));
adminNotificationCategories.value.forEach(adminCat => {
if (!prefsCopy.subscriptions[adminCat.id]) {
prefsCopy.subscriptions[adminCat.id] = { subscribed: false, channel: searchedUser.value.preferences.defaultPreferredChannel, frequency: adminFrequencies.value.find(f=>f.isDefault)?.id || adminFrequencies.value[0]?.id };
}
});
userPreferencesForm.value = prefsCopy;
} else {
userPreferencesForm.value = null;
}
}
// --- Admin: Bulk Operations ---
const handleAdminImport = () => {
alert('Admin bulk import initiated (not implemented).');
};
const handleAdminExport = () => {
alert('Admin bulk export initiated (not implemented).');
};
const tabs = [
{ key: 'categories', label: 'Notification Categories' },
{ key: 'channels', label: 'Channels & Governance' },
{ key: 'frequencies', label: 'Frequency Options' },
{ key: 'globalQuietHours', label: 'Global Quiet Hours' },
{ key: 'userAudit', label: 'User Preference Audit' },
{ key: 'bulkOps', label: 'Bulk Operations' },
];
const getChannelName = (channelId) => {
const channel = adminChannels.value.find(c => c.id === channelId);
return channel ? channel.name : channelId;
};
const getFrequencyLabel = (frequencyId) => {
const freq = adminFrequencies.value.find(f => f.id === frequencyId);
return freq ? freq.label : frequencyId;
};
</script>
<template> <template>
<div></div> <div>
<LayoutsBreadcrumb />
<rs-card>
<template #header>
<div class="text-xl font-semibold text-gray-900">
Admin: User Preferences & System Settings
</div>
</template>
<template #body>
<div class="p-4 md:p-6">
<!-- Tab Navigation -->
<div class="mb-6 border-b border-gray-200">
<nav class="-mb-px flex space-x-4 overflow-x-auto" aria-label="Tabs">
<button
v-for="tab in tabs"
:key="tab.key"
@click="currentTab = tab.key; searchedUser = null; userPreferencesForm = null; userAuditSearchQuery = ''"
:class="[
currentTab === tab.key
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
'whitespace-nowrap py-4 px-3 border-b-2 font-medium text-sm transition-colors duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50'
]"
:aria-current="currentTab === tab.key ? 'page' : undefined"
>
{{ tab.label }}
</button>
</nav>
</div>
<!-- Tab Content -->
<div :key="currentTab">
<!-- == Section: Notification Categories (Admin CRUD) == -->
<section v-if="currentTab === 'categories'" aria-labelledby="categories-heading">
<div class="flex justify-between items-center mb-4">
<div>
<h2 id="categories-heading" class="text-lg font-semibold text-gray-800">Manage Notification Categories</h2>
<p class="text-sm text-gray-600">Define categories users can subscribe to. Inactive categories are hidden from users.</p>
</div>
<button @click="openAddCategoryModal" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Add New Category
</button>
</div>
<div class="overflow-x-auto shadow border-b border-gray-200 sm:rounded-lg">
<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">Description</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Default Subscribed</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-for="category in adminNotificationCategories" :key="category.id">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ category.name }}</td>
<td class="px-6 py-4 whitespace-normal text-sm text-gray-500 max-w-xs truncate" :title="category.description">{{ category.description }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ category.defaultSubscribed ? 'Yes' : 'No' }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="['px-2 inline-flex text-xs leading-5 font-semibold rounded-full', category.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800']">
{{ category.isActive ? 'Active' : 'Inactive' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
<button @click="openEditCategoryModal(category)" class="text-indigo-600 hover:text-indigo-900">Edit</button>
<button @click="toggleCategoryStatus(category)" :class="[category.isActive ? 'text-red-600 hover:text-red-900' : 'text-green-600 hover:text-green-900']">
{{ category.isActive ? 'Deactivate' : 'Activate' }}
</button>
</td>
</tr>
<tr v-if="adminNotificationCategories.length === 0">
<td colspan="5" class="px-6 py-4 text-center text-sm text-gray-500">No categories defined yet.</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- == Section: Channels & Governance (Admin) == -->
<section v-if="currentTab === 'channels'" aria-labelledby="channels-heading">
<h2 id="channels-heading" class="text-lg font-semibold text-gray-800 mb-3">Manage Channels & Governance</h2>
<p class="text-sm text-gray-600 mb-4">Enable/disable communication channels and set global rules.</p>
<div class="space-y-6">
<div v-for="channel in adminChannels" :key="channel.id" class="p-4 border rounded-lg shadow-sm">
<div class="flex items-center justify-between">
<h3 class="text-md font-medium text-gray-900">{{ channel.name }}</h3>
<label :for="`channel-enabled-${channel.id}`" class="flex items-center cursor-pointer">
<span class="mr-2 text-sm text-gray-700">{{ channel.isEnabled ? 'Enabled' : 'Disabled' }}</span>
<div class="relative">
<input type="checkbox" :id="`channel-enabled-${channel.id}`" class="sr-only peer" v-model="channel.isEnabled">
<div class="w-10 h-4 bg-gray-300 rounded-full shadow-inner peer-checked:bg-blue-500 transition-colors"></div>
<div class="absolute left-0 top-[-4px] w-6 h-6 bg-white border-2 border-gray-300 rounded-full shadow transform peer-checked:translate-x-full peer-checked:border-blue-500 transition-transform"></div>
</div>
</label>
</div>
<div v-if="channel.isEnabled" class="mt-4 pt-4 border-t">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label :for="`channel-cap-${channel.id}`" class="block text-sm font-medium text-gray-700">Default Frequency Cap</label>
<input type="text" :id="`channel-cap-${channel.id}`" v-model="channel.defaultFrequencyCap" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder="e.g., 5 per day">
</div>
<div>
<label :for="`channel-types-${channel.id}`" class="block text-sm font-medium text-gray-700">Supported Message Types (Category IDs)</label>
<input type="text" :id="`channel-types-${channel.id}`" :value="channel.supportedMessageTypes.join(', ')" @change="channel.supportedMessageTypes = $event.target.value.split(',').map(s => s.trim()).filter(Boolean)" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" placeholder="e.g., cat_alerts, cat_updates">
<p class="text-xs text-gray-500 mt-1">Comma-separated category IDs that can use this channel.</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- == Section: Frequency Options (Admin) == -->
<section v-if="currentTab === 'frequencies'" aria-labelledby="frequencies-heading">
<div class="flex justify-between items-center mb-4">
<div>
<h2 id="frequencies-heading" class="text-lg font-semibold text-gray-800">Manage Frequency Options</h2>
<p class="text-sm text-gray-600">Define frequency choices available to users or for system defaults.</p>
</div>
<button @click="openAddFrequencyModal" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Add New Frequency
</button>
</div>
<div class="overflow-x-auto shadow border-b border-gray-200 sm:rounded-lg">
<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">Label</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Value (System ID)</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">User Selectable</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Default for New Users</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-for="freq in adminFrequencies" :key="freq.id">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ freq.label }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ freq.value }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ freq.isUserSelectable ? 'Yes' : 'No' }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ freq.isDefault ? 'Yes' : 'No' }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
<button @click="openEditFrequencyModal(freq)" class="text-indigo-600 hover:text-indigo-900">Edit</button>
<button @click="deleteFrequency(freq.id)" class="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
<tr v-if="adminFrequencies.length === 0">
<td colspan="5" class="px-6 py-4 text-center text-sm text-gray-500">No frequency options defined yet.</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- == Section: Global Quiet Hours (Admin) == -->
<section v-if="currentTab === 'globalQuietHours'" aria-labelledby="globalqh-heading">
<h2 id="globalqh-heading" class="text-lg font-semibold text-gray-800 mb-3">Configure Global Quiet Hours</h2>
<p class="text-sm text-gray-600 mb-4">Set system-wide default "Do Not Disturb" periods. These can potentially be overridden by users if allowed.</p>
<div class="space-y-4 max-w-lg p-4 border rounded-lg shadow-sm">
<div class="flex items-center">
<input id="admin-qh-enabled" type="checkbox" class="form-checkbox h-5 w-5 text-blue-600 rounded focus:ring-blue-500" v-model="adminGlobalQuietHours.enabled">
<label for="admin-qh-enabled" class="ml-3 block text-sm font-medium text-gray-700 cursor-pointer">Enable Global Quiet Hours</label>
</div>
<div v-if="adminGlobalQuietHours.enabled" class="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-3 mt-3">
<div>
<label for="admin-qh-start" class="block text-sm font-medium text-gray-700 mb-1">Start Time:</label>
<input type="time" id="admin-qh-start" class="form-input mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" v-model="adminGlobalQuietHours.startTime">
</div>
<div>
<label for="admin-qh-end" class="block text-sm font-medium text-gray-700 mb-1">End Time:</label>
<input type="time" id="admin-qh-end" class="form-input mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" v-model="adminGlobalQuietHours.endTime">
</div>
<div class="sm:col-span-2 flex items-center">
<input id="admin-qh-override" type="checkbox" class="form-checkbox h-5 w-5 text-blue-600 rounded focus:ring-blue-500" v-model="adminGlobalQuietHours.allowUserOverride">
<label for="admin-qh-override" class="ml-3 block text-sm font-medium text-gray-700 cursor-pointer">Allow users to override global quiet hours</label>
</div>
</div>
<button @click="alert('Save Global Quiet Hours clicked (mock)')" class="mt-4 px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">Save Global Quiet Hours</button>
</div>
</section>
<!-- == Section: User Preference Audit (Admin) == -->
<section v-if="currentTab === 'userAudit'" aria-labelledby="useraudit-heading">
<h2 id="useraudit-heading" class="text-lg font-semibold text-gray-800 mb-3">User Preference Audit & Management</h2>
<p class="text-sm text-gray-600 mb-4">Search for a user by ID, name, or email to view or modify their notification preferences.</p>
<div class="flex space-x-3 mb-6 max-w-xl">
<input type="text" v-model="userAuditSearchQuery" placeholder="Enter User ID, Name, or Email" class="form-input flex-grow block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<button @click="handleUserSearch" :disabled="isSearchingUser || !userAuditSearchQuery.trim()" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50">
{{ isSearchingUser ? 'Searching...' : 'Search User' }}
</button>
</div>
<div v-if="isSearchingUser" class="text-center py-6">
<p class="text-gray-500">Loading user data...</p> <!-- Add a spinner later -->
</div>
<div v-if="!isSearchingUser && searchedUser && userPreferencesForm" class="mt-6 p-6 border rounded-lg shadow-lg">
<h3 class="text-xl font-semibold text-gray-800 mb-2">Editing Preferences for: <span class="font-normal">{{ searchedUser.name }} ({{ searchedUser.id }})</span></h3>
<p class="text-sm text-gray-500 mb-6">Email: {{ searchedUser.email }}</p>
<form @submit.prevent="saveUserPreferences">
<div class="space-y-8">
<!-- Default Preferred Channel for User -->
<div>
<label for="userDefaultChannel" class="block text-sm font-medium text-gray-700 mb-1">User's Default Notification Channel</label>
<select id="userDefaultChannel" v-model="userPreferencesForm.defaultPreferredChannel" class="form-select mt-1 block w-full md:w-1/2 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<option v-for="channel in adminChannels.filter(c => c.isEnabled)" :key="channel.id" :value="channel.id">{{ channel.name }}</option>
<option value="none">None (Mute All)</option>
</select>
</div>
<!-- User Subscriptions to Categories -->
<div>
<h4 class="text-md font-semibold text-gray-700 mb-3">Category Subscriptions & Overrides</h4>
<div class="space-y-6">
<div v-for="adminCat in adminNotificationCategories.filter(ac => ac.isActive)" :key="adminCat.id" class="p-4 border rounded-md bg-gray-50">
<div class="flex items-start justify-between mb-3">
<div>
<h5 class="font-medium text-gray-800">{{ adminCat.name }}</h5>
<p class="text-xs text-gray-500">{{ adminCat.description }}</p>
</div>
<input :id="`user-cat-sub-${adminCat.id}`" type="checkbox" v-model="userPreferencesForm.subscriptions[adminCat.id].subscribed" class="form-checkbox h-5 w-5 text-blue-600 rounded focus:ring-blue-500 cursor-pointer">
</div>
<div v-if="userPreferencesForm.subscriptions[adminCat.id].subscribed" class="mt-3 pt-3 border-t grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-3">
<div>
<label :for="`user-cat-channel-${adminCat.id}`" class="block text-xs font-medium text-gray-600 mb-0.5">Channel Override:</label>
<select :id="`user-cat-channel-${adminCat.id}`" v-model="userPreferencesForm.subscriptions[adminCat.id].channel" class="form-select mt-0.5 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-xs">
<option v-for="channel in adminChannels.filter(c => c.isEnabled && c.supportedMessageTypes.includes(adminCat.id))" :key="channel.id" :value="channel.id">{{ channel.name }}</option>
<option :value="userPreferencesForm.defaultPreferredChannel">(User Default: {{ getChannelName(userPreferencesForm.defaultPreferredChannel) }})</option>
<option value="none">None (Mute This Category)</option>
</select>
</div>
<div>
<label :for="`user-cat-freq-${adminCat.id}`" class="block text-xs font-medium text-gray-600 mb-0.5">Frequency Override:</label>
<select :id="`user-cat-freq-${adminCat.id}`" v-model="userPreferencesForm.subscriptions[adminCat.id].frequency" class="form-select mt-0.5 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-xs">
<option v-for="freq in adminFrequencies.filter(f => f.isUserSelectable)" :key="freq.id" :value="freq.id">{{ freq.label }}</option>
</select>
</div>
</div>
</div>
</div>
</div>
<!-- User Quiet Hours -->
<div>
<h4 class="text-md font-semibold text-gray-700 mb-3">User Quiet Hours Override</h4>
<div class="space-y-3 p-4 border rounded-md bg-gray-50 max-w-md">
<div class="flex items-center">
<input id="user-qh-enabled" type="checkbox" v-model="userPreferencesForm.quietHours.enabled" class="form-checkbox h-5 w-5 text-blue-600 rounded focus:ring-blue-500">
<label for="user-qh-enabled" class="ml-3 block text-sm font-medium text-gray-700 cursor-pointer">Enable Quiet Hours for this User</label>
</div>
<div v-if="userPreferencesForm.quietHours.enabled" class="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-2 mt-2">
<div>
<label for="user-qh-start" class="block text-xs font-medium text-gray-600 mb-0.5">Start Time:</label>
<input type="time" id="user-qh-start" v-model="userPreferencesForm.quietHours.startTime" class="form-input mt-0.5 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="user-qh-end" class="block text-xs font-medium text-gray-600 mb-0.5">End Time:</label>
<input type="time" id="user-qh-end" v-model="userPreferencesForm.quietHours.endTime" class="form-input mt-0.5 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
</div>
</div>
</div>
</div>
<!-- Save/Cancel User Prefs -->
<div class="mt-8 pt-5 border-t">
<div class="flex justify-end space-x-3">
<button type="button" @click="cancelUserEdit" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Reset / Cancel Edit
</button>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-green-600 border border-transparent rounded-md shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
Save Changes for {{ searchedUser.name }}
</button>
</div>
</div>
</div>
</form>
</div>
<div v-if="!isSearchingUser && !searchedUser && userAuditSearchQuery" class="text-center py-6">
<p class="text-gray-500">No user found matching "{{ userAuditSearchQuery }}". Try a different search term.</p>
</div>
<div v-if="!isSearchingUser && !searchedUser && !userAuditSearchQuery" class="text-center py-6">
<p class="text-gray-500">Enter a user ID, name, or email to search.</p>
</div>
</section>
<section v-if="currentTab === 'bulkOps'" aria-labelledby="bulkops-heading">
<h2 id="bulkops-heading" class="text-lg font-semibold text-gray-800 mb-3">Bulk User Preference Operations</h2>
<p class="text-sm text-gray-600 mb-4">Import or export user preference data for backup, migration, or system-wide updates.</p>
<div class="flex flex-col sm:flex-row space-y-3 sm:space-y-0 sm:space-x-3 mt-4">
<button @click="handleAdminImport" class="w-full sm:w-auto px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition ease-in-out duration-150">
Import User Preferences (Bulk)
</button>
<button @click="handleAdminExport" class="w-full sm:w-auto 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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition ease-in-out duration-150">
Export User Preferences (Bulk)
</button>
</div>
</section>
</div>
</div>
</template>
</rs-card>
<!-- Modal for Add/Edit Notification Category -->
<div v-if="showCategoryModal" class="fixed inset-0 z-50 overflow-y-auto bg-gray-600 bg-opacity-75 flex items-center justify-center p-4">
<div class="relative bg-white rounded-lg shadow-xl w-full max-w-lg mx-auto my-8 max-h-[90vh] overflow-y-auto">
<form @submit.prevent="saveCategory" class="p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
{{ editingCategory ? 'Edit' : 'Add New' }} Notification Category
</h3>
<div class="space-y-4">
<div>
<label for="catName" class="block text-sm font-medium text-gray-700">Category Name</label>
<input type="text" v-model="categoryForm.name" id="catName" 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="catDesc" class="block text-sm font-medium text-gray-700">Description</label>
<textarea v-model="categoryForm.description" id="catDesc" rows="3" 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="flex items-start">
<div class="flex items-center h-5">
<input id="catDefaultSubscribed" v-model="categoryForm.defaultSubscribed" type="checkbox" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded">
</div>
<div class="ml-3 text-sm">
<label for="catDefaultSubscribed" class="font-medium text-gray-700">Default Subscribed</label>
<p class="text-gray-500">Users will be subscribed to this category by default.</p>
</div>
</div>
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="catIsActive" v-model="categoryForm.isActive" type="checkbox" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded">
</div>
<div class="ml-3 text-sm">
<label for="catIsActive" class="font-medium text-gray-700">Active</label>
<p class="text-gray-500">Inactive categories are not visible or configurable by users.</p>
</div>
</div>
</div>
<div class="mt-6 flex justify-end space-x-3 sticky bottom-0 bg-white py-4 px-6 -mx-6 -mb-6 border-t border-gray-200 rounded-b-lg">
<button type="button" @click="showCategoryModal = false" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Cancel
</button>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
{{ editingCategory ? 'Save Changes' : 'Create Category' }}
</button>
</div>
</form>
</div>
</div>
<!-- Modal for Add/Edit Frequency Option -->
<div v-if="showFrequencyModal" class="fixed inset-0 z-50 overflow-y-auto bg-gray-600 bg-opacity-75 flex items-center justify-center p-4">
<div class="relative bg-white rounded-lg shadow-xl w-full max-w-lg mx-auto my-8 max-h-[90vh] overflow-y-auto">
<form @submit.prevent="saveFrequency" class="p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
{{ editingFrequency ? 'Edit' : 'Add New' }} Frequency Option
</h3>
<div class="space-y-4">
<div>
<label for="freqLabel" class="block text-sm font-medium text-gray-700">Label (User-facing)</label>
<input type="text" v-model="frequencyForm.label" id="freqLabel" 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="freqValue" class="block text-sm font-medium text-gray-700">Value (System ID)</label>
<input type="text" v-model="frequencyForm.value" id="freqValue" required :disabled="editingFrequency !== null" placeholder="e.g., immediate, daily_digest" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm disabled:bg-gray-100">
<p v-if="editingFrequency" class="text-xs text-gray-500 mt-1">System value cannot be changed after creation.</p>
</div>
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="freqUserSelectable" v-model="frequencyForm.isUserSelectable" type="checkbox" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded">
</div>
<div class="ml-3 text-sm">
<label for="freqUserSelectable" class="font-medium text-gray-700">User Selectable</label>
<p class="text-gray-500">Can users choose this frequency option for their preferences?</p>
</div>
</div>
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="freqIsDefault" v-model="frequencyForm.isDefault" type="checkbox" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded">
</div>
<div class="ml-3 text-sm">
<label for="freqIsDefault" class="font-medium text-gray-700">Default for New Users</label>
<p class="text-gray-500">Is this a default frequency for new users or new subscriptions?</p>
</div>
</div>
</div>
<div class="mt-6 flex justify-end space-x-3 sticky bottom-0 bg-white py-4 px-6 -mx-6 -mb-6 border-t border-gray-200 rounded-b-lg">
<button type="button" @click="showFrequencyModal = false" class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Cancel
</button>
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
{{ editingFrequency ? 'Save Changes' : 'Create Frequency' }}
</button>
</div>
</form>
</div>
</div>
</div>
</template> </template>
<script setup></script> <style scoped>
/* Using Tailwind utility classes. */
<style lang="scss" scoped></style> /* Ensure @tailwindcss/forms plugin is installed for nice form styling. */
.form-checkbox:focus, .form-input:focus, .form-select:focus, .form-textarea:focus {
/* You might want to ensure focus rings are consistent if not using the plugin */
/* Example: ring-2 ring-offset-2 ring-indigo-500 */
}
</style>

View File

@ -1,7 +1,314 @@
<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> <template>
<div></div> <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> </template>
<script setup></script> <style scoped>
/* Scoped styles if needed */
<style lang="scss" scoped></style> .max-h-\[90vh\] {
max-height: 90vh;
}
</style>