Implement batch processing and queue management features, including new pages for batch scheduling, queue monitoring, rate limiting, and retry logic. Update existing components for improved camel case conversion and remove unused navigation items.

This commit is contained in:
Haqeem Solehan 2025-05-30 12:01:32 +08:00
parent b181f99237
commit ab9a8ae8c4
8 changed files with 3655 additions and 17 deletions

View File

@ -116,11 +116,14 @@ const spacingCharactertoCamelCase = (array) => {
let result = [];
array.forEach((element) => {
if (element.charAt(0) == element.charAt(0).toUpperCase()) {
// Handle both string elements and object elements with key property
const stringElement = typeof element === 'string' ? element : element.key;
if (stringElement && stringElement.charAt(0) == stringElement.charAt(0).toUpperCase()) {
// Camelcase the string and remove spacing
// and if there is () in the string, do Uppercase inside the () and dont spacing it
let camelCase = element
let camelCase = stringElement
.replace(/([A-Z])/g, " $1")
.replace(/^./, (str) => {
return str.toUpperCase();
@ -133,7 +136,7 @@ const spacingCharactertoCamelCase = (array) => {
result.push(resultCamelCase);
} else {
result.push(element);
result.push(stringElement);
}
});

View File

@ -10,18 +10,6 @@ export default [
child: [],
meta: {},
},
{
title: "Notes",
path: "/notes",
icon: "",
child: [],
},
{
title: "Metabase",
path: "/metabase",
icon: "",
child: [],
},
],
meta: {},
},

View File

@ -0,0 +1,706 @@
<template>
<div>
<LayoutsBreadcrumb />
<!-- Header Section -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-batch-prediction"></Icon>
<h1 class="text-xl font-bold text-primary">Batch Processing</h1>
</div>
</template>
<template #body>
<p class="text-gray-600">
Schedule or trigger the processing of large groups of messages in one go.
Ideal for sending newsletters, campaigns, or system-wide alerts to thousands/millions of users.
</p>
</template>
</rs-card>
<!-- Quick Actions -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<rs-card class="cursor-pointer transition-all duration-300 hover:shadow-lg" @click="showCreateBatchModal = true">
<div class="pt-5 pb-3 px-5 flex items-center gap-4">
<div class="p-4 flex justify-center items-center bg-blue-100 rounded-2xl">
<Icon class="text-blue-600 text-2xl" name="ic:outline-add"></Icon>
</div>
<div class="flex-1">
<span class="block font-bold text-lg text-blue-600">Create New Batch</span>
<span class="text-sm text-gray-600">Start a new batch processing job</span>
</div>
</div>
</rs-card>
<rs-card class="cursor-pointer transition-all duration-300 hover:shadow-lg" @click="showScheduleModal = true">
<div class="pt-5 pb-3 px-5 flex items-center gap-4">
<div class="p-4 flex justify-center items-center bg-green-100 rounded-2xl">
<Icon class="text-green-600 text-2xl" name="ic:outline-schedule"></Icon>
</div>
<div class="flex-1">
<span class="block font-bold text-lg text-green-600">Schedule Batch</span>
<span class="text-sm text-gray-600">Schedule for later execution</span>
</div>
</div>
</rs-card>
<rs-card class="cursor-pointer transition-all duration-300 hover:shadow-lg" @click="showTemplatesModal = true">
<div class="pt-5 pb-3 px-5 flex items-center gap-4">
<div class="p-4 flex justify-center items-center bg-purple-100 rounded-2xl">
<Icon class="text-purple-600 text-2xl" name="ic:outline-library-books"></Icon>
</div>
<div class="flex-1">
<span class="block font-bold text-lg text-purple-600">Batch Templates</span>
<span class="text-sm text-gray-600">Use predefined batch configurations</span>
</div>
</div>
</rs-card>
</div>
<!-- Batch Statistics -->
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
<rs-card
v-for="(stat, index) in batchStats"
:key="index"
class="transition-all duration-300 hover:shadow-lg"
>
<div class="pt-5 pb-3 px-5 flex items-center gap-4">
<div
class="p-4 flex justify-center items-center rounded-2xl"
:class="stat.bgColor"
>
<Icon class="text-2xl" :class="stat.iconColor" :name="stat.icon"></Icon>
</div>
<div class="flex-1 truncate">
<span class="block font-bold text-xl leading-tight" :class="stat.textColor">
{{ stat.value }}
</span>
<span class="text-sm font-medium text-gray-600">
{{ stat.title }}
</span>
</div>
</div>
</rs-card>
</div>
<!-- Active Batches -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-primary">Active Batches</h3>
<rs-button variant="outline" size="sm" @click="refreshBatches">
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
Refresh
</rs-button>
</div>
</template>
<template #body>
<div class="space-y-4">
<div
v-for="(batch, index) in activeBatches"
:key="index"
class="border border-gray-200 rounded-lg p-4"
>
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<div
class="w-3 h-3 rounded-full mr-3"
:class="{
'bg-blue-500': batch.status === 'pending',
'bg-yellow-500': batch.status === 'processing',
'bg-green-500': batch.status === 'completed',
'bg-red-500': batch.status === 'failed',
'bg-gray-500': batch.status === 'paused'
}"
></div>
<div>
<h4 class="font-semibold">{{ batch.name }}</h4>
<p class="text-sm text-gray-600">{{ batch.description }}</p>
</div>
</div>
<div class="flex items-center gap-2">
<rs-badge :variant="getBatchStatusVariant(batch.status)">
{{ batch.status }}
</rs-badge>
<rs-dropdown>
<template #trigger>
<rs-button variant="outline" size="sm">
<Icon name="ic:outline-more-vert"></Icon>
</rs-button>
</template>
<rs-dropdown-item @click="viewBatchDetails(batch)">
<Icon class="mr-2" name="ic:outline-visibility"></Icon>
View Details
</rs-dropdown-item>
<rs-dropdown-item v-if="batch.status === 'processing'" @click="pauseBatch(batch)">
<Icon class="mr-2" name="ic:outline-pause"></Icon>
Pause
</rs-dropdown-item>
<rs-dropdown-item v-if="batch.status === 'paused'" @click="resumeBatch(batch)">
<Icon class="mr-2" name="ic:outline-play-arrow"></Icon>
Resume
</rs-dropdown-item>
<rs-dropdown-item v-if="['pending', 'paused'].includes(batch.status)" @click="cancelBatch(batch)" class="text-red-600">
<Icon class="mr-2" name="ic:outline-cancel"></Icon>
Cancel
</rs-dropdown-item>
</rs-dropdown>
</div>
</div>
<!-- Progress Bar -->
<div class="mb-3">
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>Progress: {{ batch.processed }}/{{ batch.total }}</span>
<span>{{ Math.round((batch.processed / batch.total) * 100) }}%</span>
</div>
<rs-progress-bar
:value="(batch.processed / batch.total) * 100"
:variant="batch.status === 'failed' ? 'danger' : 'primary'"
/>
</div>
<!-- Batch Info -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<span class="text-gray-600">Type:</span>
<span class="ml-1 font-medium">{{ batch.type }}</span>
</div>
<div>
<span class="text-gray-600">Chunk Size:</span>
<span class="ml-1 font-medium">{{ batch.chunkSize }}</span>
</div>
<div>
<span class="text-gray-600">Started:</span>
<span class="ml-1 font-medium">{{ batch.startedAt }}</span>
</div>
<div>
<span class="text-gray-600">ETA:</span>
<span class="ml-1 font-medium">{{ batch.eta }}</span>
</div>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Batch History -->
<rs-card>
<template #header>
<h3 class="text-lg font-semibold text-primary">Batch History</h3>
</template>
<template #body>
<rs-table
:field="historyTableFields"
:data="batchHistory"
:options="{ striped: true, hover: true }"
:optionsAdvanced="{ sortable: true, filterable: true }"
advanced
/>
</template>
</rs-card>
<!-- Create Batch Modal -->
<rs-modal v-model="showCreateBatchModal" size="lg">
<template #header>
<h3 class="text-lg font-semibold">Create New Batch</h3>
</template>
<template #body>
<form @submit.prevent="createBatch" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Batch Name</label>
<input
v-model="newBatch.name"
type="text"
class="w-full p-2 border border-gray-300 rounded-md"
placeholder="Enter batch name"
required
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Message Type</label>
<select v-model="newBatch.type" class="w-full p-2 border border-gray-300 rounded-md" required>
<option value="">Select type</option>
<option value="email">Email</option>
<option value="sms">SMS</option>
<option value="push">Push Notification</option>
<option value="webhook">Webhook</option>
</select>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Description</label>
<textarea
v-model="newBatch.description"
class="w-full p-2 border border-gray-300 rounded-md"
rows="3"
placeholder="Describe this batch processing job"
></textarea>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Chunk Size</label>
<input
v-model.number="newBatch.chunkSize"
type="number"
class="w-full p-2 border border-gray-300 rounded-md"
placeholder="500"
min="1"
max="10000"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Priority</label>
<select v-model="newBatch.priority" class="w-full p-2 border border-gray-300 rounded-md">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Delay Between Chunks (ms)</label>
<input
v-model.number="newBatch.delay"
type="number"
class="w-full p-2 border border-gray-300 rounded-md"
placeholder="1000"
min="0"
/>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Target Criteria</label>
<textarea
v-model="newBatch.criteria"
class="w-full p-2 border border-gray-300 rounded-md"
rows="3"
placeholder="JSON criteria for selecting recipients (e.g., user type, timezone, preferences)"
></textarea>
</div>
</form>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<rs-button variant="outline" @click="showCreateBatchModal = false">Cancel</rs-button>
<rs-button @click="createBatch" variant="primary">Create Batch</rs-button>
</div>
</template>
</rs-modal>
<!-- Schedule Modal -->
<rs-modal v-model="showScheduleModal" size="md">
<template #header>
<h3 class="text-lg font-semibold">Schedule Batch</h3>
</template>
<template #body>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Select Batch Template</label>
<select v-model="scheduleBatch.templateId" class="w-full p-2 border border-gray-300 rounded-md">
<option value="">Choose a template</option>
<option value="newsletter">Newsletter Campaign</option>
<option value="birthday">Birthday Reminders</option>
<option value="system">System Notifications</option>
</select>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Schedule Date</label>
<input
v-model="scheduleBatch.date"
type="date"
class="w-full p-2 border border-gray-300 rounded-md"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Schedule Time</label>
<input
v-model="scheduleBatch.time"
type="time"
class="w-full p-2 border border-gray-300 rounded-md"
/>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Timezone</label>
<select v-model="scheduleBatch.timezone" class="w-full p-2 border border-gray-300 rounded-md">
<option value="UTC">UTC</option>
<option value="Asia/Kuala_Lumpur">Asia/Kuala_Lumpur</option>
<option value="America/New_York">America/New_York</option>
<option value="Europe/London">Europe/London</option>
</select>
</div>
<div class="flex items-center">
<input
v-model="scheduleBatch.recurring"
type="checkbox"
class="mr-2"
/>
<label class="text-sm font-medium text-gray-700">Recurring batch</label>
</div>
<div v-if="scheduleBatch.recurring">
<label class="block text-sm font-medium text-gray-700 mb-2">Recurrence Pattern</label>
<select v-model="scheduleBatch.recurrencePattern" class="w-full p-2 border border-gray-300 rounded-md">
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
</select>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<rs-button variant="outline" @click="showScheduleModal = false">Cancel</rs-button>
<rs-button @click="scheduleNewBatch" variant="primary">Schedule</rs-button>
</div>
</template>
</rs-modal>
<!-- Templates Modal -->
<rs-modal v-model="showTemplatesModal" size="lg">
<template #header>
<h3 class="text-lg font-semibold">Batch Templates</h3>
</template>
<template #body>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div
v-for="(template, index) in batchTemplates"
:key="index"
class="border border-gray-200 rounded-lg p-4 cursor-pointer hover:border-primary transition-colors"
@click="useBatchTemplate(template)"
>
<div class="flex items-center mb-3">
<Icon class="mr-2 text-primary" :name="template.icon"></Icon>
<h4 class="font-semibold">{{ template.name }}</h4>
</div>
<p class="text-sm text-gray-600 mb-3">{{ template.description }}</p>
<div class="space-y-1 text-xs text-gray-500">
<div>Type: {{ template.type }}</div>
<div>Chunk Size: {{ template.chunkSize }}</div>
<div>Priority: {{ template.priority }}</div>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end">
<rs-button variant="outline" @click="showTemplatesModal = false">Close</rs-button>
</div>
</template>
</rs-modal>
</div>
</template>
<script setup>
definePageMeta({
title: "Batch Processing",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Notification",
path: "/notification",
},
{
name: "Queue & Scheduler",
path: "/notification/queue-scheduler",
},
{
name: "Batch Processing",
path: "/notification/queue-scheduler/batch",
},
],
});
// Reactive data
const showCreateBatchModal = ref(false);
const showScheduleModal = ref(false);
const showTemplatesModal = ref(false);
// New batch form
const newBatch = ref({
name: '',
type: '',
description: '',
chunkSize: 500,
priority: 'medium',
delay: 1000,
criteria: ''
});
// Schedule batch form
const scheduleBatch = ref({
templateId: '',
date: '',
time: '',
timezone: 'UTC',
recurring: false,
recurrencePattern: 'daily'
});
// Batch statistics
const batchStats = ref([
{
title: "Active Batches",
value: "12",
icon: "ic:outline-play-circle",
bgColor: "bg-blue-100",
iconColor: "text-blue-600",
textColor: "text-blue-600"
},
{
title: "Scheduled",
value: "8",
icon: "ic:outline-schedule",
bgColor: "bg-green-100",
iconColor: "text-green-600",
textColor: "text-green-600"
},
{
title: "Completed Today",
value: "45",
icon: "ic:outline-check-circle",
bgColor: "bg-purple-100",
iconColor: "text-purple-600",
textColor: "text-purple-600"
},
{
title: "Total Messages",
value: "2.3M",
icon: "ic:outline-email",
bgColor: "bg-orange-100",
iconColor: "text-orange-600",
textColor: "text-orange-600"
}
]);
// Active batches
const activeBatches = ref([
{
id: 'batch_001',
name: 'Newsletter Campaign Q1',
description: 'Quarterly newsletter to all subscribers',
status: 'processing',
type: 'email',
processed: 15000,
total: 50000,
chunkSize: 500,
startedAt: '2024-01-15 09:00:00',
eta: '2 hours'
},
{
id: 'batch_002',
name: 'Birthday Reminders',
description: 'Daily birthday notifications',
status: 'completed',
type: 'push',
processed: 1200,
total: 1200,
chunkSize: 100,
startedAt: '2024-01-15 08:00:00',
eta: 'Completed'
},
{
id: 'batch_003',
name: 'SMS OTP Batch',
description: 'OTP messages for verification',
status: 'paused',
type: 'sms',
processed: 800,
total: 2000,
chunkSize: 200,
startedAt: '2024-01-15 10:30:00',
eta: 'Paused'
}
]);
// Batch history table fields
const historyTableFields = ref([
{ key: 'name', label: 'Batch Name', sortable: true },
{ key: 'type', label: 'Type', sortable: true },
{ key: 'status', label: 'Status', sortable: true },
{ key: 'total', label: 'Total Messages', sortable: true },
{ key: 'processed', label: 'Processed', sortable: true },
{ key: 'startedAt', label: 'Started', sortable: true },
{ key: 'completedAt', label: 'Completed', sortable: true },
{ key: 'duration', label: 'Duration', sortable: true }
]);
// Batch history data
const batchHistory = ref([
{
name: 'Welcome Email Series',
type: 'email',
status: 'completed',
total: 25000,
processed: 25000,
startedAt: '2024-01-14 14:00:00',
completedAt: '2024-01-14 16:30:00',
duration: '2h 30m'
},
{
name: 'Product Update Push',
type: 'push',
status: 'completed',
total: 100000,
processed: 98500,
startedAt: '2024-01-13 10:00:00',
completedAt: '2024-01-13 12:45:00',
duration: '2h 45m'
},
{
name: 'Security Alert SMS',
type: 'sms',
status: 'failed',
total: 5000,
processed: 2300,
startedAt: '2024-01-12 16:00:00',
completedAt: '2024-01-12 16:45:00',
duration: '45m'
}
]);
// Batch templates
const batchTemplates = ref([
{
name: 'Newsletter Campaign',
description: 'Standard newsletter template for marketing campaigns',
type: 'email',
chunkSize: 1000,
priority: 'medium',
icon: 'ic:outline-email'
},
{
name: 'Birthday Reminders',
description: 'Daily birthday notification template',
type: 'push',
chunkSize: 100,
priority: 'low',
icon: 'ic:outline-cake'
},
{
name: 'System Notifications',
description: 'Critical system alerts and updates',
type: 'push',
chunkSize: 500,
priority: 'high',
icon: 'ic:outline-notification-important'
},
{
name: 'SMS Verification',
description: 'OTP and verification SMS template',
type: 'sms',
chunkSize: 200,
priority: 'high',
icon: 'ic:outline-sms'
}
]);
// Methods
function getBatchStatusVariant(status) {
const variants = {
pending: 'info',
processing: 'warning',
completed: 'success',
failed: 'danger',
paused: 'secondary'
};
return variants[status] || 'default';
}
function viewBatchDetails(batch) {
// Navigate to batch details page or show detailed modal
console.log('Viewing batch details:', batch);
}
function pauseBatch(batch) {
batch.status = 'paused';
batch.eta = 'Paused';
}
function resumeBatch(batch) {
batch.status = 'processing';
batch.eta = '1.5 hours';
}
function cancelBatch(batch) {
batch.status = 'cancelled';
batch.eta = 'Cancelled';
}
function createBatch() {
// Mock batch creation
const batch = {
id: `batch_${Date.now()}`,
name: newBatch.value.name,
description: newBatch.value.description,
status: 'pending',
type: newBatch.value.type,
processed: 0,
total: 10000, // Mock total
chunkSize: newBatch.value.chunkSize,
startedAt: new Date().toLocaleString(),
eta: 'Pending'
};
activeBatches.value.unshift(batch);
showCreateBatchModal.value = false;
// Reset form
newBatch.value = {
name: '',
type: '',
description: '',
chunkSize: 500,
priority: 'medium',
delay: 1000,
criteria: ''
};
}
function scheduleNewBatch() {
// Mock batch scheduling
console.log('Scheduling batch:', scheduleBatch.value);
showScheduleModal.value = false;
// Reset form
scheduleBatch.value = {
templateId: '',
date: '',
time: '',
timezone: 'UTC',
recurring: false,
recurrencePattern: 'daily'
};
}
function useBatchTemplate(template) {
newBatch.value = {
name: template.name,
type: template.type,
description: template.description,
chunkSize: template.chunkSize,
priority: template.priority,
delay: 1000,
criteria: ''
};
showTemplatesModal.value = false;
showCreateBatchModal.value = true;
}
function refreshBatches() {
// Mock refresh
console.log('Refreshing batches...');
}
</script>
<style lang="scss" scoped></style>

