642 lines
42 KiB
Vue

<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>
<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>
<style scoped>
/* Using Tailwind utility classes. */
/* 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>