Add notification delivery management pages, including Delivery Monitor, Provider Management, and Webhook Management. Implement real-time metrics, message tracking, and provider configuration features. Enhance UI with detailed statistics, performance tracking, and delivery logs for improved user experience.
This commit is contained in:
parent
1dc8f4cf7a
commit
be224f7508
@ -1,7 +1,955 @@
|
|||||||
<template>
|
<template>
|
||||||
<div></div>
|
<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">Notification Delivery Engine</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
The heart of the system responsible for reliably delivering notifications to
|
||||||
|
users across multiple channels. Features intelligent routing, fault-tolerance,
|
||||||
|
real-time tracking, and deep observability.
|
||||||
|
</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 deliveryStats"
|
||||||
|
: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>
|
||||||
|
|
||||||
|
<!-- Main Features Grid -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
|
<!-- Multi-Channel Delivery -->
|
||||||
|
<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-alt-route"></Icon>
|
||||||
|
<h2 class="text-lg font-semibold text-primary">Multi-Channel Delivery</h2>
|
||||||
|
</div>
|
||||||
|
<rs-button
|
||||||
|
size="sm"
|
||||||
|
variant="primary-outline"
|
||||||
|
@click="showChannelModal = true"
|
||||||
|
>
|
||||||
|
Configure
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div
|
||||||
|
v-for="channel in channels"
|
||||||
|
:key="channel.type"
|
||||||
|
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="channel.icon"></Icon>
|
||||||
|
<span class="font-semibold">{{ channel.name }}</span>
|
||||||
|
</div>
|
||||||
|
<rs-badge
|
||||||
|
:variant="channel.status === 'active' ? 'success' : 'secondary'"
|
||||||
|
>
|
||||||
|
{{ channel.status }}
|
||||||
|
</rs-badge>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-600">Provider:</span>
|
||||||
|
<span class="ml-2 font-medium">{{ channel.provider }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-600">Success Rate:</span>
|
||||||
|
<span class="ml-2 font-medium">{{ channel.successRate }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Real-time Status Tracking -->
|
||||||
|
<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-track-changes"></Icon>
|
||||||
|
<h2 class="text-lg font-semibold text-primary">
|
||||||
|
Real-time Status Tracking
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<rs-button size="sm" variant="primary-outline" @click="refreshStatusData">
|
||||||
|
Refresh
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="status in deliveryStatuses"
|
||||||
|
:key="status.stage"
|
||||||
|
class="flex items-center justify-between p-3 border border-gray-200 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon class="mr-2" :name="status.icon" :class="status.color"></Icon>
|
||||||
|
<span class="font-medium">{{ status.stage }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<div class="font-bold text-lg">{{ status.count }}</div>
|
||||||
|
<div class="text-xs text-gray-500">{{ status.percentage }}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Fallback Mechanisms -->
|
||||||
|
<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-route"></Icon>
|
||||||
|
<h2 class="text-lg font-semibold text-primary">Fallback Mechanisms</h2>
|
||||||
|
</div>
|
||||||
|
<rs-button
|
||||||
|
size="sm"
|
||||||
|
variant="primary-outline"
|
||||||
|
@click="showFallbackModal = true"
|
||||||
|
>
|
||||||
|
Manage
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div
|
||||||
|
v-for="fallback in fallbackRules"
|
||||||
|
:key="fallback.id"
|
||||||
|
class="border border-gray-200 rounded-lg p-4"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="font-semibold">{{ fallback.name }}</span>
|
||||||
|
<rs-badge :variant="fallback.enabled ? 'success' : 'secondary'">
|
||||||
|
{{ fallback.enabled ? "Active" : "Disabled" }}
|
||||||
|
</rs-badge>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600">
|
||||||
|
<div>Primary: {{ fallback.primary }}</div>
|
||||||
|
<div>Fallback: {{ fallback.fallback }}</div>
|
||||||
|
<div>Trigger: {{ fallback.trigger }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Delivery Optimization -->
|
||||||
|
<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-tune"></Icon>
|
||||||
|
<h2 class="text-lg font-semibold text-primary">Delivery Optimization</h2>
|
||||||
|
</div>
|
||||||
|
<rs-button
|
||||||
|
size="sm"
|
||||||
|
variant="primary-outline"
|
||||||
|
@click="showOptimizationModal = true"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="border border-gray-200 rounded-lg p-4">
|
||||||
|
<h4 class="font-semibold mb-2">Smart Routing</h4>
|
||||||
|
<div class="text-sm text-gray-600 space-y-1">
|
||||||
|
<div>Best performing provider per region</div>
|
||||||
|
<div>Dynamic route selection based on success rates</div>
|
||||||
|
<div>Geographic optimization enabled</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="border border-gray-200 rounded-lg p-4">
|
||||||
|
<h4 class="font-semibold mb-2">Timing Optimization</h4>
|
||||||
|
<div class="text-sm text-gray-600 space-y-1">
|
||||||
|
<div>Off-peak scheduling for non-critical messages</div>
|
||||||
|
<div>Timezone-aware delivery</div>
|
||||||
|
<div>Quiet hours enforcement</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="border border-gray-200 rounded-lg p-4">
|
||||||
|
<h4 class="font-semibold mb-2">Rate Limiting</h4>
|
||||||
|
<div class="text-sm text-gray-600 space-y-1">
|
||||||
|
<div>Bulk traffic throttling active</div>
|
||||||
|
<div>Provider quota management</div>
|
||||||
|
<div>Anti-blacklisting protection</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bounce Handling & Recent Activity -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
|
<!-- Bounce Handling -->
|
||||||
|
<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-bounce-rate"></Icon>
|
||||||
|
<h2 class="text-lg font-semibold text-primary">Bounce Handling</h2>
|
||||||
|
</div>
|
||||||
|
<rs-button
|
||||||
|
size="sm"
|
||||||
|
variant="primary-outline"
|
||||||
|
@click="showBounceModal = true"
|
||||||
|
>
|
||||||
|
View All
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||||
|
<div class="text-center p-3 bg-red-50 rounded-lg">
|
||||||
|
<div class="font-bold text-2xl text-red-600">{{ bounceStats.hard }}</div>
|
||||||
|
<div class="text-sm text-red-600">Hard Bounces</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-3 bg-yellow-50 rounded-lg">
|
||||||
|
<div class="font-bold text-2xl text-yellow-600">
|
||||||
|
{{ bounceStats.soft }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-yellow-600">Soft Bounces</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="bounce in recentBounces"
|
||||||
|
:key="bounce.id"
|
||||||
|
class="flex items-center justify-between p-2 border border-gray-200 rounded"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon
|
||||||
|
class="mr-2"
|
||||||
|
:name="
|
||||||
|
bounce.type === 'hard' ? 'ic:outline-error' : 'ic:outline-warning'
|
||||||
|
"
|
||||||
|
:class="bounce.type === 'hard' ? 'text-red-500' : 'text-yellow-500'"
|
||||||
|
></Icon>
|
||||||
|
<div>
|
||||||
|
<div class="font-medium text-sm">{{ bounce.recipient }}</div>
|
||||||
|
<div class="text-xs text-gray-500">{{ bounce.reason }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500">{{ bounce.time }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Recent Delivery 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-history"></Icon>
|
||||||
|
<h2 class="text-lg font-semibold text-primary">Recent Activity</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="activity in recentActivity"
|
||||||
|
:key="activity.id"
|
||||||
|
class="flex items-center p-3 border border-gray-200 rounded-lg"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
class="mr-3"
|
||||||
|
:name="activity.icon"
|
||||||
|
:class="activity.statusColor"
|
||||||
|
></Icon>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="font-medium text-sm">{{ activity.message }}</div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{{ activity.channel }} • {{ activity.time }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<rs-badge :variant="activity.badgeVariant" size="sm">
|
||||||
|
{{ activity.status }}
|
||||||
|
</rs-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Third-Party Integrations & Webhooks -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
|
<!-- Third-Party Integrations -->
|
||||||
|
<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-extension"></Icon>
|
||||||
|
<h2 class="text-lg font-semibold text-primary">Third-Party Integrations</h2>
|
||||||
|
</div>
|
||||||
|
<rs-button
|
||||||
|
size="sm"
|
||||||
|
variant="primary-outline"
|
||||||
|
@click="showIntegrationsModal = true"
|
||||||
|
>
|
||||||
|
Manage
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div
|
||||||
|
v-for="integration in integrations"
|
||||||
|
:key="integration.id"
|
||||||
|
class="flex items-center justify-between p-4 border border-gray-200 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon class="mr-3 text-2xl" :name="integration.icon"></Icon>
|
||||||
|
<div>
|
||||||
|
<div class="font-semibold">{{ integration.name }}</div>
|
||||||
|
<div class="text-sm text-gray-600">{{ integration.description }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<rs-badge
|
||||||
|
:variant="integration.status === 'connected' ? 'success' : 'danger'"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{{ integration.status }}
|
||||||
|
</rs-badge>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">{{ integration.lastSync }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Webhook 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-webhook"></Icon>
|
||||||
|
<h2 class="text-lg font-semibold text-primary">Webhook Configuration</h2>
|
||||||
|
</div>
|
||||||
|
<rs-button
|
||||||
|
size="sm"
|
||||||
|
variant="primary-outline"
|
||||||
|
@click="showWebhookModal = true"
|
||||||
|
>
|
||||||
|
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-center justify-between mb-2">
|
||||||
|
<div class="font-semibold">{{ webhook.name }}</div>
|
||||||
|
<rs-badge :variant="webhook.enabled ? 'success' : 'secondary'" size="sm">
|
||||||
|
{{ webhook.enabled ? "Active" : "Disabled" }}
|
||||||
|
</rs-badge>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 space-y-1">
|
||||||
|
<div>URL: {{ webhook.url }}</div>
|
||||||
|
<div>Events: {{ webhook.events.join(", ") }}</div>
|
||||||
|
<div>Last delivery: {{ webhook.lastDelivery }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delivery Reports -->
|
||||||
|
<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-assessment"></Icon>
|
||||||
|
<h2 class="text-lg font-semibold text-primary">Delivery Reports</h2>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<rs-button size="sm" variant="primary-outline" @click="exportReports">
|
||||||
|
Export
|
||||||
|
</rs-button>
|
||||||
|
<rs-button
|
||||||
|
size="sm"
|
||||||
|
variant="primary-outline"
|
||||||
|
@click="showReportModal = true"
|
||||||
|
>
|
||||||
|
Generate Report
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
<div class="text-center p-4 bg-blue-50 rounded-lg">
|
||||||
|
<div class="font-bold text-3xl text-blue-600">{{ reports.totalSent }}</div>
|
||||||
|
<div class="text-blue-600 font-medium">Total Messages Sent</div>
|
||||||
|
<div class="text-sm text-gray-500 mt-1">Last 24 hours</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-4 bg-green-50 rounded-lg">
|
||||||
|
<div class="font-bold text-3xl text-green-600">
|
||||||
|
{{ reports.successRate }}%
|
||||||
|
</div>
|
||||||
|
<div class="text-green-600 font-medium">Overall Success Rate</div>
|
||||||
|
<div class="text-sm text-gray-500 mt-1">7-day average</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center p-4 bg-purple-50 rounded-lg">
|
||||||
|
<div class="font-bold text-3xl text-purple-600">
|
||||||
|
{{ reports.avgDeliveryTime }}s
|
||||||
|
</div>
|
||||||
|
<div class="text-purple-600 font-medium">Avg Delivery Time</div>
|
||||||
|
<div class="text-sm text-gray-500 mt-1">End-to-end</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<h4 class="font-semibold mb-4">Channel Performance</h4>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="channelReport in channelReports"
|
||||||
|
:key="channelReport.channel"
|
||||||
|
class="flex items-center justify-between p-3 border border-gray-200 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon class="mr-3" :name="channelReport.icon"></Icon>
|
||||||
|
<span class="font-medium">{{ channelReport.channel }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-6 text-sm">
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-600">Sent:</span>
|
||||||
|
<span class="ml-1 font-medium">{{ channelReport.sent }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-600">Success:</span>
|
||||||
|
<span class="ml-1 font-medium">{{ channelReport.successRate }}%</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-600">Avg Time:</span>
|
||||||
|
<span class="ml-1 font-medium">{{ channelReport.avgTime }}s</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Modals would go here - Channel Config, Fallback Rules, etc. -->
|
||||||
|
<!-- For brevity, showing one example modal -->
|
||||||
|
|
||||||
|
<!-- Channel Configuration Modal -->
|
||||||
|
<rs-modal v-model="showChannelModal" size="lg">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-lg font-semibold">Channel Configuration</h3>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div
|
||||||
|
v-for="channel in channels"
|
||||||
|
:key="channel.type"
|
||||||
|
class="border border-gray-200 rounded-lg p-4"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon class="mr-2" :name="channel.icon"></Icon>
|
||||||
|
<span class="font-semibold">{{ channel.name }}</span>
|
||||||
|
</div>
|
||||||
|
<FormKit type="toggle" v-model="channel.enabled" name="enabled" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Primary Provider"
|
||||||
|
:value="channel.provider"
|
||||||
|
:options="channel.providerOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<FormKit
|
||||||
|
type="select"
|
||||||
|
label="Fallback Provider"
|
||||||
|
:value="channel.fallbackProvider"
|
||||||
|
:options="channel.providerOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<rs-button variant="outline" @click="showChannelModal = false"
|
||||||
|
>Cancel</rs-button
|
||||||
|
>
|
||||||
|
<rs-button variant="primary" @click="saveChannelConfig"
|
||||||
|
>Save Configuration</rs-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-modal>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup></script>
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
title: "Notification Delivery Engine",
|
||||||
|
middleware: ["auth"],
|
||||||
|
requiresAuth: true,
|
||||||
|
breadcrumb: [
|
||||||
|
{
|
||||||
|
name: "Dashboard",
|
||||||
|
path: "/dashboard",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Notification",
|
||||||
|
path: "/notification",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Delivery Engine",
|
||||||
|
path: "/notification/delivery",
|
||||||
|
type: "current",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modal states
|
||||||
|
const showChannelModal = ref(false);
|
||||||
|
const showFallbackModal = ref(false);
|
||||||
|
const showOptimizationModal = ref(false);
|
||||||
|
const showBounceModal = ref(false);
|
||||||
|
const showIntegrationsModal = ref(false);
|
||||||
|
const showWebhookModal = ref(false);
|
||||||
|
const showReportModal = ref(false);
|
||||||
|
|
||||||
|
// Quick delivery stats
|
||||||
|
const deliveryStats = ref([
|
||||||
|
{
|
||||||
|
title: "Messages/Hour",
|
||||||
|
value: "15.2K",
|
||||||
|
icon: "ic:outline-speed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Success Rate",
|
||||||
|
value: "98.7%",
|
||||||
|
icon: "ic:outline-check-circle",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Active Channels",
|
||||||
|
value: "4",
|
||||||
|
icon: "ic:outline-alt-route",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Avg Delivery Time",
|
||||||
|
value: "1.2s",
|
||||||
|
icon: "ic:outline-timer",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Multi-channel delivery data
|
||||||
|
const channels = ref([
|
||||||
|
{
|
||||||
|
type: "email",
|
||||||
|
name: "Email",
|
||||||
|
icon: "ic:outline-email",
|
||||||
|
status: "active",
|
||||||
|
provider: "SendGrid",
|
||||||
|
fallbackProvider: "Mailgun",
|
||||||
|
successRate: 99.2,
|
||||||
|
enabled: true,
|
||||||
|
providerOptions: [
|
||||||
|
{ label: "SendGrid", value: "sendgrid" },
|
||||||
|
{ label: "Mailgun", value: "mailgun" },
|
||||||
|
{ label: "AWS SES", value: "ses" },
|
||||||
|
{ label: "Postmark", value: "postmark" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "sms",
|
||||||
|
name: "SMS",
|
||||||
|
icon: "ic:outline-sms",
|
||||||
|
status: "active",
|
||||||
|
provider: "Twilio",
|
||||||
|
fallbackProvider: "Nexmo",
|
||||||
|
successRate: 97.8,
|
||||||
|
enabled: true,
|
||||||
|
providerOptions: [
|
||||||
|
{ label: "Twilio", value: "twilio" },
|
||||||
|
{ label: "Nexmo", value: "nexmo" },
|
||||||
|
{ label: "CM.com", value: "cm" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "push",
|
||||||
|
name: "Push Notifications",
|
||||||
|
icon: "ic:outline-notifications",
|
||||||
|
status: "active",
|
||||||
|
provider: "FCM",
|
||||||
|
fallbackProvider: "APNs",
|
||||||
|
successRate: 95.4,
|
||||||
|
enabled: true,
|
||||||
|
providerOptions: [
|
||||||
|
{ label: "Firebase FCM", value: "fcm" },
|
||||||
|
{ label: "Apple APNs", value: "apns" },
|
||||||
|
{ label: "OneSignal", value: "onesignal" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "inapp",
|
||||||
|
name: "In-App",
|
||||||
|
icon: "ic:outline-app-registration",
|
||||||
|
status: "active",
|
||||||
|
provider: "WebSocket",
|
||||||
|
fallbackProvider: "Polling",
|
||||||
|
successRate: 99.9,
|
||||||
|
enabled: true,
|
||||||
|
providerOptions: [
|
||||||
|
{ label: "WebSocket", value: "websocket" },
|
||||||
|
{ label: "Server-Sent Events", value: "sse" },
|
||||||
|
{ label: "Polling", value: "polling" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Real-time delivery status tracking
|
||||||
|
const deliveryStatuses = ref([
|
||||||
|
{
|
||||||
|
stage: "Queued",
|
||||||
|
count: 1247,
|
||||||
|
percentage: 5.2,
|
||||||
|
icon: "ic:outline-queue",
|
||||||
|
color: "text-blue-500",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stage: "Sent",
|
||||||
|
count: 18563,
|
||||||
|
percentage: 77.8,
|
||||||
|
icon: "ic:outline-send",
|
||||||
|
color: "text-yellow-500",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stage: "Delivered",
|
||||||
|
count: 17821,
|
||||||
|
percentage: 74.7,
|
||||||
|
icon: "ic:outline-check-circle",
|
||||||
|
color: "text-green-500",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stage: "Opened",
|
||||||
|
count: 12456,
|
||||||
|
percentage: 52.2,
|
||||||
|
icon: "ic:outline-mark-email-read",
|
||||||
|
color: "text-purple-500",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stage: "Failed",
|
||||||
|
count: 324,
|
||||||
|
percentage: 1.4,
|
||||||
|
icon: "ic:outline-error",
|
||||||
|
color: "text-red-500",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stage: "Bounced",
|
||||||
|
count: 187,
|
||||||
|
percentage: 0.8,
|
||||||
|
icon: "ic:outline-reply",
|
||||||
|
color: "text-orange-500",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Fallback mechanism rules
|
||||||
|
const fallbackRules = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "Email to SMS Fallback",
|
||||||
|
primary: "Email (SendGrid)",
|
||||||
|
fallback: "SMS (Twilio)",
|
||||||
|
trigger: "Hard bounce or 30s timeout",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Provider Failover",
|
||||||
|
primary: "SendGrid",
|
||||||
|
fallback: "Mailgun",
|
||||||
|
trigger: "API error or rate limit",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "Push to In-App",
|
||||||
|
primary: "Push Notification",
|
||||||
|
fallback: "In-App Notification",
|
||||||
|
trigger: "Device offline",
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "SMS Regional Failover",
|
||||||
|
primary: "Twilio",
|
||||||
|
fallback: "Local Carrier",
|
||||||
|
trigger: "Region-specific delivery failure",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Bounce handling statistics
|
||||||
|
const bounceStats = ref({
|
||||||
|
hard: 47,
|
||||||
|
soft: 128,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recent bounces
|
||||||
|
const recentBounces = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
recipient: "user@invalid.com",
|
||||||
|
type: "hard",
|
||||||
|
reason: "Domain not found",
|
||||||
|
time: "2 min ago",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
recipient: "+1234567890",
|
||||||
|
type: "soft",
|
||||||
|
reason: "Temporary network issue",
|
||||||
|
time: "5 min ago",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
recipient: "blocked@example.com",
|
||||||
|
type: "hard",
|
||||||
|
reason: "Recipient blocked sender",
|
||||||
|
time: "8 min ago",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Recent delivery activity
|
||||||
|
const recentActivity = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
message: "Bulk email campaign completed",
|
||||||
|
channel: "Email",
|
||||||
|
status: "Success",
|
||||||
|
badgeVariant: "success",
|
||||||
|
icon: "ic:outline-email",
|
||||||
|
statusColor: "text-green-500",
|
||||||
|
time: "1 min ago",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
message: "SMS OTP batch processing",
|
||||||
|
channel: "SMS",
|
||||||
|
status: "Processing",
|
||||||
|
badgeVariant: "warning",
|
||||||
|
icon: "ic:outline-sms",
|
||||||
|
statusColor: "text-yellow-500",
|
||||||
|
time: "3 min ago",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
message: "Push notification sent to 1.2K devices",
|
||||||
|
channel: "Push",
|
||||||
|
status: "Delivered",
|
||||||
|
badgeVariant: "success",
|
||||||
|
icon: "ic:outline-notifications",
|
||||||
|
statusColor: "text-green-500",
|
||||||
|
time: "5 min ago",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
message: "Webhook delivery failed",
|
||||||
|
channel: "Webhook",
|
||||||
|
status: "Failed",
|
||||||
|
badgeVariant: "danger",
|
||||||
|
icon: "ic:outline-webhook",
|
||||||
|
statusColor: "text-red-500",
|
||||||
|
time: "7 min ago",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Third-party integrations
|
||||||
|
const integrations = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "SendGrid",
|
||||||
|
description: "Email delivery service",
|
||||||
|
status: "connected",
|
||||||
|
icon: "ic:outline-email",
|
||||||
|
lastSync: "2 min ago",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Twilio",
|
||||||
|
description: "SMS & Voice API",
|
||||||
|
status: "connected",
|
||||||
|
icon: "ic:outline-sms",
|
||||||
|
lastSync: "5 min ago",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "Firebase FCM",
|
||||||
|
description: "Push notifications",
|
||||||
|
status: "connected",
|
||||||
|
icon: "ic:outline-notifications",
|
||||||
|
lastSync: "1 min ago",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "Slack",
|
||||||
|
description: "Team notifications",
|
||||||
|
status: "disconnected",
|
||||||
|
icon: "ic:outline-chat",
|
||||||
|
lastSync: "Never",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Webhook configurations
|
||||||
|
const webhooks = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "CRM Integration",
|
||||||
|
url: "https://api.crm.com/webhooks/delivery",
|
||||||
|
events: ["delivered", "opened", "bounced"],
|
||||||
|
enabled: true,
|
||||||
|
lastDelivery: "2 min ago",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Analytics Platform",
|
||||||
|
url: "https://analytics.example.com/webhook",
|
||||||
|
events: ["sent", "delivered", "failed"],
|
||||||
|
enabled: true,
|
||||||
|
lastDelivery: "5 min ago",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "Support System",
|
||||||
|
url: "https://support.example.com/notify",
|
||||||
|
events: ["failed", "bounced"],
|
||||||
|
enabled: false,
|
||||||
|
lastDelivery: "1 hour ago",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Delivery reports
|
||||||
|
const reports = ref({
|
||||||
|
totalSent: "245,678",
|
||||||
|
successRate: 98.7,
|
||||||
|
avgDeliveryTime: 1.2,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Channel-specific reports
|
||||||
|
const channelReports = ref([
|
||||||
|
{
|
||||||
|
channel: "Email",
|
||||||
|
icon: "ic:outline-email",
|
||||||
|
sent: "156,234",
|
||||||
|
successRate: 99.2,
|
||||||
|
avgTime: 0.8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channel: "SMS",
|
||||||
|
icon: "ic:outline-sms",
|
||||||
|
sent: "45,678",
|
||||||
|
successRate: 97.8,
|
||||||
|
avgTime: 2.1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channel: "Push",
|
||||||
|
icon: "ic:outline-notifications",
|
||||||
|
sent: "89,123",
|
||||||
|
successRate: 95.4,
|
||||||
|
avgTime: 0.3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
channel: "In-App",
|
||||||
|
icon: "ic:outline-app-registration",
|
||||||
|
sent: "34,567",
|
||||||
|
successRate: 99.9,
|
||||||
|
avgTime: 0.1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
function refreshStatusData() {
|
||||||
|
// Simulate real-time data refresh
|
||||||
|
console.log("Refreshing delivery status data...");
|
||||||
|
// In real implementation, this would fetch latest data from API
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveChannelConfig() {
|
||||||
|
// Save channel configuration
|
||||||
|
console.log("Saving channel configuration...", channels.value);
|
||||||
|
showChannelModal.value = false;
|
||||||
|
// Show success toast
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportReports() {
|
||||||
|
// Export delivery reports
|
||||||
|
console.log("Exporting delivery reports...");
|
||||||
|
// In real implementation, this would generate and download reports
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-refresh data every 30 seconds
|
||||||
|
let refreshInterval;
|
||||||
|
onMounted(() => {
|
||||||
|
refreshInterval = setInterval(refreshStatusData, 30000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (refreshInterval) {
|
||||||
|
clearInterval(refreshInterval);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
661
pages/notification/delivery/monitor.vue
Normal file
661
pages/notification/delivery/monitor.vue
Normal 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>
|
817
pages/notification/delivery/providers.vue
Normal file
817
pages/notification/delivery/providers.vue
Normal file
@ -0,0 +1,817 @@
|
|||||||
|
<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: "SendGrid",
|
||||||
|
description: "Primary email delivery service",
|
||||||
|
icon: "ic:outline-email",
|
||||||
|
status: "active",
|
||||||
|
successRate: 99.2,
|
||||||
|
quotaUsed: 65,
|
||||||
|
priority: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Mailgun",
|
||||||
|
description: "Fallback email service",
|
||||||
|
icon: "ic:outline-email",
|
||||||
|
status: "active",
|
||||||
|
successRate: 98.8,
|
||||||
|
quotaUsed: 23,
|
||||||
|
priority: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "AWS SES",
|
||||||
|
description: "Cost-effective bulk email",
|
||||||
|
icon: "ic:outline-cloud",
|
||||||
|
status: "inactive",
|
||||||
|
successRate: 97.5,
|
||||||
|
quotaUsed: 5,
|
||||||
|
priority: 3,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
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: "SendGrid", value: "sendgrid" },
|
||||||
|
{ label: "Mailgun", value: "mailgun" },
|
||||||
|
{ label: "AWS SES", value: "ses" },
|
||||||
|
{ label: "Postmark", value: "postmark" },
|
||||||
|
];
|
||||||
|
|
||||||
|
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>
|
822
pages/notification/delivery/webhooks.vue
Normal file
822
pages/notification/delivery/webhooks.vue
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user