View File

@ -1,7 +1,278 @@
<template>
<div></div>
<div>
<LayoutsBreadcrumb />
<!-- Header Section -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-schedule"></Icon>
<h1 class="text-xl font-bold text-primary">Queue & Scheduler</h1>
</div>
</template>
<template #body>
<p class="text-gray-600">
Controls how messages and background tasks are managed crucial for
reliability, scalability, and timing precision in any notification or
event-driven system.
</p>
</template>
</rs-card>
<!-- Quick Stats -->
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
<rs-card
v-for="(stat, index) in quickStats"
:key="index"
class="transition-all duration-300 hover:shadow-lg"
>
<div class="pt-5 pb-3 px-5 flex items-center gap-4">
<div
class="p-5 flex justify-center items-center bg-primary/20 rounded-2xl transition-all duration-300 hover:bg-primary/30"
>
<Icon class="text-primary text-3xl" :name="stat.icon"></Icon>
</div>
<div class="flex-1 truncate">
<span class="block font-bold text-2xl leading-tight text-primary">
{{ stat.value }}
</span>
<span class="text-sm font-medium text-gray-600">
{{ stat.title }}
</span>
</div>
</div>
</rs-card>
</div>
<!-- Feature Cards -->
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
<rs-card
v-for="(feature, index) in features"
:key="index"
class="transition-all duration-300 hover:shadow-lg cursor-pointer"
@click="navigateTo(feature.path)"
>
<template #header>
<div class="flex items-center">
<Icon class="mr-2 text-primary" :name="feature.icon"></Icon>
<h3 class="text-lg font-semibold text-primary">{{ feature.title }}</h3>
</div>
</template>
<template #body>
<p class="text-gray-600 mb-4">{{ feature.description }}</p>
<div class="space-y-2">
<div
v-for="(useCase, idx) in feature.useCases"
:key="idx"
class="flex items-start"
>
<Icon
class="mr-2 mt-1 text-green-500 text-sm"
name="ic:outline-check-circle"
></Icon>
<span class="text-sm text-gray-700">{{ useCase }}</span>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end">
<rs-button variant="outline" size="sm">
<Icon class="mr-1" name="ic:outline-arrow-forward"></Icon>
Open
</rs-button>
</div>
</template>
</rs-card>
</div>
<!-- Recent Activity -->
<rs-card class="mt-6">
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-history"></Icon>
<h3 class="text-lg font-semibold text-primary">Recent Queue Activity</h3>
</div>
<rs-button
variant="outline"
size="sm"
@click="navigateTo('/notification/queue-scheduler/monitor')"
>
View All
</rs-button>
</div>
</template>
<template #body>
<div class="space-y-3">
<div
v-for="(activity, index) in recentActivity"
:key="index"
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<div class="flex items-center">
<div
class="w-3 h-3 rounded-full mr-3"
:class="{
'bg-green-500': activity.status === 'completed',
'bg-yellow-500': activity.status === 'processing',
'bg-red-500': activity.status === 'failed',
'bg-blue-500': activity.status === 'pending',
}"
></div>
<div>
<p class="font-medium">{{ activity.job }}</p>
<p class="text-sm text-gray-600">{{ activity.description }}</p>
</div>
</div>
<div class="text-right">
<p class="text-sm font-medium capitalize">{{ activity.status }}</p>
<p class="text-xs text-gray-500">{{ activity.time }}</p>
</div>
</div>
</div>
</template>
</rs-card>
</div>
</template>
<script setup></script>
<script setup>
definePageMeta({
title: "Queue & Scheduler",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Notification",
path: "/notification",
},
{
name: "Queue & Scheduler",
path: "/notification/queue-scheduler",
},
],
});
// Quick stats data
const quickStats = ref([
{
title: "Active Jobs",
value: "1,247",
icon: "ic:outline-work",
},
{
title: "Completed Today",
value: "8,932",
icon: "ic:outline-check-circle",
},
{
title: "Failed Jobs",
value: "23",
icon: "ic:outline-error",
},
{
title: "Avg Processing Time",
value: "2.3s",
icon: "ic:outline-timer",
},
]);
// Feature cards data
const features = ref([
{
title: "Queue Monitor",
description:
"Displays a live overview of all queued jobs — what's currently pending, processing, succeeded, or failed.",
icon: "ic:outline-monitor",
path: "/notification/queue-scheduler/monitor",
useCases: [
"Monitor load spikes and backlogs in real-time",
"Track job status with auto-refresh",
"Filter by job type, channel, and status",
],
},
{
title: "Batch Processing",
description:
"Schedule or trigger the processing of large groups of messages in one go.",
icon: "ic:outline-batch-prediction",
path: "/notification/queue-scheduler/batch",
useCases: [
"Send newsletters and campaigns",
"Process messages in chunks (500 per batch)",
"Schedule batches for optimal timing",
],
},
{
title: "Retry & Dead Letter",
description:
"Handles failed jobs with retry logic and dead letter queue for permanent failures.",
icon: "ic:outline-refresh",
path: "/notification/queue-scheduler/retry",
useCases: [
"Auto/manual retry of failed messages",
"Store permanently failed messages",
"Debug with detailed error reasons",
],
},
{
title: "Timezone Handling",
description:
"Ensures messages are delivered at the right local time for each recipient.",
icon: "ic:outline-schedule",
path: "/notification/queue-scheduler/timezone",
useCases: [
"Schedule birthday messages at 9AM local time",
"Avoid 2AM push alerts across timezones",
"UTC + offset logic for global delivery",
],
},
{
title: "Rate Limiting",
description:
"Throttles how many messages/jobs can be processed per second/minute/hour.",
icon: "ic:outline-speed",
path: "/notification/queue-scheduler/rate-limit",
useCases: [
"Avoid hitting API limits (Twilio, SendGrid)",
"Prevent spammy behavior and blacklisting",
"Global and per-channel rate controls",
],
},
]);
// Recent activity data
const recentActivity = ref([
{
job: "Email Campaign #1247",
description: "Newsletter to 50,000 subscribers",
status: "completed",
time: "2 minutes ago",
},
{
job: "SMS Batch #892",
description: "OTP messages to 1,200 users",
status: "processing",
time: "5 minutes ago",
},
{
job: "Push Notification #445",
description: "App update notification",
status: "failed",
time: "8 minutes ago",
},
{
job: "Webhook Delivery #223",
description: "Order confirmation webhooks",
status: "pending",
time: "12 minutes ago",
},
{
job: "Birthday Reminders",
description: "Daily birthday notifications",
status: "completed",
time: "1 hour ago",
},
]);
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,503 @@
<template>
<div>
<LayoutsBreadcrumb />
<!-- Header Section -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-monitor"></Icon>
<h1 class="text-xl font-bold text-primary">Queue Monitor</h1>
</div>
<div class="flex items-center gap-3">
<div class="flex items-center">
<div class="w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse"></div>
<span class="text-sm text-gray-600">Live Updates</span>
</div>
<rs-button variant="outline" size="sm" @click="toggleAutoRefresh">
<Icon
class="mr-1"
:name="autoRefresh ? 'ic:outline-pause' : 'ic:outline-play-arrow'"
></Icon>
{{ autoRefresh ? "Pause" : "Resume" }}
</rs-button>
</div>
</div>
</template>
<template #body>
<p class="text-gray-600">
Live overview of all queued jobs monitor what's currently pending, processing,
succeeded, or failed.
</p>
</template>
</rs-card>
<!-- Stats Overview -->
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-5 gap-6 mb-6">
<rs-card
v-for="(stat, index) in queueStats"
:key="index"
class="transition-all duration-300 hover:shadow-lg"
>
<div class="pt-5 pb-3 px-5 flex items-center gap-4">
<div
class="p-4 flex justify-center items-center rounded-2xl"
:class="stat.bgColor"
>
<Icon class="text-2xl" :class="stat.iconColor" :name="stat.icon"></Icon>
</div>
<div class="flex-1 truncate">
<span class="block font-bold text-xl leading-tight" :class="stat.textColor">
{{ stat.value }}
</span>
<span class="text-sm font-medium text-gray-600">
{{ stat.title }}
</span>
</div>
</div>
</rs-card>
</div>
<!-- Filters and Controls -->
<rs-card class="mb-6">
<template #body>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Job Type</label>
<select
v-model="filters.jobType"
class="w-full p-2 border border-gray-300 rounded-md"
>
<option value="">All Types</option>
<option value="email">Email</option>
<option value="sms">SMS</option>
<option value="push">Push Notification</option>
<option value="webhook">Webhook</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Status</label>
<select
v-model="filters.status"
class="w-full p-2 border border-gray-300 rounded-md"
>
<option value="">All Status</option>
<option value="pending">Pending</option>
<option value="processing">Processing</option>
<option value="completed">Completed</option>
<option value="failed">Failed</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Channel</label>
<select
v-model="filters.channel"
class="w-full p-2 border border-gray-300 rounded-md"
>
<option value="">All Channels</option>
<option value="sendgrid">SendGrid</option>
<option value="twilio">Twilio</option>
<option value="firebase">Firebase</option>
<option value="slack">Slack</option>
</select>
</div>
<div class="flex items-end">
<rs-button @click="clearFilters" variant="outline" class="w-full">
<Icon class="mr-1" name="ic:outline-clear"></Icon>
Clear Filters
</rs-button>
</div>
</div>
</template>
</rs-card>
<!-- Queue Table -->
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-primary">Queue Jobs</h3>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-600">Last updated: {{ lastUpdated }}</span>
<rs-button variant="outline" size="sm" @click="refreshData">
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
Refresh
</rs-button>
</div>
</div>
</template>
<template #body>
<rs-table
:field="tableFields"
:data="filteredJobs"
:options="{ striped: true, hover: true }"
:optionsAdvanced="{ sortable: true, filterable: false }"
advanced
>
<template #status="{ value }">
<div class="flex items-center">
<div
class="w-2 h-2 rounded-full mr-2"
:class="getStatusColor(value.status)"
></div>
<span class="capitalize">{{ value.status }}</span>
</div>
</template>
<template #actions="{ value }">
<div class="flex gap-2">
<button
class="text-blue-600 hover:text-blue-800 text-sm"
@click="viewJobDetails(value)"
>
View
</button>
<button
v-if="value.status === 'failed'"
class="text-green-600 hover:text-green-800 text-sm"
@click="retryJob(value)"
>
Retry
</button>
</div>
</template>
</rs-table>
</template>
</rs-card>
<!-- Job Details Modal -->
<rs-modal v-model="showJobModal" size="lg">
<template #header>
<h3 class="text-lg font-semibold">Job Details</h3>
</template>
<template #body>
<div v-if="selectedJob" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700">Job ID</label>
<p class="text-sm text-gray-900">{{ selectedJob.id }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Status</label>
<rs-badge :variant="getStatusVariant(selectedJob.status)">
{{ selectedJob.status }}
</rs-badge>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Type</label>
<p class="text-sm text-gray-900">{{ selectedJob.type }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Channel</label>
<p class="text-sm text-gray-900">{{ selectedJob.channel }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Created</label>
<p class="text-sm text-gray-900">{{ selectedJob.created }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700"
>Processing Time</label
>
<p class="text-sm text-gray-900">{{ selectedJob.processingTime }}</p>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Payload</label>
<pre class="bg-gray-100 p-3 rounded text-xs overflow-auto max-h-40">{{
JSON.stringify(selectedJob.payload, null, 2)
}}</pre>
</div>
<div v-if="selectedJob.error">
<label class="block text-sm font-medium text-gray-700 mb-2"
>Error Details</label
>
<div class="bg-red-50 border border-red-200 rounded p-3">
<p class="text-sm text-red-800">{{ selectedJob.error }}</p>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<rs-button variant="outline" @click="showJobModal = false">Close</rs-button>
<rs-button
v-if="selectedJob?.status === 'failed'"
@click="retryJob"
variant="primary"
>
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
Retry Job
</rs-button>
</div>
</template>
</rs-modal>
</div>
</template>
<script setup>
definePageMeta({
title: "Queue Monitor",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Notification",
path: "/notification",
},
{
name: "Queue & Scheduler",
path: "/notification/queue-scheduler",
},
{
name: "Monitor",
path: "/notification/queue-scheduler/monitor",
},
],
});
// Reactive data
const autoRefresh = ref(true);
const lastUpdated = ref(new Date().toLocaleTimeString());
const showJobModal = ref(false);
const selectedJob = ref(null);
// Filters
const filters = ref({
jobType: "",
status: "",
channel: "",
});
// Queue statistics
const queueStats = ref([
{
title: "Pending",
value: "1,247",
icon: "ic:outline-schedule",
bgColor: "bg-blue-100",
iconColor: "text-blue-600",
textColor: "text-blue-600",
},
{
title: "Processing",
value: "89",
icon: "ic:outline-sync",
bgColor: "bg-yellow-100",
iconColor: "text-yellow-600",
textColor: "text-yellow-600",
},
{
title: "Completed",
value: "8,932",
icon: "ic:outline-check-circle",
bgColor: "bg-green-100",
iconColor: "text-green-600",
textColor: "text-green-600",
},
{
title: "Failed",
value: "23",
icon: "ic:outline-error",
bgColor: "bg-red-100",
iconColor: "text-red-600",
textColor: "text-red-600",
},
{
title: "Retrying",
value: "7",
icon: "ic:outline-refresh",
bgColor: "bg-purple-100",
iconColor: "text-purple-600",
textColor: "text-purple-600",
},
]);
// Table fields
const tableFields = ref([
{ key: "id", label: "Job ID", sortable: true },
{ key: "type", label: "Type", sortable: true },
{ key: "channel", label: "Channel", sortable: true },
{ key: "status", label: "Status", sortable: true },
{ key: "priority", label: "Priority", sortable: true },
{ key: "created", label: "Created", sortable: true },
{ key: "processingTime", label: "Processing Time", sortable: true },
{ key: "actions", label: "Actions", sortable: false },
]);
// Mock job data
const jobs = ref([
{
id: "job_001",
type: "email",
channel: "sendgrid",
status: "completed",
priority: "high",
created: "2024-01-15 10:30:00",
processingTime: "2.3s",
payload: { to: "user@example.com", subject: "Welcome!", template: "welcome" },
error: null,
},
{
id: "job_002",
type: "sms",
channel: "twilio",
status: "processing",
priority: "medium",
created: "2024-01-15 10:32:00",
processingTime: "1.8s",
payload: { to: "+1234567890", message: "Your OTP is 123456" },
error: null,
},
{
id: "job_003",
type: "push",
channel: "firebase",
status: "failed",
priority: "low",
created: "2024-01-15 10:35:00",
processingTime: "0.5s",
payload: {
token: "device_token_123",
title: "New Message",
body: "You have a new message",
},
error: "Invalid device token",
},
{
id: "job_004",
type: "webhook",
channel: "slack",
status: "pending",
priority: "high",
created: "2024-01-15 10:40:00",
processingTime: "-",
payload: { url: "https://hooks.slack.com/...", data: { text: "Order completed" } },
error: null,
},
{
id: "job_005",
type: "email",
channel: "sendgrid",
status: "retrying",
priority: "medium",
created: "2024-01-15 10:25:00",
processingTime: "5.2s",
payload: {
to: "customer@example.com",
subject: "Order Confirmation",
template: "order",
},
error: "Rate limit exceeded",
},
]);
// Computed filtered jobs
const filteredJobs = computed(() => {
let filtered = jobs.value;
if (filters.value.jobType) {
filtered = filtered.filter((job) => job.type === filters.value.jobType);
}
if (filters.value.status) {
filtered = filtered.filter((job) => job.status === filters.value.status);
}
if (filters.value.channel) {
filtered = filtered.filter((job) => job.channel === filters.value.channel);
}
// Return plain data without VNodes
return filtered;
});
// Methods
function getStatusColor(status) {
const colors = {
pending: "bg-blue-500",
processing: "bg-yellow-500",
completed: "bg-green-500",
failed: "bg-red-500",
retrying: "bg-purple-500",
};
return colors[status] || "bg-gray-500";
}
function getStatusVariant(status) {
const variants = {
pending: "info",
processing: "warning",
completed: "success",
failed: "danger",
retrying: "secondary",
};
return variants[status] || "default";
}
function viewJobDetails(job) {
selectedJob.value = job;
showJobModal.value = true;
}
function retryJob(job) {
// Mock retry logic
if (job) {
job.status = "pending";
job.error = null;
}
showJobModal.value = false;
// In real app, would make API call to retry the job
}
function toggleAutoRefresh() {
autoRefresh.value = !autoRefresh.value;
}
function refreshData() {
lastUpdated.value = new Date().toLocaleTimeString();
// In real app, would fetch fresh data from API
}
function clearFilters() {
filters.value = {
jobType: "",
status: "",
channel: "",
};
}
// Auto refresh functionality
let refreshInterval;
onMounted(() => {
if (autoRefresh.value) {
refreshInterval = setInterval(() => {
if (autoRefresh.value) {
refreshData();
}
}, 5000); // Refresh every 5 seconds
}
});
onUnmounted(() => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
});
watch(autoRefresh, (newValue) => {
if (newValue) {
refreshInterval = setInterval(() => {
if (autoRefresh.value) {
refreshData();
}
}, 5000);
} else {
if (refreshInterval) {
clearInterval(refreshInterval);
}
}
});
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,768 @@
<template>
<div>
<LayoutsBreadcrumb />
<!-- Header Section -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-speed"></Icon>
<h1 class="text-xl font-bold text-primary">Rate Limiting</h1>
</div>
</template>
<template #body>
<p class="text-gray-600">
Throttles how many messages/jobs can be processed per second/minute/hour.
Avoid hitting API limits (Twilio, SendGrid) and prevent spammy behavior that can trigger blacklisting.
</p>
</template>
</rs-card>
<!-- Rate Limit Statistics -->
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
<rs-card
v-for="(stat, index) in rateLimitStats"
:key="index"
class="transition-all duration-300 hover:shadow-lg"
>
<div class="pt-5 pb-3 px-5 flex items-center gap-4">
<div
class="p-4 flex justify-center items-center rounded-2xl"
:class="stat.bgColor"
>
<Icon class="text-2xl" :class="stat.iconColor" :name="stat.icon"></Icon>
</div>
<div class="flex-1 truncate">
<span class="block font-bold text-xl leading-tight" :class="stat.textColor">
{{ stat.value }}
</span>
<span class="text-sm font-medium text-gray-600">
{{ stat.title }}
</span>
</div>
</div>
</rs-card>
</div>
<!-- Current Usage Overview -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-primary">Current Usage Overview</h3>
<div class="flex items-center gap-2">
<div class="flex items-center">
<div class="w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse"></div>
<span class="text-sm text-gray-600">Live Updates</span>
</div>
<rs-button variant="outline" size="sm" @click="refreshUsage">
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
Refresh
</rs-button>
</div>
</div>
</template>
<template #body>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="(usage, index) in currentUsage"
:key="index"
class="border border-gray-200 rounded-lg p-4"
>
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<Icon class="mr-2 text-primary" :name="usage.icon"></Icon>
<h4 class="font-semibold">{{ usage.service }}</h4>
</div>
<rs-badge :variant="getUsageVariant(usage.percentage)">
{{ usage.percentage }}%
</rs-badge>
</div>
<div class="space-y-2">
<div class="flex justify-between text-sm">
<span class="text-gray-600">Current Rate:</span>
<span class="font-medium">{{ usage.currentRate }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600">Limit:</span>
<span class="font-medium">{{ usage.limit }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600">Window:</span>
<span class="font-medium">{{ usage.window }}</span>
</div>
</div>
<div class="mt-3">
<rs-progress-bar
:value="usage.percentage"
:variant="usage.percentage > 80 ? 'danger' : usage.percentage > 60 ? 'warning' : 'success'"
/>
</div>
<div class="mt-2 text-xs text-gray-500">
Resets in {{ usage.resetTime }}
</div>
</div>
</div>
</template>
</rs-card>
<!-- Rate Limit Configuration -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-primary">Rate Limit Configuration</h3>
<rs-button variant="outline" size="sm" @click="showConfigModal = true">
<Icon class="mr-1" name="ic:outline-settings"></Icon>
Configure Limits
</rs-button>
</div>
</template>
<template #body>
<rs-table
:field="configTableFields"
:data="rateLimitConfigs"
:options="{ striped: true, hover: true }"
:optionsAdvanced="{ sortable: true, filterable: false }"
advanced
/>
</template>
</rs-card>
<!-- Rate Limit Violations -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-primary">Recent Rate Limit Violations</h3>
<div class="flex items-center gap-2">
<select v-model="violationFilter" class="p-2 border border-gray-300 rounded-md text-sm">
<option value="">All Services</option>
<option value="email">Email</option>
<option value="sms">SMS</option>
<option value="push">Push</option>
<option value="webhook">Webhook</option>
</select>
<rs-button variant="outline" size="sm" @click="refreshViolations">
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
Refresh
</rs-button>
</div>
</div>
</template>
<template #body>
<div v-if="filteredViolations.length === 0" class="text-center py-8 text-gray-500">
<Icon class="text-4xl mb-2" name="ic:outline-check-circle"></Icon>
<p>No rate limit violations in the selected timeframe</p>
</div>
<div v-else class="space-y-3">
<div
v-for="(violation, index) in filteredViolations"
:key="index"
class="border border-red-200 bg-red-50 rounded-lg p-4"
>
<div class="flex items-center justify-between mb-2">
<div class="flex items-center">
<Icon class="mr-2 text-red-600" name="ic:outline-warning"></Icon>
<span class="font-semibold text-red-800">{{ violation.service }} Rate Limit Exceeded</span>
</div>
<span class="text-sm text-red-600">{{ violation.timestamp }}</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div>
<span class="text-red-700">Attempted Rate:</span>
<span class="ml-1 font-medium">{{ violation.attemptedRate }}</span>
</div>
<div>
<span class="text-red-700">Limit:</span>
<span class="ml-1 font-medium">{{ violation.limit }}</span>
</div>
<div>
<span class="text-red-700">Messages Dropped:</span>
<span class="ml-1 font-medium">{{ violation.droppedMessages }}</span>
</div>
</div>
<p class="text-sm text-red-700 mt-2">{{ violation.description }}</p>
</div>
</div>
</template>
</rs-card>
<!-- Rate Limit Analytics -->
<rs-card class="mb-6">
<template #header>
<h3 class="text-lg font-semibold text-primary">Rate Limit Analytics</h3>
</template>
<template #body>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Usage Trends -->
<div>
<h4 class="font-semibold mb-4">Usage Trends (Last 24 Hours)</h4>
<div class="space-y-3">
<div
v-for="(trend, index) in usageTrends"
:key="index"
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<div class="flex items-center">
<Icon class="mr-2 text-primary" :name="trend.icon"></Icon>
<div>
<p class="font-medium">{{ trend.service }}</p>
<p class="text-sm text-gray-600">Peak: {{ trend.peak }}</p>
</div>
</div>
<div class="text-right">
<p class="font-medium">{{ trend.average }}</p>
<p class="text-sm text-gray-600">Average</p>
</div>
</div>
</div>
</div>
<!-- Efficiency Metrics -->
<div>
<h4 class="font-semibold mb-4">Efficiency Metrics</h4>
<div class="space-y-4">
<div class="bg-green-50 border border-green-200 rounded p-3">
<div class="flex items-center mb-2">
<Icon class="mr-2 text-green-600" name="ic:outline-trending-up"></Icon>
<span class="font-medium text-green-800">Throughput Optimization</span>
</div>
<p class="text-sm text-green-700">
Current efficiency: {{ efficiencyMetrics.throughputOptimization }}%
</p>
</div>
<div class="bg-blue-50 border border-blue-200 rounded p-3">
<div class="flex items-center mb-2">
<Icon class="mr-2 text-blue-600" name="ic:outline-schedule"></Icon>
<span class="font-medium text-blue-800">Queue Utilization</span>
</div>
<p class="text-sm text-blue-700">
Average queue utilization: {{ efficiencyMetrics.queueUtilization }}%
</p>
</div>
<div class="bg-purple-50 border border-purple-200 rounded p-3">
<div class="flex items-center mb-2">
<Icon class="mr-2 text-purple-600" name="ic:outline-timer"></Icon>
<span class="font-medium text-purple-800">Response Time</span>
</div>
<p class="text-sm text-purple-700">
Average response time: {{ efficiencyMetrics.responseTime }}ms
</p>
</div>
<div class="bg-orange-50 border border-orange-200 rounded p-3">
<div class="flex items-center mb-2">
<Icon class="mr-2 text-orange-600" name="ic:outline-error"></Icon>
<span class="font-medium text-orange-800">Error Rate</span>
</div>
<p class="text-sm text-orange-700">
Rate limit errors: {{ efficiencyMetrics.errorRate }}%
</p>
</div>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Rate Limit Testing -->
<rs-card>
<template #header>
<h3 class="text-lg font-semibold text-primary">Rate Limit Testing</h3>
</template>
<template #body>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Service to Test</label>
<select v-model="testConfig.service" class="w-full p-2 border border-gray-300 rounded-md">
<option value="">Select service</option>
<option value="email">Email (SendGrid)</option>
<option value="sms">SMS (Twilio)</option>
<option value="push">Push (Firebase)</option>
<option value="webhook">Webhook</option>
</select>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Messages per Second</label>
<input
v-model.number="testConfig.rate"
type="number"
class="w-full p-2 border border-gray-300 rounded-md"
placeholder="10"
min="1"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Duration (seconds)</label>
<input
v-model.number="testConfig.duration"
type="number"
class="w-full p-2 border border-gray-300 rounded-md"
placeholder="60"
min="1"
max="300"
/>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Test Message</label>
<textarea
v-model="testConfig.message"
class="w-full p-2 border border-gray-300 rounded-md"
rows="3"
placeholder="Test message content"
></textarea>
</div>
<rs-button
@click="startRateLimitTest"
variant="primary"
class="w-full"
:disabled="testRunning"
>
<Icon class="mr-1" :name="testRunning ? 'ic:outline-stop' : 'ic:outline-play-arrow'"></Icon>
{{ testRunning ? 'Test Running...' : 'Start Rate Limit Test' }}
</rs-button>
</div>
<div class="space-y-4">
<h4 class="font-semibold">Test Results</h4>
<div v-if="testResults.length === 0" class="text-center text-gray-500 py-8">
<Icon class="text-4xl mb-2" name="ic:outline-science"></Icon>
<p>Run a test to see results</p>
</div>
<div v-else class="space-y-3 max-h-60 overflow-y-auto">
<div
v-for="(result, index) in testResults"
:key="index"
class="border border-gray-200 rounded p-3"
>
<div class="flex justify-between items-start mb-2">
<span class="font-medium">{{ result.service }}</span>
<span :class="{
'text-green-600': result.status === 'success',
'text-red-600': result.status === 'rate_limited',
'text-yellow-600': result.status === 'warning'
}" class="text-sm font-medium">{{ result.status }}</span>
</div>
<div class="text-sm space-y-1">
<div class="flex justify-between">
<span class="text-gray-600">Messages Sent:</span>
<span>{{ result.messagesSent }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Rate Achieved:</span>
<span>{{ result.rateAchieved }}/sec</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Errors:</span>
<span>{{ result.errors }}</span>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">{{ result.timestamp }}</p>
</div>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Configuration Modal -->
<rs-modal v-model="showConfigModal" size="lg">
<template #header>
<h3 class="text-lg font-semibold">Rate Limit Configuration</h3>
</template>
<template #body>
<div class="space-y-6">
<div
v-for="(config, index) in editableConfigs"
:key="index"
class="border border-gray-200 rounded-lg p-4"
>
<div class="flex items-center mb-4">
<Icon class="mr-2 text-primary" :name="config.icon"></Icon>
<h4 class="font-semibold">{{ config.service }} Configuration</h4>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Messages per Second</label>
<input
v-model.number="config.perSecond"
type="number"
class="w-full p-2 border border-gray-300 rounded-md"
min="1"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Messages per Minute</label>
<input
v-model.number="config.perMinute"
type="number"
class="w-full p-2 border border-gray-300 rounded-md"
min="1"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Messages per Hour</label>
<input
v-model.number="config.perHour"
type="number"
class="w-full p-2 border border-gray-300 rounded-md"
min="1"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Burst Limit</label>
<input
v-model.number="config.burstLimit"
type="number"
class="w-full p-2 border border-gray-300 rounded-md"
min="1"
/>
</div>
</div>
<div class="mt-4">
<label class="flex items-center">
<input
v-model="config.enabled"
type="checkbox"
class="mr-2"
/>
<span class="text-sm font-medium text-gray-700">Enable rate limiting for this service</span>
</label>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<rs-button variant="outline" @click="showConfigModal = false">Cancel</rs-button>
<rs-button @click="saveRateLimitConfig" variant="primary">Save Configuration</rs-button>
</div>
</template>
</rs-modal>
</div>
</template>
<script setup>
definePageMeta({
title: "Rate Limiting",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Notification",
path: "/notification",
},
{
name: "Queue & Scheduler",
path: "/notification/queue-scheduler",
},
{
name: "Rate Limiting",
path: "/notification/queue-scheduler/rate-limit",
},
],
});
// Reactive data
const showConfigModal = ref(false);
const violationFilter = ref('');
const testRunning = ref(false);
const testResults = ref([]);
// Statistics
const rateLimitStats = ref([
{
title: "Active Limits",
value: "12",
icon: "ic:outline-speed",
bgColor: "bg-blue-100",
iconColor: "text-blue-600",
textColor: "text-blue-600"
},
{
title: "Messages/Hour",
value: "45.2K",
icon: "ic:outline-trending-up",
bgColor: "bg-green-100",
iconColor: "text-green-600",
textColor: "text-green-600"
},
{
title: "Violations Today",
value: "3",
icon: "ic:outline-warning",
bgColor: "bg-red-100",
iconColor: "text-red-600",
textColor: "text-red-600"
},
{
title: "Efficiency",
value: "96.8%",
icon: "ic:outline-check-circle",
bgColor: "bg-purple-100",
iconColor: "text-purple-600",
textColor: "text-purple-600"
}
]);
// Current usage
const currentUsage = ref([
{
service: 'Email (SendGrid)',
icon: 'ic:outline-email',
currentRate: '850/hour',
limit: '1000/hour',
percentage: 85,
window: '1 hour',
resetTime: '23 minutes'
},
{
service: 'SMS (Twilio)',
icon: 'ic:outline-sms',
currentRate: '45/minute',
limit: '100/minute',
percentage: 45,
window: '1 minute',
resetTime: '32 seconds'
},
{
service: 'Push (Firebase)',
icon: 'ic:outline-notifications',
currentRate: '1200/hour',
limit: '5000/hour',
percentage: 24,
window: '1 hour',
resetTime: '45 minutes'
},
{
service: 'Webhook',
icon: 'ic:outline-webhook',
currentRate: '15/second',
limit: '20/second',
percentage: 75,
window: '1 second',
resetTime: '0.5 seconds'
}
]);
// Configuration table fields
const configTableFields = ref([
{ key: 'service', label: 'Service', sortable: true },
{ key: 'perSecond', label: 'Per Second', sortable: true },
{ key: 'perMinute', label: 'Per Minute', sortable: true },
{ key: 'perHour', label: 'Per Hour', sortable: true },
{ key: 'burstLimit', label: 'Burst Limit', sortable: true },
{ key: 'status', label: 'Status', sortable: true },
{ key: 'actions', label: 'Actions', sortable: false }
]);
// Rate limit configurations
const rateLimitConfigs = ref([
{
service: 'Email (SendGrid)',
icon: 'ic:outline-email',
perSecond: 10,
perMinute: 600,
perHour: 1000,
burstLimit: 50,
enabled: true
},
{
service: 'SMS (Twilio)',
icon: 'ic:outline-sms',
perSecond: 5,
perMinute: 100,
perHour: 2000,
burstLimit: 20,
enabled: true
},
{
service: 'Push (Firebase)',
icon: 'ic:outline-notifications',
perSecond: 50,
perMinute: 1000,
perHour: 5000,
burstLimit: 200,
enabled: true
},
{
service: 'Webhook',
icon: 'ic:outline-webhook',
perSecond: 20,
perMinute: 500,
perHour: 10000,
burstLimit: 100,
enabled: true
}
].map(config => ({
...config,
status: h('span', {
class: `px-2 py-1 rounded text-xs font-medium ${
config.enabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
}`
}, config.enabled ? 'Active' : 'Disabled'),
actions: h('button', {
class: 'text-blue-600 hover:text-blue-800 text-sm',
onClick: () => editRateLimit(config)
}, 'Edit')
})));
// Editable configs for modal
const editableConfigs = ref(JSON.parse(JSON.stringify(rateLimitConfigs.value.map(c => ({
service: c.service,
icon: c.icon,
perSecond: c.perSecond,
perMinute: c.perMinute,
perHour: c.perHour,
burstLimit: c.burstLimit,
enabled: c.enabled
})))));
// Rate limit violations
const violations = ref([
{
service: 'Email',
timestamp: '2024-01-15 14:30:00',
attemptedRate: '1200/hour',
limit: '1000/hour',
droppedMessages: 45,
description: 'Newsletter campaign exceeded hourly limit during peak hours'
},
{
service: 'SMS',
timestamp: '2024-01-15 12:15:00',
attemptedRate: '150/minute',
limit: '100/minute',
droppedMessages: 23,
description: 'OTP verification burst exceeded per-minute limit'
},
{
service: 'Webhook',
timestamp: '2024-01-15 10:45:00',
attemptedRate: '25/second',
limit: '20/second',
droppedMessages: 12,
description: 'Order webhook notifications exceeded per-second limit'
}
]);
// Usage trends
const usageTrends = ref([
{
service: 'Email',
icon: 'ic:outline-email',
peak: '950/hour',
average: '650/hour'
},
{
service: 'SMS',
icon: 'ic:outline-sms',
peak: '85/minute',
average: '45/minute'
},
{
service: 'Push',
icon: 'ic:outline-notifications',
peak: '2100/hour',
average: '1200/hour'
},
{
service: 'Webhook',
icon: 'ic:outline-webhook',
peak: '18/second',
average: '12/second'
}
]);
// Efficiency metrics
const efficiencyMetrics = ref({
throughputOptimization: 96.8,
queueUtilization: 78.5,
responseTime: 245,
errorRate: 0.3
});
// Test configuration
const testConfig = ref({
service: '',
rate: 10,
duration: 60,
message: 'This is a rate limit test message'
});
// Computed filtered violations
const filteredViolations = computed(() => {
if (!violationFilter.value) {
return violations.value;
}
return violations.value.filter(v => v.service.toLowerCase() === violationFilter.value);
});
// Methods
function getUsageVariant(percentage) {
if (percentage > 80) return 'danger';
if (percentage > 60) return 'warning';
return 'success';
}
function refreshUsage() {
// Mock refresh
console.log('Refreshing usage data...');
}
function refreshViolations() {
// Mock refresh
console.log('Refreshing violations...');
}
function editRateLimit(config) {
// Find and update the editable config
const editableConfig = editableConfigs.value.find(c => c.service === config.service);
if (editableConfig) {
Object.assign(editableConfig, config);
}
showConfigModal.value = true;
}
function saveRateLimitConfig() {
// Mock save
console.log('Saving rate limit configuration...', editableConfigs.value);
showConfigModal.value = false;
}
function startRateLimitTest() {
if (!testConfig.value.service) {
return;
}
testRunning.value = true;
// Mock test execution
setTimeout(() => {
const result = {
service: testConfig.value.service,
messagesSent: Math.floor(testConfig.value.rate * testConfig.value.duration * 0.9),
rateAchieved: Math.floor(testConfig.value.rate * 0.9),
errors: Math.floor(Math.random() * 5),
status: Math.random() > 0.7 ? 'rate_limited' : 'success',
timestamp: new Date().toLocaleString()
};
testResults.value.unshift(result);
testRunning.value = false;
}, 3000);
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,692 @@
<template>
<div>
<LayoutsBreadcrumb />
<!-- Header Section -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-refresh"></Icon>
<h1 class="text-xl font-bold text-primary">Retry & Dead Letter Queue</h1>
</div>
</template>
<template #body>
<p class="text-gray-600">
Handles failed jobs with retry logic and dead letter queue for permanent failures.
Critical for debugging and ensuring no message is lost without explanation.
</p>
</template>
</rs-card>
<!-- Statistics -->
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
<rs-card
v-for="(stat, index) in retryStats"
:key="index"
class="transition-all duration-300 hover:shadow-lg"
>
<div class="pt-5 pb-3 px-5 flex items-center gap-4">
<div
class="p-4 flex justify-center items-center rounded-2xl"
:class="stat.bgColor"
>
<Icon class="text-2xl" :class="stat.iconColor" :name="stat.icon"></Icon>
</div>
<div class="flex-1 truncate">
<span class="block font-bold text-xl leading-tight" :class="stat.textColor">
{{ stat.value }}
</span>
<span class="text-sm font-medium text-gray-600">
{{ stat.title }}
</span>
</div>
</div>
</rs-card>
</div>
<!-- Retry Configuration -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-primary">Retry Configuration</h3>
<rs-button variant="outline" size="sm" @click="showRetryConfigModal = true">
<Icon class="mr-1" name="ic:outline-settings"></Icon>
Configure
</rs-button>
</div>
</template>
<template #body>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div
v-for="(config, index) in retryConfigs"
:key="index"
class="border border-gray-200 rounded-lg p-4"
>
<div class="flex items-center mb-3">
<Icon class="mr-2 text-primary" :name="config.icon"></Icon>
<h4 class="font-semibold">{{ config.type }}</h4>
</div>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-gray-600">Max Attempts:</span>
<span class="font-medium">{{ config.maxAttempts }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Backoff Strategy:</span>
<span class="font-medium">{{ config.backoffStrategy }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Initial Delay:</span>
<span class="font-medium">{{ config.initialDelay }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Max Delay:</span>
<span class="font-medium">{{ config.maxDelay }}</span>
</div>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Tabs for Failed Jobs and Dead Letter Queue -->
<rs-tab v-model="activeTab" class="mb-6">
<rs-tab-item name="failed" label="Failed Jobs">
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-primary">Failed Jobs</h3>
<div class="flex items-center gap-2">
<rs-button variant="outline" size="sm" @click="retryAllFailed">
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
Retry All
</rs-button>
<rs-button variant="outline" size="sm" @click="refreshFailedJobs">
<Icon class="mr-1" name="ic:outline-sync"></Icon>
Refresh
</rs-button>
</div>
</div>
</template>
<template #body>
<!-- Filters -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Job Type</label>
<select v-model="failedFilters.type" class="w-full p-2 border border-gray-300 rounded-md">
<option value="">All Types</option>
<option value="email">Email</option>
<option value="sms">SMS</option>
<option value="push">Push Notification</option>
<option value="webhook">Webhook</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Error Type</label>
<select v-model="failedFilters.errorType" class="w-full p-2 border border-gray-300 rounded-md">
<option value="">All Errors</option>
<option value="network">Network Error</option>
<option value="rate_limit">Rate Limit</option>
<option value="invalid_token">Invalid Token</option>
<option value="timeout">Timeout</option>
</select>
</div>
<div class="flex items-end">
<rs-button @click="clearFailedFilters" variant="outline" class="w-full">
<Icon class="mr-1" name="ic:outline-clear"></Icon>
Clear Filters
</rs-button>
</div>
</div>
<rs-table
:field="failedJobsFields"
:data="filteredFailedJobs"
:options="{ striped: true, hover: true }"
:optionsAdvanced="{ sortable: true, filterable: false }"
advanced
/>
</template>
</rs-card>
</rs-tab-item>
<rs-tab-item name="dead-letter" label="Dead Letter Queue">
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-primary">Dead Letter Queue</h3>
<div class="flex items-center gap-2">
<rs-button variant="outline" size="sm" @click="exportDeadLetters">
<Icon class="mr-1" name="ic:outline-download"></Icon>
Export
</rs-button>
<rs-button variant="outline" size="sm" @click="refreshDeadLetters">
<Icon class="mr-1" name="ic:outline-sync"></Icon>
Refresh
</rs-button>
</div>
</div>
</template>
<template #body>
<!-- Filters -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Job Type</label>
<select v-model="deadLetterFilters.type" class="w-full p-2 border border-gray-300 rounded-md">
<option value="">All Types</option>
<option value="email">Email</option>
<option value="sms">SMS</option>
<option value="push">Push Notification</option>
<option value="webhook">Webhook</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Date Range</label>
<select v-model="deadLetterFilters.dateRange" class="w-full p-2 border border-gray-300 rounded-md">
<option value="">All Time</option>
<option value="today">Today</option>
<option value="week">This Week</option>
<option value="month">This Month</option>
</select>
</div>
<div class="flex items-end">
<rs-button @click="clearDeadLetterFilters" variant="outline" class="w-full">
<Icon class="mr-1" name="ic:outline-clear"></Icon>
Clear Filters
</rs-button>
</div>
</div>
<rs-table
:field="deadLetterFields"
:data="filteredDeadLetters"
:options="{ striped: true, hover: true }"
:optionsAdvanced="{ sortable: true, filterable: false }"
advanced
/>
</template>
</rs-card>
</rs-tab-item>
</rs-tab>
<!-- Job Details Modal -->
<rs-modal v-model="showJobModal" size="lg">
<template #header>
<h3 class="text-lg font-semibold">Job Details</h3>
</template>
<template #body>
<div v-if="selectedJob" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700">Job ID</label>
<p class="text-sm text-gray-900">{{ selectedJob.id }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Type</label>
<p class="text-sm text-gray-900">{{ selectedJob.type }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Attempts</label>
<p class="text-sm text-gray-900">{{ selectedJob.attempts }}/{{ selectedJob.maxAttempts }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Last Attempt</label>
<p class="text-sm text-gray-900">{{ selectedJob.lastAttempt }}</p>
</div>
</div>
<!-- Error History -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Error History</label>
<div class="space-y-2 max-h-40 overflow-y-auto">
<div
v-for="(error, index) in selectedJob.errorHistory"
:key="index"
class="bg-red-50 border border-red-200 rounded p-3"
>
<div class="flex justify-between items-start mb-1">
<span class="text-sm font-medium text-red-800">Attempt {{ error.attempt }}</span>
<span class="text-xs text-red-600">{{ error.timestamp }}</span>
</div>
<p class="text-sm text-red-700">{{ error.message }}</p>
</div>
</div>
</div>
<!-- Payload -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Payload</label>
<pre class="bg-gray-100 p-3 rounded text-xs overflow-auto max-h-40">{{ JSON.stringify(selectedJob.payload, null, 2) }}</pre>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<rs-button variant="outline" @click="showJobModal = false">Close</rs-button>
<rs-button v-if="selectedJob?.canRetry" @click="retryJob(selectedJob)" variant="primary">
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
Retry Job
</rs-button>
<rs-button v-if="!selectedJob?.isDead" @click="moveToDeadLetter(selectedJob)" variant="danger">
<Icon class="mr-1" name="ic:outline-delete"></Icon>
Move to DLQ
</rs-button>
</div>
</template>
</rs-modal>
<!-- Retry Configuration Modal -->
<rs-modal v-model="showRetryConfigModal" size="lg">
<template #header>
<h3 class="text-lg font-semibold">Retry Configuration</h3>
</template>
<template #body>
<div class="space-y-6">
<div
v-for="(config, index) in retryConfigs"
:key="index"
class="border border-gray-200 rounded-lg p-4"
>
<div class="flex items-center mb-4">
<Icon class="mr-2 text-primary" :name="config.icon"></Icon>
<h4 class="font-semibold">{{ config.type }} Configuration</h4>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Max Attempts</label>
<input
v-model.number="config.maxAttempts"
type="number"
class="w-full p-2 border border-gray-300 rounded-md"
min="1"
max="10"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Backoff Strategy</label>
<select v-model="config.backoffStrategy" class="w-full p-2 border border-gray-300 rounded-md">
<option value="fixed">Fixed</option>
<option value="exponential">Exponential</option>
<option value="linear">Linear</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Initial Delay (seconds)</label>
<input
v-model.number="config.initialDelay"
type="number"
class="w-full p-2 border border-gray-300 rounded-md"
min="1"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Max Delay (seconds)</label>
<input
v-model.number="config.maxDelay"
type="number"
class="w-full p-2 border border-gray-300 rounded-md"
min="1"
/>
</div>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<rs-button variant="outline" @click="showRetryConfigModal = false">Cancel</rs-button>
<rs-button @click="saveRetryConfig" variant="primary">Save Configuration</rs-button>
</div>
</template>
</rs-modal>
</div>
</template>
<script setup>
definePageMeta({
title: "Retry & Dead Letter Queue",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Notification",
path: "/notification",
},
{
name: "Queue & Scheduler",
path: "/notification/queue-scheduler",
},
{
name: "Retry & Dead Letter",
path: "/notification/queue-scheduler/retry",
},
],
});
// Reactive data
const activeTab = ref('failed');
const showJobModal = ref(false);
const showRetryConfigModal = ref(false);
const selectedJob = ref(null);
// Filters
const failedFilters = ref({
type: '',
errorType: ''
});
const deadLetterFilters = ref({
type: '',
dateRange: ''
});
// Statistics
const retryStats = ref([
{
title: "Failed Jobs",
value: "156",
icon: "ic:outline-error",
bgColor: "bg-red-100",
iconColor: "text-red-600",
textColor: "text-red-600"
},
{
title: "Retrying",
value: "23",
icon: "ic:outline-refresh",
bgColor: "bg-yellow-100",
iconColor: "text-yellow-600",
textColor: "text-yellow-600"
},
{
title: "Dead Letters",
value: "45",
icon: "ic:outline-delete",
bgColor: "bg-gray-100",
iconColor: "text-gray-600",
textColor: "text-gray-600"
},
{
title: "Success Rate",
value: "94.2%",
icon: "ic:outline-trending-up",
bgColor: "bg-green-100",
iconColor: "text-green-600",
textColor: "text-green-600"
}
]);
// Retry configurations
const retryConfigs = ref([
{
type: 'Email',
icon: 'ic:outline-email',
maxAttempts: 3,
backoffStrategy: 'exponential',
initialDelay: 30,
maxDelay: 300
},
{
type: 'SMS',
icon: 'ic:outline-sms',
maxAttempts: 5,
backoffStrategy: 'linear',
initialDelay: 10,
maxDelay: 120
},
{
type: 'Push',
icon: 'ic:outline-notifications',
maxAttempts: 2,
backoffStrategy: 'fixed',
initialDelay: 60,
maxDelay: 60
},
{
type: 'Webhook',
icon: 'ic:outline-webhook',
maxAttempts: 4,
backoffStrategy: 'exponential',
initialDelay: 15,
maxDelay: 600
}
]);
// Table fields for failed jobs
const failedJobsFields = ref([
{ key: 'id', label: 'Job ID', sortable: true },
{ key: 'type', label: 'Type', sortable: true },
{ key: 'attempts', label: 'Attempts', sortable: true },
{ key: 'lastError', label: 'Last Error', sortable: false },
{ key: 'lastAttempt', label: 'Last Attempt', sortable: true },
{ key: 'nextRetry', label: 'Next Retry', sortable: true },
{ key: 'actions', label: 'Actions', sortable: false }
]);
// Table fields for dead letter queue
const deadLetterFields = ref([
{ key: 'id', label: 'Job ID', sortable: true },
{ key: 'type', label: 'Type', sortable: true },
{ key: 'totalAttempts', label: 'Total Attempts', sortable: true },
{ key: 'finalError', label: 'Final Error', sortable: false },
{ key: 'failedAt', label: 'Failed At', sortable: true },
{ key: 'actions', label: 'Actions', sortable: false }
]);
// Mock failed jobs data
const failedJobs = ref([
{
id: 'job_fail_001',
type: 'email',
attempts: 2,
maxAttempts: 3,
lastError: 'SMTP connection timeout',
lastAttempt: '2024-01-15 10:30:00',
nextRetry: '2024-01-15 11:00:00',
canRetry: true,
isDead: false,
payload: { to: 'user@example.com', subject: 'Welcome!' },
errorHistory: [
{ attempt: 1, message: 'SMTP server unavailable', timestamp: '2024-01-15 10:00:00' },
{ attempt: 2, message: 'SMTP connection timeout', timestamp: '2024-01-15 10:30:00' }
]
},
{
id: 'job_fail_002',
type: 'sms',
attempts: 3,
maxAttempts: 5,
lastError: 'Invalid phone number format',
lastAttempt: '2024-01-15 10:25:00',
nextRetry: '2024-01-15 10:45:00',
canRetry: true,
isDead: false,
payload: { to: '+invalid', message: 'Your OTP is 123456' },
errorHistory: [
{ attempt: 1, message: 'Invalid phone number format', timestamp: '2024-01-15 10:00:00' },
{ attempt: 2, message: 'Invalid phone number format', timestamp: '2024-01-15 10:15:00' },
{ attempt: 3, message: 'Invalid phone number format', timestamp: '2024-01-15 10:25:00' }
]
}
]);
// Mock dead letter queue data
const deadLetters = ref([
{
id: 'job_dead_001',
type: 'push',
totalAttempts: 2,
finalError: 'Invalid device token - token expired',
failedAt: '2024-01-14 15:30:00',
canRetry: false,
isDead: true,
payload: { token: 'expired_token_123', title: 'New Message' },
errorHistory: [
{ attempt: 1, message: 'Invalid device token', timestamp: '2024-01-14 15:00:00' },
{ attempt: 2, message: 'Invalid device token - token expired', timestamp: '2024-01-14 15:30:00' }
]
},
{
id: 'job_dead_002',
type: 'webhook',
totalAttempts: 4,
finalError: 'Endpoint not found (404)',
failedAt: '2024-01-13 12:45:00',
canRetry: false,
isDead: true,
payload: { url: 'https://api.example.com/webhook', data: { event: 'order.completed' } },
errorHistory: [
{ attempt: 1, message: 'Connection timeout', timestamp: '2024-01-13 12:00:00' },
{ attempt: 2, message: 'Connection timeout', timestamp: '2024-01-13 12:15:00' },
{ attempt: 3, message: 'Endpoint not found (404)', timestamp: '2024-01-13 12:30:00' },
{ attempt: 4, message: 'Endpoint not found (404)', timestamp: '2024-01-13 12:45:00' }
]
}
]);
// Computed filtered data
const filteredFailedJobs = computed(() => {
let filtered = failedJobs.value;
if (failedFilters.value.type) {
filtered = filtered.filter(job => job.type === failedFilters.value.type);
}
// Add actions column
return filtered.map(job => ({
...job,
attempts: `${job.attempts}/${job.maxAttempts}`,
actions: h('div', { class: 'flex gap-2' }, [
h('button', {
class: 'text-blue-600 hover:text-blue-800 text-sm',
onClick: () => viewJobDetails(job)
}, 'View'),
h('button', {
class: 'text-green-600 hover:text-green-800 text-sm',
onClick: () => retryJob(job)
}, 'Retry'),
h('button', {
class: 'text-red-600 hover:text-red-800 text-sm',
onClick: () => moveToDeadLetter(job)
}, 'Move to DLQ')
])
}));
});
const filteredDeadLetters = computed(() => {
let filtered = deadLetters.value;
if (deadLetterFilters.value.type) {
filtered = filtered.filter(job => job.type === deadLetterFilters.value.type);
}
// Add actions column
return filtered.map(job => ({
...job,
actions: h('div', { class: 'flex gap-2' }, [
h('button', {
class: 'text-blue-600 hover:text-blue-800 text-sm',
onClick: () => viewJobDetails(job)
}, 'View'),
h('button', {
class: 'text-red-600 hover:text-red-800 text-sm',
onClick: () => deleteDeadLetter(job)
}, 'Delete')
])
}));
});
// Methods
function viewJobDetails(job) {
selectedJob.value = job;
showJobModal.value = true;
}
function retryJob(job) {
// Mock retry logic
job.attempts += 1;
job.lastAttempt = new Date().toLocaleString();
job.nextRetry = new Date(Date.now() + 30 * 60000).toLocaleString(); // 30 minutes from now
if (job.attempts >= job.maxAttempts) {
moveToDeadLetter(job);
}
showJobModal.value = false;
}
function retryAllFailed() {
failedJobs.value.forEach(job => {
if (job.canRetry) {
retryJob(job);
}
});
}
function moveToDeadLetter(job) {
// Move job to dead letter queue
const deadJob = {
...job,
totalAttempts: job.attempts,
finalError: job.lastError,
failedAt: new Date().toLocaleString(),
canRetry: false,
isDead: true
};
deadLetters.value.unshift(deadJob);
// Remove from failed jobs
const index = failedJobs.value.findIndex(j => j.id === job.id);
if (index > -1) {
failedJobs.value.splice(index, 1);
}
showJobModal.value = false;
}
function deleteDeadLetter(job) {
const index = deadLetters.value.findIndex(j => j.id === job.id);
if (index > -1) {
deadLetters.value.splice(index, 1);
}
}
function clearFailedFilters() {
failedFilters.value = {
type: '',
errorType: ''
};
}
function clearDeadLetterFilters() {
deadLetterFilters.value = {
type: '',
dateRange: ''
};
}
function refreshFailedJobs() {
// Mock refresh
console.log('Refreshing failed jobs...');
}
function refreshDeadLetters() {
// Mock refresh
console.log('Refreshing dead letters...');
}
function exportDeadLetters() {
// Mock export
console.log('Exporting dead letters...');
}
function saveRetryConfig() {
// Mock save
console.log('Saving retry configuration...');
showRetryConfigModal.value = false;
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,707 @@
<template>
<div>
<LayoutsBreadcrumb />
<!-- Header Section -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-schedule"></Icon>
<h1 class="text-xl font-bold text-primary">Timezone Handling</h1>
</div>
</template>
<template #body>
<p class="text-gray-600">
Ensures messages are delivered at the right local time for each recipient.
Schedule birthday messages at 9AM local time and avoid 2AM push alerts across timezones.
</p>
</template>
</rs-card>
<!-- Current Time Display -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<rs-card
v-for="(timezone, index) in majorTimezones"
:key="index"
class="transition-all duration-300 hover:shadow-lg"
>
<div class="pt-5 pb-3 px-5 text-center">
<div class="mb-2">
<Icon class="text-primary text-2xl" name="ic:outline-access-time"></Icon>
</div>
<h3 class="font-semibold text-lg">{{ timezone.name }}</h3>
<p class="text-2xl font-bold text-primary">{{ timezone.time }}</p>
<p class="text-sm text-gray-600">{{ timezone.zone }}</p>
</div>
</rs-card>
</div>
<!-- Timezone Statistics -->
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
<rs-card
v-for="(stat, index) in timezoneStats"
:key="index"
class="transition-all duration-300 hover:shadow-lg"
>
<div class="pt-5 pb-3 px-5 flex items-center gap-4">
<div
class="p-4 flex justify-center items-center rounded-2xl"
:class="stat.bgColor"
>
<Icon class="text-2xl" :class="stat.iconColor" :name="stat.icon"></Icon>
</div>
<div class="flex-1 truncate">
<span class="block font-bold text-xl leading-tight" :class="stat.textColor">
{{ stat.value }}
</span>
<span class="text-sm font-medium text-gray-600">
{{ stat.title }}
</span>
</div>
</div>
</rs-card>
</div>
<!-- Timezone Configuration -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-primary">Timezone Configuration</h3>
<rs-button variant="outline" size="sm" @click="showConfigModal = true">
<Icon class="mr-1" name="ic:outline-settings"></Icon>
Configure
</rs-button>
</div>
</template>
<template #body>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="border border-gray-200 rounded-lg p-4">
<h4 class="font-semibold mb-3 flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-schedule"></Icon>
Default Delivery Times
</h4>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-gray-600">Morning Messages:</span>
<span class="font-medium">{{ config.morningTime }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Afternoon Messages:</span>
<span class="font-medium">{{ config.afternoonTime }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Evening Messages:</span>
<span class="font-medium">{{ config.eveningTime }}</span>
</div>
</div>
</div>
<div class="border border-gray-200 rounded-lg p-4">
<h4 class="font-semibold mb-3 flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-block"></Icon>
Quiet Hours
</h4>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-gray-600">Start Time:</span>
<span class="font-medium">{{ config.quietHours.start }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">End Time:</span>
<span class="font-medium">{{ config.quietHours.end }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Emergency Override:</span>
<span class="font-medium">{{ config.quietHours.allowEmergency ? 'Enabled' : 'Disabled' }}</span>
</div>
</div>
</div>
<div class="border border-gray-200 rounded-lg p-4">
<h4 class="font-semibold mb-3 flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-public"></Icon>
Timezone Detection
</h4>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-gray-600">Auto-detect:</span>
<span class="font-medium">{{ config.autoDetect ? 'Enabled' : 'Disabled' }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Fallback Timezone:</span>
<span class="font-medium">{{ config.fallbackTimezone }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Update Frequency:</span>
<span class="font-medium">{{ config.updateFrequency }}</span>
</div>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Scheduled Messages by Timezone -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-primary">Scheduled Messages by Timezone</h3>
<div class="flex items-center gap-2">
<select v-model="selectedTimezone" class="p-2 border border-gray-300 rounded-md text-sm">
<option value="">All Timezones</option>
<option v-for="tz in availableTimezones" :key="tz.value" :value="tz.value">
{{ tz.label }}
</option>
</select>
<rs-button variant="outline" size="sm" @click="refreshScheduledMessages">
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
Refresh
</rs-button>
</div>
</div>
</template>
<template #body>
<rs-table
:field="scheduledMessagesFields"
:data="filteredScheduledMessages"
:options="{ striped: true, hover: true }"
:optionsAdvanced="{ sortable: true, filterable: false }"
advanced
/>
</template>
</rs-card>
<!-- Timezone Distribution Chart -->
<rs-card class="mb-6">
<template #header>
<h3 class="text-lg font-semibold text-primary">User Distribution by Timezone</h3>
</template>
<template #body>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Chart would go here in a real implementation -->
<div class="space-y-3">
<div
v-for="(distribution, index) in timezoneDistribution"
:key="index"
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<div class="flex items-center">
<div class="w-4 h-4 rounded mr-3" :style="{ backgroundColor: distribution.color }"></div>
<div>
<p class="font-medium">{{ distribution.timezone }}</p>
<p class="text-sm text-gray-600">{{ distribution.users }} users</p>
</div>
</div>
<div class="text-right">
<p class="font-medium">{{ distribution.percentage }}%</p>
<p class="text-sm text-gray-600">{{ distribution.currentTime }}</p>
</div>
</div>
</div>
<div class="space-y-4">
<h4 class="font-semibold">Delivery Optimization</h4>
<div class="space-y-3">
<div class="bg-green-50 border border-green-200 rounded p-3">
<div class="flex items-center mb-2">
<Icon class="mr-2 text-green-600" name="ic:outline-check-circle"></Icon>
<span class="font-medium text-green-800">Optimal Delivery Windows</span>
</div>
<p class="text-sm text-green-700">
{{ optimizationStats.optimalWindows }} messages scheduled during optimal hours
</p>
</div>
<div class="bg-yellow-50 border border-yellow-200 rounded p-3">
<div class="flex items-center mb-2">
<Icon class="mr-2 text-yellow-600" name="ic:outline-warning"></Icon>
<span class="font-medium text-yellow-800">Quiet Hours Conflicts</span>
</div>
<p class="text-sm text-yellow-700">
{{ optimizationStats.quietHoursConflicts }} messages would be sent during quiet hours
</p>
</div>
<div class="bg-blue-50 border border-blue-200 rounded p-3">
<div class="flex items-center mb-2">
<Icon class="mr-2 text-blue-600" name="ic:outline-info"></Icon>
<span class="font-medium text-blue-800">Timezone Coverage</span>
</div>
<p class="text-sm text-blue-700">
Messages will be delivered across {{ optimizationStats.timezoneCoverage }} timezones
</p>
</div>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Timezone Testing Tool -->
<rs-card>
<template #header>
<h3 class="text-lg font-semibold text-primary">Timezone Testing Tool</h3>
</template>
<template #body>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Test Message</label>
<textarea
v-model="testMessage.content"
class="w-full p-2 border border-gray-300 rounded-md"
rows="3"
placeholder="Enter test message content"
></textarea>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Scheduled Time (UTC)</label>
<input
v-model="testMessage.scheduledTime"
type="datetime-local"
class="w-full p-2 border border-gray-300 rounded-md"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Message Type</label>
<select v-model="testMessage.type" class="w-full p-2 border border-gray-300 rounded-md">
<option value="email">Email</option>
<option value="sms">SMS</option>
<option value="push">Push Notification</option>
</select>
</div>
</div>
<rs-button @click="testTimezoneDelivery" variant="primary" class="w-full">
<Icon class="mr-1" name="ic:outline-play-arrow"></Icon>
Test Timezone Delivery
</rs-button>
</div>
<div class="space-y-4">
<h4 class="font-semibold">Delivery Preview</h4>
<div v-if="deliveryPreview.length > 0" class="space-y-2 max-h-60 overflow-y-auto">
<div
v-for="(preview, index) in deliveryPreview"
:key="index"
class="border border-gray-200 rounded p-3"
>
<div class="flex justify-between items-start mb-1">
<span class="font-medium">{{ preview.timezone }}</span>
<span class="text-sm text-gray-600">{{ preview.users }} users</span>
</div>
<div class="text-sm">
<p class="text-gray-600">Local delivery time: <span class="font-medium">{{ preview.localTime }}</span></p>
<p class="text-gray-600">Status:
<span :class="{
'text-green-600': preview.status === 'optimal',
'text-yellow-600': preview.status === 'suboptimal',
'text-red-600': preview.status === 'blocked'
}" class="font-medium">{{ preview.status }}</span>
</p>
</div>
</div>
</div>
<div v-else class="text-center text-gray-500 py-8">
<Icon class="text-4xl mb-2" name="ic:outline-schedule"></Icon>
<p>Run a test to see delivery preview</p>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Configuration Modal -->
<rs-modal v-model="showConfigModal" size="lg">
<template #header>
<h3 class="text-lg font-semibold">Timezone Configuration</h3>
</template>
<template #body>
<div class="space-y-6">
<div>
<h4 class="font-semibold mb-3">Default Delivery Times</h4>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Morning Messages</label>
<input
v-model="config.morningTime"
type="time"
class="w-full p-2 border border-gray-300 rounded-md"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Afternoon Messages</label>
<input
v-model="config.afternoonTime"
type="time"
class="w-full p-2 border border-gray-300 rounded-md"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Evening Messages</label>
<input
v-model="config.eveningTime"
type="time"
class="w-full p-2 border border-gray-300 rounded-md"
/>
</div>
</div>
</div>
<div>
<h4 class="font-semibold mb-3">Quiet Hours</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Start Time</label>
<input
v-model="config.quietHours.start"
type="time"
class="w-full p-2 border border-gray-300 rounded-md"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">End Time</label>
<input
v-model="config.quietHours.end"
type="time"
class="w-full p-2 border border-gray-300 rounded-md"
/>
</div>
</div>
<div class="mt-4">
<label class="flex items-center">
<input
v-model="config.quietHours.allowEmergency"
type="checkbox"
class="mr-2"
/>
<span class="text-sm font-medium text-gray-700">Allow emergency messages during quiet hours</span>
</label>
</div>
</div>
<div>
<h4 class="font-semibold mb-3">Timezone Detection</h4>
<div class="space-y-4">
<label class="flex items-center">
<input
v-model="config.autoDetect"
type="checkbox"
class="mr-2"
/>
<span class="text-sm font-medium text-gray-700">Auto-detect user timezones</span>
</label>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Fallback Timezone</label>
<select v-model="config.fallbackTimezone" class="w-full p-2 border border-gray-300 rounded-md">
<option value="UTC">UTC</option>
<option value="America/New_York">America/New_York</option>
<option value="Europe/London">Europe/London</option>
<option value="Asia/Tokyo">Asia/Tokyo</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Update Frequency</label>
<select v-model="config.updateFrequency" class="w-full p-2 border border-gray-300 rounded-md">
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
</select>
</div>
</div>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<rs-button variant="outline" @click="showConfigModal = false">Cancel</rs-button>
<rs-button @click="saveConfiguration" variant="primary">Save Configuration</rs-button>
</div>
</template>
</rs-modal>
</div>
</template>
<script setup>
definePageMeta({
title: "Timezone Handling",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Notification",
path: "/notification",
},
{
name: "Queue & Scheduler",
path: "/notification/queue-scheduler",
},
{
name: "Timezone Handling",
path: "/notification/queue-scheduler/timezone",
},
],
});
// Reactive data
const showConfigModal = ref(false);
const selectedTimezone = ref('');
const deliveryPreview = ref([]);
// Current time for major timezones
const majorTimezones = ref([
{
name: 'New York',
zone: 'America/New_York',
time: new Date().toLocaleTimeString('en-US', { timeZone: 'America/New_York' })
},
{
name: 'London',
zone: 'Europe/London',
time: new Date().toLocaleTimeString('en-US', { timeZone: 'Europe/London' })
},
{
name: 'Tokyo',
zone: 'Asia/Tokyo',
time: new Date().toLocaleTimeString('en-US', { timeZone: 'Asia/Tokyo' })
}
]);
// Update times every second
setInterval(() => {
majorTimezones.value.forEach(tz => {
tz.time = new Date().toLocaleTimeString('en-US', { timeZone: tz.zone });
});
}, 1000);
// Statistics
const timezoneStats = ref([
{
title: "Active Timezones",
value: "24",
icon: "ic:outline-public",
bgColor: "bg-blue-100",
iconColor: "text-blue-600",
textColor: "text-blue-600"
},
{
title: "Scheduled Messages",
value: "1,847",
icon: "ic:outline-schedule",
bgColor: "bg-green-100",
iconColor: "text-green-600",
textColor: "text-green-600"
},
{
title: "Optimal Deliveries",
value: "94.2%",
icon: "ic:outline-trending-up",
bgColor: "bg-purple-100",
iconColor: "text-purple-600",
textColor: "text-purple-600"
},
{
title: "Quiet Hours Respected",
value: "99.8%",
icon: "ic:outline-nights-stay",
bgColor: "bg-orange-100",
iconColor: "text-orange-600",
textColor: "text-orange-600"
}
]);
// Configuration
const config = ref({
morningTime: '09:00',
afternoonTime: '14:00',
eveningTime: '18:00',
quietHours: {
start: '22:00',
end: '07:00',
allowEmergency: true
},
autoDetect: true,
fallbackTimezone: 'UTC',
updateFrequency: 'daily'
});
// Available timezones
const availableTimezones = ref([
{ value: 'America/New_York', label: 'America/New_York (EST/EDT)' },
{ value: 'America/Los_Angeles', label: 'America/Los_Angeles (PST/PDT)' },
{ value: 'Europe/London', label: 'Europe/London (GMT/BST)' },
{ value: 'Europe/Paris', label: 'Europe/Paris (CET/CEST)' },
{ value: 'Asia/Tokyo', label: 'Asia/Tokyo (JST)' },
{ value: 'Asia/Shanghai', label: 'Asia/Shanghai (CST)' },
{ value: 'Asia/Kolkata', label: 'Asia/Kolkata (IST)' },
{ value: 'Australia/Sydney', label: 'Australia/Sydney (AEST/AEDT)' }
]);
// Table fields for scheduled messages
const scheduledMessagesFields = ref([
{ key: 'id', label: 'Message ID', sortable: true },
{ key: 'type', label: 'Type', sortable: true },
{ key: 'timezone', label: 'Timezone', sortable: true },
{ key: 'scheduledUTC', label: 'Scheduled (UTC)', sortable: true },
{ key: 'localTime', label: 'Local Time', sortable: true },
{ key: 'recipients', label: 'Recipients', sortable: true },
{ key: 'status', label: 'Status', sortable: true }
]);
// Mock scheduled messages data
const scheduledMessages = ref([
{
id: 'msg_001',
type: 'email',
timezone: 'America/New_York',
scheduledUTC: '2024-01-15 14:00:00',
localTime: '2024-01-15 09:00:00',
recipients: 1250,
status: 'scheduled'
},
{
id: 'msg_002',
type: 'push',
timezone: 'Europe/London',
scheduledUTC: '2024-01-15 09:00:00',
localTime: '2024-01-15 09:00:00',
recipients: 890,
status: 'scheduled'
},
{
id: 'msg_003',
type: 'sms',
timezone: 'Asia/Tokyo',
scheduledUTC: '2024-01-15 00:00:00',
localTime: '2024-01-15 09:00:00',
recipients: 2100,
status: 'scheduled'
}
]);
// Timezone distribution
const timezoneDistribution = ref([
{
timezone: 'America/New_York',
users: 15420,
percentage: 32.5,
color: '#3B82F6',
currentTime: new Date().toLocaleTimeString('en-US', { timeZone: 'America/New_York' })
},
{
timezone: 'Europe/London',
users: 12890,
percentage: 27.2,
color: '#10B981',
currentTime: new Date().toLocaleTimeString('en-US', { timeZone: 'Europe/London' })
},
{
timezone: 'Asia/Tokyo',
users: 9650,
percentage: 20.3,
color: '#F59E0B',
currentTime: new Date().toLocaleTimeString('en-US', { timeZone: 'Asia/Tokyo' })
},
{
timezone: 'Australia/Sydney',
users: 5840,
percentage: 12.3,
color: '#EF4444',
currentTime: new Date().toLocaleTimeString('en-US', { timeZone: 'Australia/Sydney' })
},
{
timezone: 'Others',
users: 3700,
percentage: 7.7,
color: '#8B5CF6',
currentTime: '-'
}
]);
// Optimization stats
const optimizationStats = ref({
optimalWindows: 1654,
quietHoursConflicts: 23,
timezoneCoverage: 18
});
// Test message
const testMessage = ref({
content: '',
scheduledTime: '',
type: 'email'
});
// Computed filtered scheduled messages
const filteredScheduledMessages = computed(() => {
let filtered = scheduledMessages.value;
if (selectedTimezone.value) {
filtered = filtered.filter(msg => msg.timezone === selectedTimezone.value);
}
return filtered.map(msg => ({
...msg,
status: h('span', {
class: `px-2 py-1 rounded text-xs font-medium ${
msg.status === 'scheduled' ? 'bg-blue-100 text-blue-800' :
msg.status === 'sent' ? 'bg-green-100 text-green-800' :
'bg-gray-100 text-gray-800'
}`
}, msg.status)
}));
});
// Methods
function refreshScheduledMessages() {
// Mock refresh
console.log('Refreshing scheduled messages...');
}
function testTimezoneDelivery() {
if (!testMessage.value.content || !testMessage.value.scheduledTime) {
return;
}
// Mock delivery preview generation
deliveryPreview.value = [
{
timezone: 'America/New_York',
users: 1250,
localTime: '09:00 AM',
status: 'optimal'
},
{
timezone: 'Europe/London',
localTime: '02:00 AM',
users: 890,
status: 'blocked'
},
{
timezone: 'Asia/Tokyo',
localTime: '11:00 AM',
users: 2100,
status: 'optimal'
},
{
timezone: 'Australia/Sydney',
localTime: '01:00 AM',
users: 580,
status: 'blocked'
}
];
}
function saveConfiguration() {
// Mock save
console.log('Saving timezone configuration...');
showConfigModal.value = false;
}
</script>
<style lang="scss" scoped></style>