Update various configuration files, components, and assets; enhance notification system and API endpoints; improve documentation and styles across the application.

This commit is contained in:
Haqeem Solehan
2025-10-16 16:05:39 +08:00
commit b124ff8092
336 changed files with 94392 additions and 0 deletions

View File

@@ -0,0 +1,643 @@
<template>
<div>
<LayoutsBreadcrumb />
<!-- Page Info Card -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-send"></Icon>
<h1 class="text-xl font-bold text-primary">Delivery Settings</h1>
</div>
</template>
<template #body>
<p class="text-gray-600">
Configure email, push notification, and SMS delivery settings.
</p>
</template>
</rs-card>
<!-- Loading Overlay -->
<div
v-if="isLoading"
class="fixed inset-0 blur-lg bg-opacity-50 z-50 flex items-center justify-center"
>
<div class="bg-white rounded-lg p-6 flex items-center space-x-4">
<Icon name="ic:outline-refresh" class="text-primary animate-spin" size="24" />
<span class="text-gray-700">Loading...</span>
</div>
</div>
<!-- Error Alert -->
<rs-alert
v-if="error"
variant="danger"
class="mb-6"
dismissible
@dismiss="error = null"
>
<template #icon>
<Icon name="ic:outline-error" />
</template>
{{ error }}
</rs-alert>
<!-- Channel Configuration -->
<div class="space-y-8 mb-6">
<!-- Email Configuration -->
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-email"></Icon>
<h2 class="text-lg font-semibold text-primary">Email Configuration</h2>
</div>
<rs-badge :variant="emailConfig.enabled ? 'success' : 'secondary'">
{{ emailConfig.enabled ? "Active" : "Disabled" }}
</rs-badge>
</div>
</template>
<template #body>
<div v-if="isLoading" class="flex items-center justify-center py-8">
<Icon name="ic:outline-refresh" class="text-primary animate-spin" size="24" />
</div>
<div v-else class="flex flex-col gap-4">
<div class="flex items-center justify-between">
<span class="font-medium">Enable Email Delivery</span>
<FormKit type="toggle" v-model="emailConfig.enabled" />
</div>
<FormKit
type="select"
label="Email Provider"
v-model="emailConfig.provider"
:options="emailProviders"
:disabled="!emailConfig.enabled"
class="w-full"
/>
<!-- Provider Configuration -->
<div class="space-y-4">
<!-- Mailtrap Configuration -->
<div v-if="emailConfig.provider === 'mailtrap'">
<!-- Mailtrap Info Banner -->
<div class="p-4 bg-blue-50 border border-blue-200 rounded-lg mb-4">
<div class="flex items-start gap-3">
<Icon name="ic:outline-info" class="text-blue-600 text-xl mt-0.5"></Icon>
<div class="text-sm">
<p class="font-semibold text-blue-900 mb-1">Mailtrap SMTP Configuration</p>
<p class="text-blue-700">
Use <code class="px-1 py-0.5 bg-blue-100 rounded">live.smtp.mailtrap.io</code> for production sending.
Port 587 (recommended) with STARTTLS.
</p>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormKit
type="text"
label="SMTP Host"
v-model="emailConfig.config.host"
:disabled="!emailConfig.enabled"
help="Use: live.smtp.mailtrap.io"
/>
<FormKit
type="number"
label="SMTP Port"
v-model="emailConfig.config.port"
:disabled="!emailConfig.enabled"
help="Recommended: 587"
/>
<FormKit
type="text"
label="SMTP Username"
v-model="emailConfig.config.user"
:disabled="!emailConfig.enabled"
help="Usually: apismtp@mailtrap.io"
/>
<FormKit
type="password"
label="SMTP Password / API Token"
v-model="emailConfig.config.pass"
:disabled="!emailConfig.enabled"
help="Your Mailtrap API token"
validation="required"
/>
<FormKit
type="email"
label="Sender Email"
v-model="emailConfig.config.senderEmail"
:disabled="!emailConfig.enabled"
help="Email address that will appear as the sender of notifications"
validation="required|email"
/>
<FormKit
type="text"
label="Sender Name (Optional)"
v-model="emailConfig.config.senderName"
:disabled="!emailConfig.enabled"
help="Name that will appear as the sender"
/>
</div>
</div>
<!-- AWS SES Configuration -->
<div v-if="emailConfig.provider === 'aws-ses'">
<!-- AWS SES Info Banner -->
<div class="p-4 bg-orange-50 border border-orange-200 rounded-lg mb-4">
<div class="flex items-start gap-3">
<Icon name="ic:outline-info" class="text-orange-600 text-xl mt-0.5"></Icon>
<div class="text-sm">
<p class="font-semibold text-orange-900 mb-1">AWS SES SMTP Configuration</p>
<p class="text-orange-700">
Use region-specific SMTP endpoint: <code class="px-1 py-0.5 bg-orange-100 rounded">email-smtp.&lt;region&gt;.amazonaws.com</code>
(e.g., email-smtp.us-east-1.amazonaws.com). Port 587 with STARTTLS.
</p>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormKit
type="text"
label="SMTP Host"
v-model="emailConfig.config.host"
:disabled="!emailConfig.enabled"
help="e.g., email-smtp.us-east-1.amazonaws.com"
placeholder="email-smtp.us-east-1.amazonaws.com"
/>
<FormKit
type="number"
label="SMTP Port"
v-model="emailConfig.config.port"
:disabled="!emailConfig.enabled"
help="Use 587 (STARTTLS) or 465 (TLS)"
/>
<FormKit
type="text"
label="SMTP Username"
v-model="emailConfig.config.user"
:disabled="!emailConfig.enabled"
help="Your AWS SES SMTP username (not IAM user)"
placeholder="AKIAIOSFODNN7EXAMPLE"
/>
<FormKit
type="password"
label="SMTP Password"
v-model="emailConfig.config.pass"
:disabled="!emailConfig.enabled"
help="Your AWS SES SMTP password (not secret key)"
validation="required"
/>
<FormKit
type="email"
label="Sender Email"
v-model="emailConfig.config.senderEmail"
:disabled="!emailConfig.enabled"
help="Must be verified in AWS SES"
validation="required|email"
/>
<FormKit
type="text"
label="Sender Name (Optional)"
v-model="emailConfig.config.senderName"
:disabled="!emailConfig.enabled"
help="Name that will appear as the sender"
/>
<FormKit
type="text"
label="AWS Region"
v-model="emailConfig.config.region"
:disabled="!emailConfig.enabled"
help="AWS region where SES is configured"
placeholder="us-east-1"
/>
<FormKit
type="text"
label="Configuration Set (Optional)"
v-model="emailConfig.config.configurationSet"
:disabled="!emailConfig.enabled"
help="AWS SES configuration set for tracking"
/>
</div>
</div>
</div>
<!-- Add more providers as needed -->
<div class="mt-6 p-4 bg-gray-50 rounded-lg flex flex-wrap gap-8">
<div>
<span class="text-gray-600">Status:</span>
<span class="ml-2 font-medium">{{ emailConfig.status }}</span>
</div>
<div>
<span class="text-gray-600">Success Rate:</span>
<span class="ml-2 font-medium">{{ emailConfig.successRate }}%</span>
</div>
</div>
<div class="flex justify-end">
<rs-button @click="saveEmailConfig" :disabled="isLoadingEmail">
<Icon
:name="isLoadingEmail ? 'ic:outline-refresh' : 'ic:outline-save'"
class="mr-1"
:class="{ 'animate-spin': isLoadingEmail }"
/>
{{ isLoadingEmail ? "Saving..." : "Save" }}
</rs-button>
</div>
</div>
</template>
</rs-card>
<!-- Push Configuration -->
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-notifications"></Icon>
<h2 class="text-lg font-semibold text-primary">
Push Notification Configuration
</h2>
</div>
<rs-badge :variant="pushConfig.enabled ? 'success' : 'secondary'">
{{ pushConfig.enabled ? "Active" : "Disabled" }}
</rs-badge>
</div>
</template>
<template #body>
<div v-if="isLoading" class="flex items-center justify-center py-8">
<Icon name="ic:outline-refresh" class="text-primary animate-spin" size="24" />
</div>
<div v-else class="flex flex-col gap-4">
<div class="flex items-center justify-between">
<span class="font-medium">Enable Push Notifications</span>
<FormKit type="toggle" v-model="pushConfig.enabled" />
</div>
<FormKit
type="select"
label="Push Provider"
v-model="pushConfig.provider"
:options="pushProviders"
:disabled="!pushConfig.enabled"
class="w-full"
/>
<!-- Provider-specific fields -->
<div
v-if="pushConfig.provider === 'firebase'"
class="grid grid-cols-1 md:grid-cols-2 gap-4"
>
<FormKit
type="text"
label="API Key"
v-model="pushConfig.config.apiKey"
:disabled="!pushConfig.enabled"
/>
<FormKit
type="text"
label="Project ID"
v-model="pushConfig.config.projectId"
:disabled="!pushConfig.enabled"
/>
</div>
<!-- Add more providers as needed -->
<div class="mt-6 p-4 bg-gray-50 rounded-lg flex flex-wrap gap-8">
<div>
<span class="text-gray-600">Status:</span>
<span class="ml-2 font-medium">{{ pushConfig.status }}</span>
</div>
<div>
<span class="text-gray-600">Success Rate:</span>
<span class="ml-2 font-medium">{{ pushConfig.successRate }}%</span>
</div>
</div>
<div class="flex justify-end">
<rs-button @click="savePushConfig" :disabled="isLoadingPush">
<Icon
:name="isLoadingPush ? 'ic:outline-refresh' : 'ic:outline-save'"
class="mr-1"
:class="{ 'animate-spin': isLoadingPush }"
/>
{{ isLoadingPush ? "Saving..." : "Save" }}
</rs-button>
</div>
</div>
</template>
</rs-card>
<!-- SMS Configuration -->
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-sms"></Icon>
<h2 class="text-lg font-semibold text-primary">SMS Configuration</h2>
</div>
<rs-badge :variant="smsConfig.enabled ? 'success' : 'secondary'">
{{ smsConfig.enabled ? "Active" : "Disabled" }}
</rs-badge>
</div>
</template>
<template #body>
<div v-if="isLoading" class="flex items-center justify-center py-8">
<Icon name="ic:outline-refresh" class="text-primary animate-spin" size="24" />
</div>
<div v-else class="flex flex-col gap-4">
<div class="flex items-center justify-between">
<span class="font-medium">Enable SMS Delivery</span>
<FormKit type="toggle" v-model="smsConfig.enabled" />
</div>
<FormKit
type="select"
label="SMS Provider"
v-model="smsConfig.provider"
:options="smsProviders"
:disabled="!smsConfig.enabled"
class="w-full"
/>
<!-- Provider-specific fields -->
<div
v-if="smsConfig.provider === 'twilio'"
class="grid grid-cols-1 md:grid-cols-2 gap-4"
>
<FormKit
type="text"
label="Account SID"
v-model="smsConfig.config.accountSid"
:disabled="!smsConfig.enabled"
/>
<FormKit
type="password"
label="Auth Token"
v-model="smsConfig.config.authToken"
:disabled="!smsConfig.enabled"
/>
<FormKit
type="text"
label="From Number"
v-model="smsConfig.config.from"
:disabled="!smsConfig.enabled"
/>
</div>
<!-- Add more providers as needed -->
<div class="mt-6 p-4 bg-gray-50 rounded-lg flex flex-wrap gap-8">
<div>
<span class="text-gray-600">Status:</span>
<span class="ml-2 font-medium">{{ smsConfig.status }}</span>
</div>
<div>
<span class="text-gray-600">Success Rate:</span>
<span class="ml-2 font-medium">{{ smsConfig.successRate }}%</span>
</div>
</div>
<div class="flex justify-end">
<rs-button @click="saveSmsConfig" :disabled="isLoadingSms">
<Icon
:name="isLoadingSms ? 'ic:outline-refresh' : 'ic:outline-save'"
class="mr-1"
:class="{ 'animate-spin': isLoadingSms }"
/>
{{ isLoadingSms ? "Saving..." : "Save" }}
</rs-button>
</div>
</div>
</template>
</rs-card>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from "vue";
import { useToast } from "@/composables/useToast";
import { useNotificationDelivery } from "@/composables/useNotificationDelivery";
definePageMeta({
title: "Notification Delivery",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Notification",
path: "/notification",
},
{
name: "Delivery",
path: "/notification/delivery",
type: "current",
},
],
});
// Email Configuration
const emailConfig = ref({
enabled: true,
provider: "mailtrap",
config: {
host: "live.smtp.mailtrap.io",
port: 587,
user: "apismtp@mailtrap.io",
pass: "",
senderEmail: "",
senderName: "",
},
status: "Connected",
successRate: 99.2,
});
const emailProviders = [
{ label: "Mailtrap", value: "mailtrap" },
{ label: "AWS SES", value: "aws-ses" }
];
// Push Configuration
const pushConfig = ref({
enabled: true,
provider: "firebase",
config: {
apiKey: "",
projectId: "",
},
status: "Connected",
successRate: 95.8,
});
const pushProviders = [{ label: "Firebase FCM", value: "firebase" }];
// Add SMS config and providers
const smsConfig = ref({
enabled: false,
provider: "twilio",
config: {
accountSid: "",
authToken: "",
from: "",
},
status: "Not Configured",
successRate: 0,
});
const smsProviders = [
{ label: "Twilio", value: "twilio" },
// Add more providers as needed
];
const isLoadingEmail = ref(false);
const isLoadingPush = ref(false);
const isLoadingSms = ref(false);
const toast = useToast();
const {
isLoading,
error,
fetchDeliveryStats,
fetchEmailConfig,
fetchPushConfig,
fetchSmsConfig,
fetchDeliverySettings,
updateEmailConfig,
updatePushConfig,
updateSmsConfig,
updateDeliverySettings,
} = useNotificationDelivery();
// Store provider-specific configs
const providerConfigs = ref({
mailtrap: null,
'aws-ses': null
});
// Methods
async function refreshData() {
try {
isLoading.value = true;
const [emailData, pushData, smsData] = await Promise.all([
fetchEmailConfig(),
fetchPushConfig(),
fetchSmsConfig(),
]);
// Store all provider configs
if (emailData.providers) {
providerConfigs.value = emailData.providers;
}
// Update email config with active provider
const activeProviderKey = emailData.activeProvider || emailData.provider;
const activeProviderData = emailData.providers?.[activeProviderKey] || emailData;
emailConfig.value = {
enabled: activeProviderData.enabled,
provider: activeProviderKey,
config: activeProviderData.config || {},
status: activeProviderData.status,
successRate: activeProviderData.successRate,
};
// Update push config
pushConfig.value = {
enabled: pushData.enabled,
provider: pushData.provider,
config: pushData.config || {},
status: pushData.status,
successRate: pushData.successRate,
};
// Update SMS config
smsConfig.value = {
enabled: smsData.enabled,
provider: smsData.provider,
config: smsData.config || {},
status: smsData.status,
successRate: smsData.successRate,
};
} catch (err) {
console.error("Error refreshing data:", err);
toast.error(err.message || "Failed to refresh data");
} finally {
isLoading.value = false;
}
}
async function saveEmailConfig() {
try {
isLoadingEmail.value = true;
await updateEmailConfig({
enabled: emailConfig.value.enabled,
provider: emailConfig.value.provider,
config: emailConfig.value.config,
});
toast.success("Email settings saved!");
} catch (err) {
toast.error(err.message || "Failed to save email settings");
} finally {
isLoadingEmail.value = false;
}
}
async function savePushConfig() {
try {
isLoadingPush.value = true;
await updatePushConfig({
enabled: pushConfig.value.enabled,
provider: pushConfig.value.provider,
config: pushConfig.value.config,
});
toast.success("Push settings saved!");
} catch (err) {
toast.error(err.message || "Failed to save push settings");
} finally {
isLoadingPush.value = false;
}
}
async function saveSmsConfig() {
try {
isLoadingSms.value = true;
await updateSmsConfig({
enabled: smsConfig.value.enabled,
provider: smsConfig.value.provider,
config: smsConfig.value.config,
});
toast.success("SMS settings saved!");
} catch (err) {
toast.error(err.message || "Failed to save SMS settings");
} finally {
isLoadingSms.value = false;
}
}
// Watch for provider changes and load saved config
watch(() => emailConfig.value.provider, (newProvider) => {
if (providerConfigs.value[newProvider]) {
// Load saved config for this provider
const savedConfig = providerConfigs.value[newProvider];
emailConfig.value.config = savedConfig.config || {};
emailConfig.value.status = savedConfig.status;
emailConfig.value.successRate = savedConfig.successRate;
} else {
// Load default config for new provider
if (newProvider === 'mailtrap') {
emailConfig.value.config = {
host: 'live.smtp.mailtrap.io',
port: 587,
user: 'apismtp@mailtrap.io',
pass: '',
senderEmail: '',
senderName: '',
};
} else if (newProvider === 'aws-ses') {
emailConfig.value.config = {
host: '',
port: 587,
user: '',
pass: '',
senderEmail: '',
senderName: '',
region: 'us-east-1',
configurationSet: '',
};
}
}
});
// Load initial data
onMounted(() => {
refreshData();
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,661 @@
<template>
<div>
<LayoutsBreadcrumb />
<!-- Page Info Card -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-monitor"></Icon>
<h1 class="text-xl font-bold text-primary">Delivery Monitor</h1>
</div>
</template>
<template #body>
<p class="text-gray-600">
Real-time monitoring of notification deliveries across all channels. Track individual messages,
monitor batch progress, and analyze delivery performance with detailed metrics.
</p>
</template>
</rs-card>
<!-- Real-time Metrics -->
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
<rs-card
v-for="(metric, index) in realTimeMetrics"
: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 rounded-2xl transition-all duration-300"
:class="metric.bgColor"
>
<Icon class="text-3xl" :name="metric.icon" :class="metric.iconColor"></Icon>
</div>
<div class="flex-1 truncate">
<span class="block font-bold text-2xl leading-tight" :class="metric.textColor">
{{ metric.value }}
</span>
<span class="text-sm font-medium text-gray-600">
{{ metric.title }}
</span>
</div>
</div>
</rs-card>
</div>
<!-- Message Search & Filter -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold text-primary">Message Tracking</h2>
<div class="flex gap-2">
<rs-button size="sm" variant="primary-outline" @click="refreshMessages">
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
Refresh
</rs-button>
<rs-button size="sm" variant="primary-outline" @click="showFilters = !showFilters">
<Icon class="mr-1" name="ic:outline-filter-list"></Icon>
Filters
</rs-button>
</div>
</div>
</template>
<template #body>
<!-- Search and Filters -->
<div class="mb-6 space-y-4">
<div class="flex gap-4">
<div class="flex-1">
<FormKit
type="text"
v-model="searchQuery"
placeholder="Search by message ID, recipient, or content..."
prefix-icon="search"
/>
</div>
<div class="w-48">
<FormKit
type="select"
v-model="selectedChannel"
:options="channelFilterOptions"
placeholder="All Channels"
/>
</div>
</div>
<!-- Advanced Filters -->
<div v-if="showFilters" class="grid grid-cols-1 md:grid-cols-4 gap-4 p-4 bg-gray-50 rounded-lg">
<FormKit
type="select"
v-model="filters.status"
:options="statusFilterOptions"
label="Status"
/>
<FormKit
type="select"
v-model="filters.priority"
:options="priorityFilterOptions"
label="Priority"
/>
<FormKit
type="date"
v-model="filters.dateFrom"
label="From Date"
/>
<FormKit
type="date"
v-model="filters.dateTo"
label="To Date"
/>
</div>
</div>
<!-- Messages Table -->
<rs-table
:field="messageTableFields"
:data="filteredMessages"
:advanced="true"
:options="{ striped: true, hover: true }"
:optionsAdvanced="{
sortable: true,
filterable: true,
responsive: true,
}"
:pageSize="20"
>
<template #messageId="{ row }">
<button
@click="viewMessageDetails(row)"
class="text-primary hover:underline font-mono text-sm"
>
{{ row.messageId }}
</button>
</template>
<template #status="{ row }">
<rs-badge :variant="getStatusVariant(row.status)" size="sm">
{{ row.status }}
</rs-badge>
</template>
<template #channel="{ row }">
<div class="flex items-center">
<Icon class="mr-2" :name="getChannelIcon(row.channel)"></Icon>
{{ row.channel }}
</div>
</template>
<template #priority="{ row }">
<rs-badge
:variant="getPriorityVariant(row.priority)"
size="sm"
>
{{ row.priority }}
</rs-badge>
</template>
<template #progress="{ row }">
<div class="w-full bg-gray-200 rounded-full h-2">
<div
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
:style="{ width: row.progress + '%' }"
></div>
</div>
<div class="text-xs text-gray-500 mt-1">{{ row.progress }}%</div>
</template>
<template #actions="{ row }">
<div class="flex gap-2">
<rs-button
size="sm"
variant="primary-outline"
@click="viewMessageDetails(row)"
>
<Icon name="ic:outline-visibility"></Icon>
</rs-button>
<rs-button
size="sm"
variant="secondary-outline"
@click="retryMessage(row)"
v-if="row.status === 'failed'"
>
<Icon name="ic:outline-refresh"></Icon>
</rs-button>
</div>
</template>
</rs-table>
</template>
</rs-card>
<!-- Live Activity Feed -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Real-time Activity -->
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-live-tv"></Icon>
<h2 class="text-lg font-semibold text-primary">Live Activity</h2>
</div>
<rs-badge variant="success" size="sm">Live</rs-badge>
</div>
</template>
<template #body>
<div class="space-y-3 max-h-96 overflow-y-auto">
<div
v-for="activity in liveActivities"
:key="activity.id"
class="flex items-start gap-3 p-3 border border-gray-200 rounded-lg"
>
<Icon
class="mt-1 flex-shrink-0"
:name="activity.icon"
:class="activity.iconColor"
></Icon>
<div class="flex-1 min-w-0">
<div class="font-medium text-sm">{{ activity.message }}</div>
<div class="text-xs text-gray-500">{{ activity.timestamp }}</div>
<div v-if="activity.details" class="text-xs text-gray-600 mt-1">
{{ activity.details }}
</div>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Channel Performance -->
<rs-card>
<template #header>
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-speed"></Icon>
<h2 class="text-lg font-semibold text-primary">Channel Performance</h2>
</div>
</template>
<template #body>
<div class="space-y-4">
<div
v-for="channel in channelPerformance"
:key="channel.name"
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" :name="channel.icon"></Icon>
<span class="font-semibold">{{ channel.name }}</span>
</div>
<rs-badge :variant="channel.statusVariant" size="sm">
{{ channel.status }}
</rs-badge>
</div>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<div class="text-gray-600">Throughput</div>
<div class="font-semibold">{{ channel.throughput }}/min</div>
</div>
<div>
<div class="text-gray-600">Success Rate</div>
<div class="font-semibold">{{ channel.successRate }}%</div>
</div>
<div>
<div class="text-gray-600">Avg Latency</div>
<div class="font-semibold">{{ channel.avgLatency }}ms</div>
</div>
<div>
<div class="text-gray-600">Queue Size</div>
<div class="font-semibold">{{ channel.queueSize }}</div>
</div>
</div>
<!-- Performance Chart -->
<div class="mt-3">
<div class="text-xs text-gray-500 mb-1">Performance Trend</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div
class="h-2 rounded-full transition-all duration-300"
:class="channel.performanceClass"
:style="{ width: channel.performanceScore + '%' }"
></div>
</div>
</div>
</div>
</div>
</template>
</rs-card>
</div>
<!-- Message Details Modal -->
<rs-modal v-model="showMessageModal" size="lg">
<template #header>
<h3 class="text-lg font-semibold">Message Details</h3>
</template>
<template #body>
<div v-if="selectedMessage" class="space-y-6">
<!-- Message Info -->
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-500 mb-1">Message ID</label>
<div class="font-mono text-sm">{{ selectedMessage.messageId }}</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-500 mb-1">Status</label>
<rs-badge :variant="getStatusVariant(selectedMessage.status)">
{{ selectedMessage.status }}
</rs-badge>
</div>
<div>
<label class="block text-sm font-medium text-gray-500 mb-1">Channel</label>
<div class="flex items-center">
<Icon class="mr-2" :name="getChannelIcon(selectedMessage.channel)"></Icon>
{{ selectedMessage.channel }}
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-500 mb-1">Priority</label>
<rs-badge :variant="getPriorityVariant(selectedMessage.priority)">
{{ selectedMessage.priority }}
</rs-badge>
</div>
</div>
<!-- Delivery Timeline -->
<div>
<label class="block text-sm font-medium text-gray-500 mb-3">Delivery Timeline</label>
<div class="space-y-3">
<div
v-for="event in selectedMessage.timeline"
:key="event.id"
class="flex items-center gap-3"
>
<div class="w-3 h-3 rounded-full" :class="event.statusColor"></div>
<div class="flex-1">
<div class="font-medium text-sm">{{ event.status }}</div>
<div class="text-xs text-gray-500">{{ event.timestamp }}</div>
<div v-if="event.details" class="text-xs text-gray-600">{{ event.details }}</div>
</div>
</div>
</div>
</div>
<!-- Message Content -->
<div>
<label class="block text-sm font-medium text-gray-500 mb-1">Content</label>
<div class="bg-gray-50 rounded-lg p-3 text-sm">
<div><strong>To:</strong> {{ selectedMessage.recipient }}</div>
<div><strong>Subject:</strong> {{ selectedMessage.subject }}</div>
<div class="mt-2"><strong>Body:</strong></div>
<div class="mt-1">{{ selectedMessage.content }}</div>
</div>
</div>
<!-- Provider Response -->
<div v-if="selectedMessage.providerResponse">
<label class="block text-sm font-medium text-gray-500 mb-1">Provider Response</label>
<div class="bg-gray-50 rounded-lg p-3">
<pre class="text-xs">{{ JSON.stringify(selectedMessage.providerResponse, null, 2) }}</pre>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<rs-button variant="outline" @click="showMessageModal = false">Close</rs-button>
<rs-button
variant="primary"
@click="retryMessage(selectedMessage)"
v-if="selectedMessage?.status === 'failed'"
>
Retry Message
</rs-button>
</div>
</template>
</rs-modal>
</div>
</template>
<script setup>
definePageMeta({
title: "Delivery Monitor",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Dashboard",
path: "/dashboard",
},
{
name: "Notification",
path: "/notification",
},
{
name: "Delivery Engine",
path: "/notification/delivery",
},
{
name: "Monitor",
path: "/notification/delivery/monitor",
type: "current",
},
],
});
import { ref, computed, onMounted, onUnmounted } from "vue";
// State
const showFilters = ref(false);
const showMessageModal = ref(false);
const selectedMessage = ref(null);
const searchQuery = ref("");
const selectedChannel = ref("");
// Filters
const filters = ref({
status: "",
priority: "",
dateFrom: "",
dateTo: "",
});
// Real-time metrics
const realTimeMetrics = ref([
{
title: "Messages/Min",
value: "1,247",
icon: "ic:outline-speed",
bgColor: "bg-blue-100",
iconColor: "text-blue-600",
textColor: "text-blue-600",
},
{
title: "Success Rate",
value: "98.7%",
icon: "ic:outline-check-circle",
bgColor: "bg-green-100",
iconColor: "text-green-600",
textColor: "text-green-600",
},
{
title: "Failed/Retrying",
value: "23",
icon: "ic:outline-error",
bgColor: "bg-red-100",
iconColor: "text-red-600",
textColor: "text-red-600",
},
{
title: "Avg Latency",
value: "1.2s",
icon: "ic:outline-timer",
bgColor: "bg-purple-100",
iconColor: "text-purple-600",
textColor: "text-purple-600",
},
]);
// Filter options
const channelFilterOptions = [
{ label: "All Channels", value: "" },
{ label: "Email", value: "email" },
{ label: "SMS", value: "sms" },
{ label: "Push", value: "push" },
{ label: "In-App", value: "inapp" },
];
const statusFilterOptions = [
{ label: "All Status", value: "" },
{ label: "Queued", value: "queued" },
{ label: "Sent", value: "sent" },
{ label: "Delivered", value: "delivered" },
{ label: "Opened", value: "opened" },
{ label: "Failed", value: "failed" },
{ label: "Bounced", value: "bounced" },
];
const priorityFilterOptions = [
{ label: "All Priorities", value: "" },
{ label: "Critical", value: "critical" },
{ label: "High", value: "high" },
{ label: "Medium", value: "medium" },
{ label: "Low", value: "low" },
];
// Table configuration
const messageTableFields = ref([
{ key: "messageId", label: "Message ID", sortable: true },
{ key: "recipient", label: "Recipient", sortable: true },
{ key: "channel", label: "Channel", sortable: true },
{ key: "status", label: "Status", sortable: true },
{ key: "priority", label: "Priority", sortable: true },
{ key: "createdAt", label: "Created", sortable: true },
{ key: "progress", label: "Progress", sortable: false },
{ key: "actions", label: "Actions", sortable: false },
]);
// Sample messages data
const messages = ref([
{
messageId: "msg_001",
recipient: "user@example.com",
channel: "Email",
status: "delivered",
priority: "high",
createdAt: "2024-01-15 10:30:00",
progress: 100,
subject: "Welcome to our platform",
content: "Thank you for joining us...",
timeline: [
{ id: 1, status: "Queued", timestamp: "2024-01-15 10:30:00", statusColor: "bg-blue-500", details: "Message queued for processing" },
{ id: 2, status: "Sent", timestamp: "2024-01-15 10:30:15", statusColor: "bg-yellow-500", details: "Sent via SendGrid" },
{ id: 3, status: "Delivered", timestamp: "2024-01-15 10:30:18", statusColor: "bg-green-500", details: "Successfully delivered" },
],
providerResponse: { messageId: "sg_abc123", status: "delivered" },
},
// Add more sample data...
]);
// Live activities
const liveActivities = ref([
{
id: 1,
message: "Email batch completed",
timestamp: "Just now",
icon: "ic:outline-email",
iconColor: "text-green-500",
details: "1,250 emails sent successfully",
},
{
id: 2,
message: "SMS delivery in progress",
timestamp: "2 seconds ago",
icon: "ic:outline-sms",
iconColor: "text-blue-500",
details: "89/120 messages delivered",
},
// Add more activities...
]);
// Channel performance
const channelPerformance = ref([
{
name: "Email",
icon: "ic:outline-email",
status: "Healthy",
statusVariant: "success",
throughput: "1,200",
successRate: 99.2,
avgLatency: 800,
queueSize: 45,
performanceScore: 95,
performanceClass: "bg-green-500",
},
{
name: "SMS",
icon: "ic:outline-sms",
status: "Warning",
statusVariant: "warning",
throughput: "450",
successRate: 97.8,
avgLatency: 2100,
queueSize: 123,
performanceScore: 78,
performanceClass: "bg-yellow-500",
},
// Add more channels...
]);
// Computed
const filteredMessages = computed(() => {
let filtered = messages.value;
if (searchQuery.value) {
filtered = filtered.filter(msg =>
msg.messageId.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
msg.recipient.toLowerCase().includes(searchQuery.value.toLowerCase())
);
}
if (selectedChannel.value) {
filtered = filtered.filter(msg => msg.channel.toLowerCase() === selectedChannel.value);
}
if (filters.value.status) {
filtered = filtered.filter(msg => msg.status === filters.value.status);
}
if (filters.value.priority) {
filtered = filtered.filter(msg => msg.priority === filters.value.priority);
}
return filtered;
});
// Methods
function getStatusVariant(status) {
const variants = {
queued: "info",
sent: "warning",
delivered: "success",
opened: "success",
failed: "danger",
bounced: "danger",
};
return variants[status] || "secondary";
}
function getChannelIcon(channel) {
const icons = {
Email: "ic:outline-email",
SMS: "ic:outline-sms",
Push: "ic:outline-notifications",
"In-App": "ic:outline-app-registration",
};
return icons[channel] || "ic:outline-help";
}
function getPriorityVariant(priority) {
const variants = {
critical: "danger",
high: "warning",
medium: "info",
low: "secondary",
};
return variants[priority] || "secondary";
}
function viewMessageDetails(message) {
selectedMessage.value = message;
showMessageModal.value = true;
}
function retryMessage(message) {
console.log("Retrying message:", message.messageId);
// Implementation for retrying failed messages
}
function refreshMessages() {
console.log("Refreshing messages...");
// Implementation for refreshing message list
}
// Auto-refresh every 10 seconds
let refreshInterval;
onMounted(() => {
refreshInterval = setInterval(() => {
refreshMessages();
}, 10000);
});
onUnmounted(() => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,805 @@
<template>
<div>
<LayoutsBreadcrumb />
<!-- Page Info Card -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-extension"></Icon>
<h1 class="text-xl font-bold text-primary">Provider Management</h1>
</div>
</template>
<template #body>
<p class="text-gray-600">
Configure and manage third-party notification service providers. Set up
credentials, fallback rules, and monitor provider performance across all
channels.
</p>
</template>
</rs-card>
<!-- Provider Overview 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 providerStats"
: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 rounded-2xl transition-all duration-300"
:class="stat.bgColor"
>
<Icon class="text-3xl" :name="stat.icon" :class="stat.iconColor"></Icon>
</div>
<div class="flex-1 truncate">
<span class="block font-bold text-2xl 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>
<!-- Providers by Channel -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Email Providers -->
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-email"></Icon>
<h2 class="text-lg font-semibold text-primary">Email Providers</h2>
</div>
<rs-button size="sm" variant="primary-outline" @click="addProvider('email')">
<Icon class="mr-1" name="ic:outline-add"></Icon>
Add Provider
</rs-button>
</div>
</template>
<template #body>
<div class="space-y-4">
<div
v-for="provider in emailProviders"
:key="provider.id"
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-2xl" :name="provider.icon"></Icon>
<div>
<div class="font-semibold">{{ provider.name }}</div>
<div class="text-sm text-gray-600">{{ provider.description }}</div>
</div>
</div>
<div class="flex items-center gap-2">
<rs-badge
:variant="provider.status === 'active' ? 'success' : 'danger'"
size="sm"
>
{{ provider.status }}
</rs-badge>
<rs-button
size="sm"
variant="secondary-outline"
@click="configureProvider(provider)"
>
<Icon name="ic:outline-settings"></Icon>
</rs-button>
</div>
</div>
<div class="grid grid-cols-3 gap-4 text-sm">
<div>
<div class="text-gray-600">Success Rate</div>
<div class="font-semibold">{{ provider.successRate }}%</div>
</div>
<div>
<div class="text-gray-600">Quota Used</div>
<div class="font-semibold">{{ provider.quotaUsed }}%</div>
</div>
<div>
<div class="text-gray-600">Priority</div>
<div class="font-semibold">{{ provider.priority }}</div>
</div>
</div>
</div>
</div>
</template>
</rs-card>
<!-- SMS Providers -->
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-sms"></Icon>
<h2 class="text-lg font-semibold text-primary">SMS Providers</h2>
</div>
<rs-button size="sm" variant="primary-outline" @click="addProvider('sms')">
<Icon class="mr-1" name="ic:outline-add"></Icon>
Add Provider
</rs-button>
</div>
</template>
<template #body>
<div class="space-y-4">
<div
v-for="provider in smsProviders"
:key="provider.id"
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-2xl" :name="provider.icon"></Icon>
<div>
<div class="font-semibold">{{ provider.name }}</div>
<div class="text-sm text-gray-600">{{ provider.description }}</div>
</div>
</div>
<div class="flex items-center gap-2">
<rs-badge
:variant="provider.status === 'active' ? 'success' : 'danger'"
size="sm"
>
{{ provider.status }}
</rs-badge>
<rs-button
size="sm"
variant="secondary-outline"
@click="configureProvider(provider)"
>
<Icon name="ic:outline-settings"></Icon>
</rs-button>
</div>
</div>
<div class="grid grid-cols-3 gap-4 text-sm">
<div>
<div class="text-gray-600">Success Rate</div>
<div class="font-semibold">{{ provider.successRate }}%</div>
</div>
<div>
<div class="text-gray-600">Quota Used</div>
<div class="font-semibold">{{ provider.quotaUsed }}%</div>
</div>
<div>
<div class="text-gray-600">Priority</div>
<div class="font-semibold">{{ provider.priority }}</div>
</div>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Push Providers -->
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-notifications"></Icon>
<h2 class="text-lg font-semibold text-primary">Push Providers</h2>
</div>
<rs-button size="sm" variant="primary-outline" @click="addProvider('push')">
<Icon class="mr-1" name="ic:outline-add"></Icon>
Add Provider
</rs-button>
</div>
</template>
<template #body>
<div class="space-y-4">
<div
v-for="provider in pushProviders"
:key="provider.id"
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-2xl" :name="provider.icon"></Icon>
<div>
<div class="font-semibold">{{ provider.name }}</div>
<div class="text-sm text-gray-600">{{ provider.description }}</div>
</div>
</div>
<div class="flex items-center gap-2">
<rs-badge
:variant="provider.status === 'active' ? 'success' : 'danger'"
size="sm"
>
{{ provider.status }}
</rs-badge>
<rs-button
size="sm"
variant="secondary-outline"
@click="configureProvider(provider)"
>
<Icon name="ic:outline-settings"></Icon>
</rs-button>
</div>
</div>
<div class="grid grid-cols-3 gap-4 text-sm">
<div>
<div class="text-gray-600">Success Rate</div>
<div class="font-semibold">{{ provider.successRate }}%</div>
</div>
<div>
<div class="text-gray-600">Quota Used</div>
<div class="font-semibold">{{ provider.quotaUsed }}%</div>
</div>
<div>
<div class="text-gray-600">Priority</div>
<div class="font-semibold">{{ provider.priority }}</div>
</div>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Webhook Providers -->
<rs-card>
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-webhook"></Icon>
<h2 class="text-lg font-semibold text-primary">Webhook Endpoints</h2>
</div>
<rs-button
size="sm"
variant="primary-outline"
@click="addProvider('webhook')"
>
<Icon class="mr-1" name="ic:outline-add"></Icon>
Add Webhook
</rs-button>
</div>
</template>
<template #body>
<div class="space-y-4">
<div
v-for="provider in webhookProviders"
:key="provider.id"
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-2xl" :name="provider.icon"></Icon>
<div>
<div class="font-semibold">{{ provider.name }}</div>
<div class="text-sm text-gray-600 font-mono">{{ provider.url }}</div>
</div>
</div>
<div class="flex items-center gap-2">
<rs-badge
:variant="provider.status === 'active' ? 'success' : 'danger'"
size="sm"
>
{{ provider.status }}
</rs-badge>
<rs-button
size="sm"
variant="secondary-outline"
@click="configureProvider(provider)"
>
<Icon name="ic:outline-settings"></Icon>
</rs-button>
</div>
</div>
<div class="grid grid-cols-3 gap-4 text-sm">
<div>
<div class="text-gray-600">Success Rate</div>
<div class="font-semibold">{{ provider.successRate }}%</div>
</div>
<div>
<div class="text-gray-600">Avg Response</div>
<div class="font-semibold">{{ provider.avgResponse }}ms</div>
</div>
<div>
<div class="text-gray-600">Last Used</div>
<div class="font-semibold">{{ provider.lastUsed }}</div>
</div>
</div>
</div>
</div>
</template>
</rs-card>
</div>
<!-- Fallback Configuration -->
<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-alt-route"></Icon>
<h2 class="text-lg font-semibold text-primary">Fallback Configuration</h2>
</div>
<rs-button
size="sm"
variant="primary-outline"
@click="showFallbackModal = true"
>
<Icon class="mr-1" name="ic:outline-add"></Icon>
Add Rule
</rs-button>
</div>
</template>
<template #body>
<div class="space-y-4">
<div
v-for="rule in fallbackRules"
:key="rule.id"
class="border border-gray-200 rounded-lg p-4"
>
<div class="flex items-center justify-between mb-3">
<div>
<div class="font-semibold">{{ rule.name }}</div>
<div class="text-sm text-gray-600">{{ rule.description }}</div>
</div>
<div class="flex items-center gap-2">
<rs-badge :variant="rule.enabled ? 'success' : 'secondary'" size="sm">
{{ rule.enabled ? "Active" : "Disabled" }}
</rs-badge>
<rs-button
size="sm"
variant="secondary-outline"
@click="editFallbackRule(rule)"
>
<Icon name="ic:outline-edit"></Icon>
</rs-button>
</div>
</div>
<div class="text-sm space-y-2">
<div class="flex items-center gap-2">
<span class="text-gray-600">Primary:</span>
<span class="font-medium">{{ rule.primary }}</span>
<Icon name="ic:outline-arrow-forward" class="text-gray-400"></Icon>
<span class="text-gray-600">Fallback:</span>
<span class="font-medium">{{ rule.fallback }}</span>
</div>
<div>
<span class="text-gray-600">Trigger:</span>
<span class="font-medium">{{ rule.trigger }}</span>
</div>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Provider Configuration Modal -->
<rs-modal v-model="showProviderModal" size="lg">
<template #header>
<h3 class="text-lg font-semibold">
{{ selectedProvider ? "Configure" : "Add" }} Provider
</h3>
</template>
<template #body>
<div v-if="selectedProvider" class="space-y-6">
<!-- Basic Info -->
<div class="grid grid-cols-2 gap-4">
<FormKit
type="text"
v-model="providerForm.name"
label="Provider Name"
placeholder="Enter provider name"
/>
<FormKit
type="select"
v-model="providerForm.channel"
label="Channel"
:options="channelOptions"
/>
</div>
<!-- Provider-specific Configuration -->
<div v-if="providerForm.channel === 'email'">
<h4 class="font-semibold mb-3">Email Configuration</h4>
<div class="grid grid-cols-2 gap-4">
<FormKit
type="select"
v-model="providerForm.provider"
label="Provider Type"
:options="emailProviderOptions"
/>
<FormKit
type="password"
v-model="providerForm.apiKey"
label="API Key"
placeholder="Enter API key"
/>
<FormKit
type="password"
v-model="providerForm.apiSecret"
label="API Secret"
placeholder="Enter API secret"
/>
<FormKit
type="text"
v-model="providerForm.fromEmail"
label="From Email"
placeholder="noreply@example.com"
/>
</div>
</div>
<div v-if="providerForm.channel === 'sms'">
<h4 class="font-semibold mb-3">SMS Configuration</h4>
<div class="grid grid-cols-2 gap-4">
<FormKit
type="select"
v-model="providerForm.provider"
label="Provider Type"
:options="smsProviderOptions"
/>
<FormKit
type="text"
v-model="providerForm.accountSid"
label="Account SID"
placeholder="Enter account SID"
/>
<FormKit
type="password"
v-model="providerForm.authToken"
label="Auth Token"
placeholder="Enter auth token"
/>
<FormKit
type="text"
v-model="providerForm.fromNumber"
label="From Number"
placeholder="+1234567890"
/>
</div>
</div>
<!-- Priority & Limits -->
<div>
<h4 class="font-semibold mb-3">Priority & Limits</h4>
<div class="grid grid-cols-3 gap-4">
<FormKit
type="number"
v-model="providerForm.priority"
label="Priority"
placeholder="1-10"
min="1"
max="10"
/>
<FormKit
type="number"
v-model="providerForm.rateLimit"
label="Rate Limit (per minute)"
placeholder="100"
/>
<FormKit
type="number"
v-model="providerForm.dailyQuota"
label="Daily Quota"
placeholder="10000"
/>
</div>
</div>
<!-- Test Connection -->
<div class="border border-gray-200 rounded-lg p-4">
<h4 class="font-semibold mb-3">Test Connection</h4>
<div class="flex items-center gap-3">
<rs-button variant="secondary" @click="testProviderConnection">
<Icon class="mr-1" name="ic:outline-wifi-tethering"></Icon>
Test Connection
</rs-button>
<div v-if="connectionTestResult" class="flex items-center gap-2">
<Icon
:name="
connectionTestResult.success
? 'ic:outline-check-circle'
: 'ic:outline-error'
"
:class="
connectionTestResult.success ? 'text-green-500' : 'text-red-500'
"
></Icon>
<span
:class="
connectionTestResult.success ? 'text-green-600' : 'text-red-600'
"
>
{{ connectionTestResult.message }}
</span>
</div>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<rs-button variant="outline" @click="showProviderModal = false"
>Cancel</rs-button
>
<rs-button variant="primary" @click="saveProvider">Save Provider</rs-button>
</div>
</template>
</rs-modal>
</div>
</template>
<script setup>
definePageMeta({
title: "Provider Management",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Dashboard",
path: "/dashboard",
},
{
name: "Notification",
path: "/notification",
},
{
name: "Delivery Engine",
path: "/notification/delivery",
},
{
name: "Providers",
path: "/notification/delivery/providers",
type: "current",
},
],
});
import { ref, reactive } from "vue";
// Modal states
const showProviderModal = ref(false);
const showFallbackModal = ref(false);
const selectedProvider = ref(null);
const connectionTestResult = ref(null);
// Provider form
const providerForm = reactive({
name: "",
channel: "",
provider: "",
apiKey: "",
apiSecret: "",
accountSid: "",
authToken: "",
fromEmail: "",
fromNumber: "",
priority: 5,
rateLimit: 100,
dailyQuota: 10000,
});
// Statistics
const providerStats = ref([
{
title: "Active Providers",
value: "12",
icon: "ic:outline-verified",
bgColor: "bg-green-100",
iconColor: "text-green-600",
textColor: "text-green-600",
},
{
title: "Total Messages Today",
value: "28.5K",
icon: "ic:outline-send",
bgColor: "bg-blue-100",
iconColor: "text-blue-600",
textColor: "text-blue-600",
},
{
title: "Avg Success Rate",
value: "98.2%",
icon: "ic:outline-trending-up",
bgColor: "bg-purple-100",
iconColor: "text-purple-600",
textColor: "text-purple-600",
},
{
title: "Failed Providers",
value: "1",
icon: "ic:outline-error",
bgColor: "bg-red-100",
iconColor: "text-red-600",
textColor: "text-red-600",
},
]);
// Provider data
const emailProviders = ref([
{
id: 1,
name: "Mailtrap",
description: "Primary email delivery service (SMTP)",
icon: "ic:outline-email",
status: "active",
successRate: 99.5,
quotaUsed: 45,
priority: 1,
},
{
id: 2,
name: "AWS SES",
description: "Scalable email service from AWS",
icon: "ic:outline-cloud",
status: "inactive",
successRate: 99.0,
quotaUsed: 0,
priority: 2,
},
]);
const smsProviders = ref([
{
id: 4,
name: "Twilio",
description: "Primary SMS service",
icon: "ic:outline-sms",
status: "active",
successRate: 97.8,
quotaUsed: 78,
priority: 1,
},
{
id: 5,
name: "Nexmo",
description: "International SMS fallback",
icon: "ic:outline-sms",
status: "active",
successRate: 96.5,
quotaUsed: 34,
priority: 2,
},
]);
const pushProviders = ref([
{
id: 6,
name: "Firebase FCM",
description: "Android push notifications",
icon: "ic:outline-notifications",
status: "active",
successRate: 95.4,
quotaUsed: 45,
priority: 1,
},
{
id: 7,
name: "Apple APNs",
description: "iOS push notifications",
icon: "ic:outline-phone-iphone",
status: "active",
successRate: 94.8,
quotaUsed: 52,
priority: 1,
},
]);
const webhookProviders = ref([
{
id: 8,
name: "CRM Webhook",
url: "https://api.crm.com/webhooks/delivery",
icon: "ic:outline-webhook",
status: "active",
successRate: 99.1,
avgResponse: 150,
lastUsed: "2 min ago",
},
{
id: 9,
name: "Analytics Webhook",
url: "https://analytics.example.com/webhook",
icon: "ic:outline-analytics",
status: "active",
successRate: 98.7,
avgResponse: 89,
lastUsed: "5 min ago",
},
]);
// Fallback rules
const fallbackRules = ref([
{
id: 1,
name: "Email Provider Failover",
description: "Switch from SendGrid to Mailgun on failure",
primary: "SendGrid",
fallback: "Mailgun",
trigger: "API error or rate limit exceeded",
enabled: true,
},
{
id: 2,
name: "SMS Provider Failover",
description: "Switch from Twilio to Nexmo on failure",
primary: "Twilio",
fallback: "Nexmo",
trigger: "Delivery failure or timeout",
enabled: true,
},
{
id: 3,
name: "Email to SMS Fallback",
description: "Send SMS if email delivery fails",
primary: "Email Channel",
fallback: "SMS Channel",
trigger: "Hard bounce or 30s timeout",
enabled: false,
},
]);
// Form options
const channelOptions = [
{ label: "Email", value: "email" },
{ label: "SMS", value: "sms" },
{ label: "Push Notification", value: "push" },
{ label: "Webhook", value: "webhook" },
];
const emailProviderOptions = [
{ label: "Mailtrap", value: "mailtrap" },
{ label: "AWS SES", value: "aws-ses" },
];
const smsProviderOptions = [
{ label: "Twilio", value: "twilio" },
{ label: "Nexmo", value: "nexmo" },
{ label: "CM.com", value: "cm" },
];
// Methods
function addProvider(channel) {
selectedProvider.value = null;
providerForm.channel = channel;
providerForm.name = "";
providerForm.provider = "";
showProviderModal.value = true;
}
function configureProvider(provider) {
selectedProvider.value = provider;
// Populate form with provider data
providerForm.name = provider.name;
showProviderModal.value = true;
}
function saveProvider() {
console.log("Saving provider:", providerForm);
showProviderModal.value = false;
// Reset form
Object.keys(providerForm).forEach((key) => {
if (typeof providerForm[key] === "string") providerForm[key] = "";
if (typeof providerForm[key] === "number") providerForm[key] = 0;
});
}
function testProviderConnection() {
console.log("Testing provider connection...");
connectionTestResult.value = null;
// Simulate connection test
setTimeout(() => {
connectionTestResult.value = {
success: Math.random() > 0.3,
message:
Math.random() > 0.3
? "Connection successful"
: "Connection failed - Check credentials",
};
}, 2000);
}
function editFallbackRule(rule) {
console.log("Editing fallback rule:", rule);
// Implementation for editing fallback rules
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,822 @@
<template>
<div>
<LayoutsBreadcrumb />
<!-- Page Info Card -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-webhook"></Icon>
<h1 class="text-xl font-bold text-primary">Webhook Management</h1>
</div>
</template>
<template #body>
<p class="text-gray-600">
Configure and manage webhook endpoints for delivery status updates. Monitor webhook performance,
manage retry policies, and view delivery logs.
</p>
</template>
</rs-card>
<!-- Webhook 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 webhookStats"
: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 rounded-2xl transition-all duration-300"
:class="stat.bgColor"
>
<Icon class="text-3xl" :name="stat.icon" :class="stat.iconColor"></Icon>
</div>
<div class="flex-1 truncate">
<span class="block font-bold text-2xl 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>
<!-- Webhook Endpoints -->
<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-webhook"></Icon>
<h2 class="text-lg font-semibold text-primary">Webhook Endpoints</h2>
</div>
<rs-button variant="primary" @click="showAddWebhookModal = true">
<Icon class="mr-1" name="ic:outline-add"></Icon>
Add Webhook
</rs-button>
</div>
</template>
<template #body>
<div class="space-y-4">
<div
v-for="webhook in webhooks"
:key="webhook.id"
class="border border-gray-200 rounded-lg p-4"
>
<div class="flex items-start justify-between mb-4">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<h3 class="font-semibold text-lg">{{ webhook.name }}</h3>
<rs-badge :variant="webhook.enabled ? 'success' : 'secondary'" size="sm">
{{ webhook.enabled ? 'Active' : 'Disabled' }}
</rs-badge>
<rs-badge :variant="getHealthVariant(webhook.health)" size="sm">
{{ webhook.health }}
</rs-badge>
</div>
<div class="text-sm text-gray-600 mb-2">{{ webhook.description }}</div>
<div class="font-mono text-sm bg-gray-50 p-2 rounded">{{ webhook.url }}</div>
</div>
<div class="flex gap-2 ml-4">
<rs-button size="sm" variant="secondary-outline" @click="editWebhook(webhook)">
<Icon name="ic:outline-edit"></Icon>
</rs-button>
<rs-button size="sm" variant="secondary-outline" @click="testWebhook(webhook)">
<Icon name="ic:outline-send"></Icon>
</rs-button>
<rs-button size="sm" variant="danger-outline" @click="deleteWebhook(webhook)">
<Icon name="ic:outline-delete"></Icon>
</rs-button>
</div>
</div>
<!-- Webhook Details -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">Events</label>
<div class="flex flex-wrap gap-1">
<rs-badge
v-for="event in webhook.events"
:key="event"
variant="info"
size="xs"
>
{{ event }}
</rs-badge>
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">Success Rate</label>
<div class="font-semibold">{{ webhook.successRate }}%</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 mb-1">Avg Response Time</label>
<div class="font-semibold">{{ webhook.avgResponseTime }}ms</div>
</div>
</div>
<!-- Performance Chart -->
<div class="mb-4">
<label class="block text-xs font-medium text-gray-500 mb-2">Performance Trend (24h)</label>
<div class="w-full bg-gray-200 rounded-full h-2">
<div
class="h-2 rounded-full transition-all duration-300"
:class="getPerformanceClass(webhook.performance)"
:style="{ width: webhook.performance + '%' }"
></div>
</div>
<div class="text-xs text-gray-500 mt-1">{{ webhook.performance }}% performance score</div>
</div>
<!-- Recent Deliveries -->
<div>
<label class="block text-xs font-medium text-gray-500 mb-2">Recent Deliveries</label>
<div class="space-y-2">
<div
v-for="delivery in webhook.recentDeliveries"
:key="delivery.id"
class="flex items-center justify-between p-2 bg-gray-50 rounded text-sm"
>
<div class="flex items-center gap-2">
<Icon
:name="delivery.success ? 'ic:outline-check-circle' : 'ic:outline-error'"
:class="delivery.success ? 'text-green-500' : 'text-red-500'"
></Icon>
<span>{{ delivery.event }}</span>
<span class="text-gray-500">{{ delivery.timestamp }}</span>
</div>
<div class="flex items-center gap-2">
<span class="text-gray-500">{{ delivery.responseTime }}ms</span>
<span :class="delivery.success ? 'text-green-600' : 'text-red-600'">
{{ delivery.status }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Delivery Logs -->
<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-history"></Icon>
<h2 class="text-lg font-semibold text-primary">Delivery Logs</h2>
</div>
<div class="flex gap-2">
<rs-button size="sm" variant="secondary-outline" @click="refreshLogs">
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
Refresh
</rs-button>
<rs-button size="sm" variant="secondary-outline" @click="exportLogs">
<Icon class="mr-1" name="ic:outline-download"></Icon>
Export
</rs-button>
</div>
</div>
</template>
<template #body>
<!-- Filters -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<FormKit
type="select"
v-model="logFilters.webhook"
:options="webhookFilterOptions"
placeholder="All Webhooks"
label="Webhook"
/>
<FormKit
type="select"
v-model="logFilters.event"
:options="eventFilterOptions"
placeholder="All Events"
label="Event"
/>
<FormKit
type="select"
v-model="logFilters.status"
:options="statusFilterOptions"
placeholder="All Statuses"
label="Status"
/>
<FormKit
type="date"
v-model="logFilters.date"
label="Date"
/>
</div>
<!-- Logs Table -->
<rs-table
:field="logTableFields"
:data="filteredLogs"
:advanced="true"
:options="{ striped: true, hover: true }"
:optionsAdvanced="{
sortable: true,
filterable: true,
responsive: true,
}"
:pageSize="20"
>
<template #webhook="{ row }">
<div class="font-medium">{{ row.webhookName }}</div>
<div class="text-xs text-gray-500">{{ row.url }}</div>
</template>
<template #event="{ row }">
<rs-badge variant="info" size="sm">
{{ row.event }}
</rs-badge>
</template>
<template #status="{ row }">
<rs-badge :variant="row.success ? 'success' : 'danger'" size="sm">
{{ row.status }}
</rs-badge>
</template>
<template #responseTime="{ row }">
<span :class="row.responseTime > 1000 ? 'text-red-600' : 'text-green-600'">
{{ row.responseTime }}ms
</span>
</template>
<template #actions="{ row }">
<div class="flex gap-2">
<rs-button
size="sm"
variant="primary-outline"
@click="viewLogDetails(row)"
>
<Icon name="ic:outline-visibility"></Icon>
</rs-button>
<rs-button
size="sm"
variant="secondary-outline"
@click="retryWebhook(row)"
v-if="!row.success"
>
<Icon name="ic:outline-refresh"></Icon>
</rs-button>
</div>
</template>
</rs-table>
</template>
</rs-card>
<!-- Add/Edit Webhook Modal -->
<rs-modal v-model="showAddWebhookModal" size="lg">
<template #header>
<h3 class="text-lg font-semibold">{{ editingWebhook ? 'Edit' : 'Add' }} Webhook</h3>
</template>
<template #body>
<div class="space-y-6">
<!-- Basic Information -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormKit
type="text"
v-model="webhookForm.name"
label="Webhook Name"
placeholder="Enter webhook name"
validation="required"
/>
<FormKit
type="url"
v-model="webhookForm.url"
label="Endpoint URL"
placeholder="https://api.example.com/webhook"
validation="required|url"
/>
</div>
<FormKit
type="textarea"
v-model="webhookForm.description"
label="Description"
placeholder="Describe what this webhook is used for"
rows="2"
/>
<!-- Events Selection -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-3">Events to Subscribe</label>
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
<div
v-for="event in availableEvents"
:key="event.value"
class="flex items-center"
>
<FormKit
type="checkbox"
v-model="webhookForm.events"
:value="event.value"
:label="event.label"
/>
</div>
</div>
</div>
<!-- Security -->
<div>
<h4 class="font-semibold mb-3">Security Settings</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormKit
type="password"
v-model="webhookForm.secret"
label="Secret Key"
placeholder="Optional secret for signature verification"
help="Used to sign webhook payloads for verification"
/>
<FormKit
type="select"
v-model="webhookForm.authType"
label="Authentication Type"
:options="authTypeOptions"
/>
</div>
<div v-if="webhookForm.authType === 'bearer'" class="mt-4">
<FormKit
type="password"
v-model="webhookForm.bearerToken"
label="Bearer Token"
placeholder="Enter bearer token"
/>
</div>
<div v-if="webhookForm.authType === 'basic'" class="grid grid-cols-2 gap-4 mt-4">
<FormKit
type="text"
v-model="webhookForm.username"
label="Username"
placeholder="Enter username"
/>
<FormKit
type="password"
v-model="webhookForm.password"
label="Password"
placeholder="Enter password"
/>
</div>
</div>
<!-- Retry Settings -->
<div>
<h4 class="font-semibold mb-3">Retry Settings</h4>
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
<FormKit
type="number"
v-model="webhookForm.maxRetries"
label="Max Retries"
placeholder="3"
min="0"
max="10"
/>
<FormKit
type="number"
v-model="webhookForm.retryDelay"
label="Retry Delay (seconds)"
placeholder="60"
min="1"
max="3600"
/>
<FormKit
type="number"
v-model="webhookForm.timeout"
label="Timeout (seconds)"
placeholder="30"
min="1"
max="300"
/>
</div>
</div>
<!-- Test Webhook -->
<div class="border border-gray-200 rounded-lg p-4">
<h4 class="font-semibold mb-3">Test Webhook</h4>
<div class="flex items-center gap-3">
<rs-button variant="secondary" @click="testWebhookEndpoint">
<Icon class="mr-1" name="ic:outline-send"></Icon>
Send Test
</rs-button>
<div v-if="testResult" class="flex items-center gap-2">
<Icon
:name="testResult.success ? 'ic:outline-check-circle' : 'ic:outline-error'"
:class="testResult.success ? 'text-green-500' : 'text-red-500'"
></Icon>
<span :class="testResult.success ? 'text-green-600' : 'text-red-600'">
{{ testResult.message }}
</span>
</div>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<rs-button variant="outline" @click="showAddWebhookModal = false">Cancel</rs-button>
<rs-button variant="primary" @click="saveWebhook">
{{ editingWebhook ? 'Update' : 'Create' }} Webhook
</rs-button>
</div>
</template>
</rs-modal>
<!-- Log Details Modal -->
<rs-modal v-model="showLogModal" size="lg">
<template #header>
<h3 class="text-lg font-semibold">Webhook Delivery Details</h3>
</template>
<template #body>
<div v-if="selectedLog" class="space-y-6">
<!-- Basic Info -->
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-500 mb-1">Webhook</label>
<div class="font-medium">{{ selectedLog.webhookName }}</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-500 mb-1">Event</label>
<rs-badge variant="info">{{ selectedLog.event }}</rs-badge>
</div>
<div>
<label class="block text-sm font-medium text-gray-500 mb-1">Status</label>
<rs-badge :variant="selectedLog.success ? 'success' : 'danger'">
{{ selectedLog.status }}
</rs-badge>
</div>
<div>
<label class="block text-sm font-medium text-gray-500 mb-1">Response Time</label>
<div class="font-medium">{{ selectedLog.responseTime }}ms</div>
</div>
</div>
<!-- Request Details -->
<div>
<label class="block text-sm font-medium text-gray-500 mb-1">Request Payload</label>
<div class="bg-gray-50 rounded-lg p-3">
<pre class="text-xs overflow-x-auto">{{ JSON.stringify(selectedLog.payload, null, 2) }}</pre>
</div>
</div>
<!-- Response Details -->
<div>
<label class="block text-sm font-medium text-gray-500 mb-1">Response</label>
<div class="bg-gray-50 rounded-lg p-3">
<div class="text-sm mb-2">
<strong>Status Code:</strong> {{ selectedLog.responseCode }}
</div>
<div class="text-sm mb-2">
<strong>Headers:</strong>
</div>
<pre class="text-xs overflow-x-auto mb-2">{{ JSON.stringify(selectedLog.responseHeaders, null, 2) }}</pre>
<div class="text-sm mb-2">
<strong>Body:</strong>
</div>
<pre class="text-xs overflow-x-auto">{{ selectedLog.responseBody }}</pre>
</div>
</div>
<!-- Error Details (if any) -->
<div v-if="selectedLog.error">
<label class="block text-sm font-medium text-gray-500 mb-1">Error Details</label>
<div class="bg-red-50 border border-red-200 rounded-lg p-3">
<div class="text-sm text-red-800">{{ selectedLog.error }}</div>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<rs-button variant="outline" @click="showLogModal = false">Close</rs-button>
<rs-button
variant="primary"
@click="retryWebhook(selectedLog)"
v-if="!selectedLog?.success"
>
Retry Delivery
</rs-button>
</div>
</template>
</rs-modal>
</div>
</template>
<script setup>
definePageMeta({
title: "Webhook Management",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Dashboard",
path: "/dashboard",
},
{
name: "Notification",
path: "/notification",
},
{
name: "Delivery Engine",
path: "/notification/delivery",
},
{
name: "Webhooks",
path: "/notification/delivery/webhooks",
type: "current",
},
],
});
import { ref, reactive, computed } from "vue";
// Modal states
const showAddWebhookModal = ref(false);
const showLogModal = ref(false);
const editingWebhook = ref(null);
const selectedLog = ref(null);
const testResult = ref(null);
// Form data
const webhookForm = reactive({
name: "",
url: "",
description: "",
events: [],
secret: "",
authType: "none",
bearerToken: "",
username: "",
password: "",
maxRetries: 3,
retryDelay: 60,
timeout: 30,
enabled: true,
});
// Filter states
const logFilters = reactive({
webhook: "",
event: "",
status: "",
date: "",
});
// Statistics
const webhookStats = ref([
{
title: "Active Webhooks",
value: "8",
icon: "ic:outline-webhook",
bgColor: "bg-blue-100",
iconColor: "text-blue-600",
textColor: "text-blue-600",
},
{
title: "Deliveries Today",
value: "1.2K",
icon: "ic:outline-send",
bgColor: "bg-green-100",
iconColor: "text-green-600",
textColor: "text-green-600",
},
{
title: "Success Rate",
value: "98.5%",
icon: "ic:outline-trending-up",
bgColor: "bg-purple-100",
iconColor: "text-purple-600",
textColor: "text-purple-600",
},
{
title: "Failed Deliveries",
value: "18",
icon: "ic:outline-error",
bgColor: "bg-red-100",
iconColor: "text-red-600",
textColor: "text-red-600",
},
]);
// Webhook data
const webhooks = ref([
{
id: 1,
name: "CRM Integration",
description: "Delivery status updates for CRM system",
url: "https://api.crm.com/webhooks/delivery",
enabled: true,
health: "Healthy",
events: ["delivered", "opened", "bounced"],
successRate: 99.2,
avgResponseTime: 150,
performance: 95,
recentDeliveries: [
{ id: 1, event: "delivered", timestamp: "2 min ago", success: true, status: "200", responseTime: 140 },
{ id: 2, event: "opened", timestamp: "5 min ago", success: true, status: "200", responseTime: 160 },
{ id: 3, event: "bounced", timestamp: "8 min ago", success: false, status: "500", responseTime: 0 },
],
},
{
id: 2,
name: "Analytics Platform",
description: "Send delivery metrics to analytics dashboard",
url: "https://analytics.example.com/webhook",
enabled: true,
health: "Warning",
events: ["sent", "delivered", "failed"],
successRate: 87.5,
avgResponseTime: 2100,
performance: 78,
recentDeliveries: [
{ id: 4, event: "sent", timestamp: "1 min ago", success: true, status: "200", responseTime: 1900 },
{ id: 5, event: "delivered", timestamp: "3 min ago", success: false, status: "timeout", responseTime: 0 },
{ id: 6, event: "failed", timestamp: "6 min ago", success: true, status: "200", responseTime: 2300 },
],
},
]);
// Delivery logs
const deliveryLogs = ref([
{
id: 1,
webhookId: 1,
webhookName: "CRM Integration",
url: "https://api.crm.com/webhooks/delivery",
event: "delivered",
success: true,
status: "200 OK",
responseTime: 140,
timestamp: "2024-01-15 10:30:00",
payload: {
messageId: "msg_001",
event: "delivered",
timestamp: "2024-01-15T10:30:00Z",
channel: "email",
recipient: "user@example.com"
},
responseCode: 200,
responseHeaders: { "content-type": "application/json" },
responseBody: '{"status": "received"}',
error: null,
},
// Add more logs...
]);
// Form options
const availableEvents = [
{ label: "Message Queued", value: "queued" },
{ label: "Message Sent", value: "sent" },
{ label: "Message Delivered", value: "delivered" },
{ label: "Message Opened", value: "opened" },
{ label: "Message Failed", value: "failed" },
{ label: "Message Bounced", value: "bounced" },
];
const authTypeOptions = [
{ label: "None", value: "none" },
{ label: "Bearer Token", value: "bearer" },
{ label: "Basic Auth", value: "basic" },
];
// Filter options
const webhookFilterOptions = computed(() => [
{ label: "All Webhooks", value: "" },
...webhooks.value.map(w => ({ label: w.name, value: w.id }))
]);
const eventFilterOptions = [
{ label: "All Events", value: "" },
...availableEvents,
];
const statusFilterOptions = [
{ label: "All Statuses", value: "" },
{ label: "Success", value: "success" },
{ label: "Failed", value: "failed" },
];
// Table configuration
const logTableFields = ref([
{ key: "timestamp", label: "Timestamp", sortable: true },
{ key: "webhook", label: "Webhook", sortable: true },
{ key: "event", label: "Event", sortable: true },
{ key: "status", label: "Status", sortable: true },
{ key: "responseTime", label: "Response Time", sortable: true },
{ key: "actions", label: "Actions", sortable: false },
]);
// Computed
const filteredLogs = computed(() => {
let filtered = deliveryLogs.value;
if (logFilters.webhook) {
filtered = filtered.filter(log => log.webhookId === logFilters.webhook);
}
if (logFilters.event) {
filtered = filtered.filter(log => log.event === logFilters.event);
}
if (logFilters.status) {
const isSuccess = logFilters.status === "success";
filtered = filtered.filter(log => log.success === isSuccess);
}
return filtered;
});
// Methods
function getHealthVariant(health) {
const variants = {
"Healthy": "success",
"Warning": "warning",
"Critical": "danger",
};
return variants[health] || "secondary";
}
function getPerformanceClass(performance) {
if (performance >= 90) return "bg-green-500";
if (performance >= 70) return "bg-yellow-500";
return "bg-red-500";
}
function editWebhook(webhook) {
editingWebhook.value = webhook;
// Populate form with webhook data
Object.keys(webhookForm).forEach(key => {
if (webhook[key] !== undefined) {
webhookForm[key] = webhook[key];
}
});
showAddWebhookModal.value = true;
}
function testWebhook(webhook) {
console.log("Testing webhook:", webhook.name);
// Implementation for testing webhook
}
function deleteWebhook(webhook) {
console.log("Deleting webhook:", webhook.name);
// Implementation for deleting webhook
}
function saveWebhook() {
console.log("Saving webhook:", webhookForm);
showAddWebhookModal.value = false;
// Reset form
Object.keys(webhookForm).forEach(key => {
if (typeof webhookForm[key] === 'string') webhookForm[key] = '';
if (typeof webhookForm[key] === 'number') webhookForm[key] = 0;
if (Array.isArray(webhookForm[key])) webhookForm[key] = [];
if (typeof webhookForm[key] === 'boolean') webhookForm[key] = true;
});
}
function testWebhookEndpoint() {
console.log("Testing webhook endpoint...");
testResult.value = null;
// Simulate test
setTimeout(() => {
testResult.value = {
success: Math.random() > 0.3,
message: Math.random() > 0.3 ? "Test successful" : "Connection failed - Check URL and credentials"
};
}, 2000);
}
function viewLogDetails(log) {
selectedLog.value = log;
showLogModal.value = true;
}
function retryWebhook(log) {
console.log("Retrying webhook delivery:", log);
// Implementation for retrying webhook delivery
}
function refreshLogs() {
console.log("Refreshing logs...");
// Implementation for refreshing logs
}
function exportLogs() {
console.log("Exporting logs...");
// Implementation for exporting logs
}
</script>
<style lang="scss" scoped></style>