Update various configuration files, components, and assets; enhance notification system and API endpoints; improve documentation and styles across the application.
This commit is contained in:
329
pages/notification/log-audit/analytics.vue
Normal file
329
pages/notification/log-audit/analytics.vue
Normal file
@@ -0,0 +1,329 @@
|
||||
<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-bar-chart"></Icon>
|
||||
<h1 class="text-xl font-bold text-primary">Analytics Dashboard</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="text-gray-600">
|
||||
View metrics for notification performance, delivery rates, and user engagement.
|
||||
</p>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Key Metrics Summary -->
|
||||
<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 keyMetrics"
|
||||
:key="index"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<Icon class="text-primary text-3xl" :name="metric.icon"></Icon>
|
||||
</div>
|
||||
<div class="flex-1 truncate">
|
||||
<span class="block font-bold text-2xl leading-tight text-primary">
|
||||
{{ metric.value }}
|
||||
</span>
|
||||
<span class="text-sm font-medium text-gray-600">
|
||||
{{ metric.title }}
|
||||
</span>
|
||||
<div class="flex items-center mt-1" v-if="metric.change">
|
||||
<Icon
|
||||
:name="metric.trend === 'up' ? 'ic:outline-trending-up' : 'ic:outline-trending-down'"
|
||||
:class="metric.trend === 'up' ? 'text-green-500' : 'text-red-500'"
|
||||
class="text-sm mr-1"
|
||||
/>
|
||||
<span
|
||||
:class="metric.trend === 'up' ? 'text-green-600' : 'text-red-600'"
|
||||
class="text-xs font-medium"
|
||||
>
|
||||
{{ metric.change }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</rs-card>
|
||||
</div>
|
||||
|
||||
<!-- Time Range Filter -->
|
||||
<rs-card class="mb-6">
|
||||
<template #body>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<h3 class="text-lg font-semibold text-primary">Analytics Period</h3>
|
||||
<FormKit
|
||||
type="select"
|
||||
v-model="selectedPeriod"
|
||||
:options="periodOptions"
|
||||
outer-class="mb-0"
|
||||
@input="updateAnalytics"
|
||||
/>
|
||||
</div>
|
||||
<rs-button variant="primary-outline" size="sm" @click="refreshAnalytics">
|
||||
<Icon name="ic:outline-refresh" class="mr-1"/> Refresh
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<div v-if="analyticsLoading" class="flex justify-center py-16">
|
||||
<Loading />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- Main Analytics Charts -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<Icon name="ic:outline-bar-chart" class="mr-2 text-primary"/>
|
||||
<h3 class="text-lg font-semibold text-primary">Delivery Rate Analysis</h3>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="h-64 bg-gray-100 dark:bg-gray-800 flex items-center justify-center rounded">
|
||||
<div class="text-center">
|
||||
<p class="text-gray-500">Delivery success over time</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-green-600">{{ channelPerformance[0]?.successRate || '0' }}%</div>
|
||||
<div class="text-sm text-gray-600">Success Rate</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-red-600">{{ channelPerformance[0]?.failureRate || '0' }}%</div>
|
||||
<div class="text-sm text-gray-600">Failed Rate</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-yellow-600">{{ channelPerformance[0]?.bounceRate || '0' }}%</div>
|
||||
<div class="text-sm text-gray-600">Bounce Rate</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<Icon name="ic:outline-device-hub" class="mr-2 text-primary"/>
|
||||
<h3 class="text-lg font-semibold text-primary">Channel Performance</h3>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
v-for="channel in channelPerformance"
|
||||
:key="channel.name"
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center">
|
||||
<Icon :name="channel.icon" class="mr-2 text-primary"/>
|
||||
<span class="font-medium">{{ channel.name }}</span>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-primary">{{ channel.successRate }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
class="bg-primary h-2 rounded-full"
|
||||
:style="{ width: channel.successRate + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="flex justify-between text-xs text-gray-500 mt-1">
|
||||
<span>{{ channel.sent }} sent</span>
|
||||
<span>{{ channel.failed }} failed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
|
||||
<!-- Recent Analytics Events -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<Icon name="ic:outline-insights" class="mr-2 text-primary"/>
|
||||
<h3 class="text-lg font-semibold text-primary">Recent Events</h3>
|
||||
</div>
|
||||
<rs-button variant="outline" size="sm" @click="navigateTo('/notification/log-audit/logs')">
|
||||
View All Logs
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="recentEvents.length > 0" class="space-y-3">
|
||||
<div
|
||||
v-for="(event, index) in recentEvents"
|
||||
:key="index"
|
||||
class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="w-3 h-3 rounded-full mr-3"
|
||||
:class="{
|
||||
'bg-green-500': event.type === 'success',
|
||||
'bg-yellow-500': event.type === 'warning',
|
||||
'bg-red-500': event.type === 'error',
|
||||
'bg-blue-500': event.type === 'info',
|
||||
}"
|
||||
></div>
|
||||
<div>
|
||||
<p class="font-medium">{{ event.title }}</p>
|
||||
<p class="text-sm text-gray-600">{{ event.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-sm font-medium">{{ event.value }}</p>
|
||||
<p class="text-xs text-gray-500">{{ event.time }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center py-8 text-gray-500">
|
||||
<Icon name="ic:outline-search-off" class="text-4xl mb-2 mx-auto" />
|
||||
<p>No recent events found.</p>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Analytics Dashboard",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/dashboard",
|
||||
},
|
||||
{
|
||||
name: "Notification",
|
||||
path: "/notification",
|
||||
},
|
||||
{
|
||||
name: "Logs & Audit Trail",
|
||||
path: "/notification/log-audit",
|
||||
},
|
||||
{
|
||||
name: "Analytics",
|
||||
path: "/notification/log-audit/analytics",
|
||||
type: "current"
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
// Use the notification logs composable
|
||||
const {
|
||||
analyticsData,
|
||||
analyticsLoading,
|
||||
analyticsError,
|
||||
fetchAnalytics,
|
||||
formatDate
|
||||
} = useNotificationLogs()
|
||||
|
||||
// Time period selection
|
||||
const selectedPeriod = ref('7d')
|
||||
const periodOptions = [
|
||||
{ label: 'Last 24 Hours', value: '1d' },
|
||||
{ label: 'Last 7 Days', value: '7d' },
|
||||
{ label: 'Last 30 Days', value: '30d' },
|
||||
{ label: 'Last 90 Days', value: '90d' },
|
||||
{ label: 'Last 12 Months', value: '12m' },
|
||||
]
|
||||
|
||||
// Key metrics data - will be updated from API
|
||||
const keyMetrics = computed(() => analyticsData.value?.keyMetrics || [
|
||||
{
|
||||
title: "Total Sent",
|
||||
value: "0",
|
||||
icon: "ic:outline-send",
|
||||
trend: "up",
|
||||
change: "+0.0%"
|
||||
},
|
||||
{
|
||||
title: "Success Rate",
|
||||
value: "0.0%",
|
||||
icon: "ic:outline-check-circle",
|
||||
trend: "up",
|
||||
change: "+0.0%"
|
||||
},
|
||||
{
|
||||
title: "Open Rate",
|
||||
value: "0.0%",
|
||||
icon: "ic:outline-open-in-new",
|
||||
trend: "up",
|
||||
change: "+0.0%"
|
||||
},
|
||||
{
|
||||
title: "Click Rate",
|
||||
value: "0.0%",
|
||||
icon: "ic:outline-touch-app",
|
||||
trend: "up",
|
||||
change: "+0.0%"
|
||||
},
|
||||
])
|
||||
|
||||
// Channel performance data - will be updated from API
|
||||
const channelPerformance = computed(() => analyticsData.value?.channelPerformance || [
|
||||
{
|
||||
name: "Email",
|
||||
icon: "ic:outline-email",
|
||||
successRate: "0",
|
||||
sent: "0",
|
||||
failed: "0",
|
||||
bounceRate: "0",
|
||||
failureRate: "0"
|
||||
},
|
||||
])
|
||||
|
||||
// Recent analytics events from API
|
||||
const recentEvents = computed(() => analyticsData.value?.recentEvents || [])
|
||||
|
||||
// Methods
|
||||
const updateAnalytics = async () => {
|
||||
await fetchAnalytics(selectedPeriod.value)
|
||||
}
|
||||
|
||||
const refreshAnalytics = async () => {
|
||||
await fetchAnalytics(selectedPeriod.value)
|
||||
}
|
||||
|
||||
// Load initial data
|
||||
onMounted(async () => {
|
||||
await fetchAnalytics(selectedPeriod.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.formkit-outer) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.formkit-label) {
|
||||
font-weight: 500;
|
||||
color: rgb(107 114 128);
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
:deep(.formkit-input) {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
382
pages/notification/log-audit/index.vue
Normal file
382
pages/notification/log-audit/index.vue
Normal file
@@ -0,0 +1,382 @@
|
||||
<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-assessment"></Icon>
|
||||
<h1 class="text-xl font-bold text-primary">Notification Logs & Audit Trail</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="text-gray-600">
|
||||
Track notification activities, monitor performance, and ensure compliance with
|
||||
detailed audit trails.
|
||||
</p>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Summary Stats -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
|
||||
<rs-card v-for="(item, index) in summaryStats" :key="index">
|
||||
<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">
|
||||
<Icon class="text-primary text-3xl" :name="item.icon"></Icon>
|
||||
</div>
|
||||
<div class="flex-1 truncate">
|
||||
<span class="block font-bold text-2xl leading-tight text-primary">
|
||||
{{ item.value }}
|
||||
</span>
|
||||
<span class="text-sm font-medium text-gray-600">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</rs-card>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Cards -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6 mb-6">
|
||||
<rs-card
|
||||
v-for="(feature, index) in features"
|
||||
:key="index"
|
||||
class="cursor-pointer"
|
||||
@click="navigateTo(feature.path)"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<Icon class="mr-2 text-primary" :name="feature.icon"></Icon>
|
||||
<h3 class="text-lg font-semibold text-primary">{{ feature.title }}</h3>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="text-gray-600 mb-4">{{ feature.description }}</p>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex justify-end">
|
||||
<rs-button variant="outline" size="sm">
|
||||
<Icon class="mr-1" name="ic:outline-arrow-forward"></Icon>
|
||||
Open
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
|
||||
<!-- Recent Logs Section -->
|
||||
<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>
|
||||
<h3 class="text-lg font-semibold text-primary">Recent Logs</h3>
|
||||
</div>
|
||||
<rs-button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="navigateTo('/notification/log-audit/logs')"
|
||||
>
|
||||
View All
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="loading" class="flex justify-center py-8">
|
||||
<Loading />
|
||||
</div>
|
||||
|
||||
<rs-table
|
||||
v-else-if="logs && logs.length > 0"
|
||||
:data="logs"
|
||||
:field="logTableFields"
|
||||
:options="{
|
||||
variant: 'default',
|
||||
striped: true,
|
||||
borderless: false,
|
||||
hover: true,
|
||||
}"
|
||||
:options-advanced="{
|
||||
sortable: true,
|
||||
responsive: true,
|
||||
outsideBorder: true,
|
||||
}"
|
||||
advanced
|
||||
>
|
||||
<template #timestamp="{ value }">
|
||||
<div class="text-sm">
|
||||
<div class="font-medium">{{ formatDate(value.created_at, true) }}</div>
|
||||
<div class="text-gray-500 text-xs">
|
||||
{{ new Date(value.created_at).toLocaleTimeString() }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #status="{ value }">
|
||||
<rs-badge
|
||||
:variant="
|
||||
value.status === 'Sent'
|
||||
? 'success'
|
||||
: value.status === 'Failed'
|
||||
? 'danger'
|
||||
: value.status === 'Opened'
|
||||
? 'info'
|
||||
: value.status === 'Queued'
|
||||
? 'warning'
|
||||
: 'secondary'
|
||||
"
|
||||
size="sm"
|
||||
>
|
||||
{{ value.status }}
|
||||
</rs-badge>
|
||||
</template>
|
||||
|
||||
<template #actions="{ value }">
|
||||
<rs-button variant="primary-text" size="sm" @click="viewLogDetails(value)">
|
||||
<Icon name="ic:outline-visibility" class="mr-1" /> View
|
||||
</rs-button>
|
||||
</template>
|
||||
</rs-table>
|
||||
|
||||
<div v-else class="text-center py-8 text-gray-500">
|
||||
<Icon name="ic:outline-search-off" class="text-4xl mb-2 mx-auto" />
|
||||
<p>No log entries found.</p>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Log Details Modal -->
|
||||
<rs-modal
|
||||
v-model="isLogDetailModalOpen"
|
||||
title="Log Entry Details"
|
||||
size="lg"
|
||||
:overlay-close="true"
|
||||
>
|
||||
<template #body>
|
||||
<div v-if="selectedLog" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Log ID</label>
|
||||
<p class="font-mono text-sm">{{ selectedLog.id }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1"
|
||||
>Timestamp</label
|
||||
>
|
||||
<p class="text-sm">{{ formatDate(selectedLog.created_at, true) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Action</label>
|
||||
<p class="text-sm">{{ selectedLog.action }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Actor</label>
|
||||
<p class="text-sm">{{ selectedLog.actor }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Channel</label>
|
||||
<p class="text-sm">{{ selectedLog.channel_type }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Status</label>
|
||||
<rs-badge
|
||||
:variant="
|
||||
selectedLog.status === 'Sent'
|
||||
? 'success'
|
||||
: selectedLog.status === 'Failed'
|
||||
? 'danger'
|
||||
: selectedLog.status === 'Opened'
|
||||
? 'info'
|
||||
: selectedLog.status === 'Queued'
|
||||
? 'warning'
|
||||
: 'secondary'
|
||||
"
|
||||
size="sm"
|
||||
>
|
||||
{{ selectedLog.status }}
|
||||
</rs-badge>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1"
|
||||
>Source IP</label
|
||||
>
|
||||
<p class="text-sm font-mono">{{ selectedLog.source_ip }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Details</label>
|
||||
<p class="text-sm p-3 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
{{ selectedLog.details }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template v-if="selectedLog.status === 'Failed'">
|
||||
<div class="border-t pt-4">
|
||||
<h4 class="text-lg font-medium text-red-600 dark:text-red-400 mb-3">
|
||||
Error Information
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1"
|
||||
>Error Code</label
|
||||
>
|
||||
<p class="text-sm font-mono text-red-600 dark:text-red-400">
|
||||
{{ selectedLog.error_code }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1"
|
||||
>Error Message</label
|
||||
>
|
||||
<p class="text-sm text-red-600 dark:text-red-400">
|
||||
{{ selectedLog.error_message }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<rs-button @click="isLogDetailModalOpen = false" variant="primary-outline">
|
||||
Close
|
||||
</rs-button>
|
||||
</template>
|
||||
</rs-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Notification Logs & Audit Trail",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/dashboard",
|
||||
},
|
||||
{
|
||||
name: "Notification",
|
||||
path: "/notification",
|
||||
},
|
||||
{
|
||||
name: "Logs & Audit Trail",
|
||||
path: "/notification/log-audit",
|
||||
type: "current",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
|
||||
// Use the notification logs composable
|
||||
const {
|
||||
logs,
|
||||
loading,
|
||||
error,
|
||||
summaryStats: rawSummaryStats,
|
||||
fetchLogs,
|
||||
formatDate,
|
||||
} = useNotificationLogs();
|
||||
|
||||
// Transform summary stats from object to array format for display
|
||||
const summaryStats = computed(() => {
|
||||
if (!rawSummaryStats.value) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
title: "Total Logs",
|
||||
value: rawSummaryStats.value.totalLogs || 0,
|
||||
icon: "ic:outline-article",
|
||||
},
|
||||
{
|
||||
title: "Successful Deliveries",
|
||||
value: rawSummaryStats.value.successfulDeliveries || 0,
|
||||
icon: "ic:outline-mark-email-read",
|
||||
},
|
||||
{
|
||||
title: "Failed Deliveries",
|
||||
value: rawSummaryStats.value.failedDeliveries || 0,
|
||||
icon: "ic:outline-error-outline",
|
||||
},
|
||||
{
|
||||
title: "Success Rate",
|
||||
value: `${rawSummaryStats.value.successRate || 0}%`,
|
||||
icon: "ic:outline-insights",
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
// Navigation features
|
||||
const features = ref([
|
||||
{
|
||||
title: "Analytics Dashboard",
|
||||
description:
|
||||
"View metrics and trends for notification performance and delivery rates.",
|
||||
icon: "ic:outline-bar-chart",
|
||||
path: "/notification/log-audit/analytics",
|
||||
},
|
||||
{
|
||||
title: "Audit Logs",
|
||||
description:
|
||||
"Detailed logs of all notification activities with filtering capabilities.",
|
||||
icon: "ic:outline-list-alt",
|
||||
path: "/notification/log-audit/logs",
|
||||
},
|
||||
{
|
||||
title: "Reports",
|
||||
description: "Generate and export reports for compliance and analysis purposes.",
|
||||
icon: "ic:outline-file-download",
|
||||
path: "/notification/log-audit/reports",
|
||||
},
|
||||
]);
|
||||
|
||||
// Fields for RsTable
|
||||
const logTableFields = [
|
||||
"timestamp",
|
||||
"action",
|
||||
"actor",
|
||||
"channel_type",
|
||||
"status",
|
||||
"actions",
|
||||
];
|
||||
|
||||
// Log detail modal
|
||||
const isLogDetailModalOpen = ref(false);
|
||||
const selectedLog = ref(null);
|
||||
|
||||
const viewLogDetails = (log) => {
|
||||
selectedLog.value = log;
|
||||
isLogDetailModalOpen.value = true;
|
||||
};
|
||||
|
||||
// Fetch logs on component mount
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await fetchLogs();
|
||||
} catch (err) {
|
||||
console.error("Error loading logs:", err);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.formkit-outer) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.formkit-label) {
|
||||
font-weight: 500;
|
||||
color: rgb(107 114 128);
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
:deep(.formkit-input) {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
410
pages/notification/log-audit/logs.vue
Normal file
410
pages/notification/log-audit/logs.vue
Normal file
@@ -0,0 +1,410 @@
|
||||
<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-list-alt"></Icon>
|
||||
<h1 class="text-xl font-bold text-primary">Audit Logs</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="text-gray-600">
|
||||
View notification logs and filter by date, channel, status, or content.
|
||||
</p>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Summary Stats -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
|
||||
<rs-card
|
||||
v-for="(item, index) in summaryStats"
|
||||
:key="index"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<Icon class="text-primary text-3xl" :name="item.icon"></Icon>
|
||||
</div>
|
||||
<div class="flex-1 truncate">
|
||||
<span class="block font-bold text-2xl leading-tight text-primary">
|
||||
{{ item.value }}
|
||||
</span>
|
||||
<span class="text-sm font-medium text-gray-600">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</rs-card>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Card -->
|
||||
<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>
|
||||
<h3 class="text-lg font-semibold text-primary">All Audit Logs</h3>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<rs-button variant="primary-outline" size="sm" @click="refreshLogs">
|
||||
<Icon name="ic:outline-refresh" class="mr-1"/> Refresh
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<!-- Filters Section -->
|
||||
<div class="mb-6 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Actor"
|
||||
v-model="filters.actor"
|
||||
placeholder="Enter user or actor"
|
||||
outer-class="mb-0"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Channel"
|
||||
v-model="filters.channel"
|
||||
placeholder="Select channel"
|
||||
:options="availableChannels"
|
||||
outer-class="mb-0"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Status"
|
||||
v-model="filters.status"
|
||||
placeholder="Select status"
|
||||
:options="availableStatuses"
|
||||
outer-class="mb-0"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="search"
|
||||
label="Search"
|
||||
v-model="filters.keyword"
|
||||
placeholder="Search in logs..."
|
||||
outer-class="mb-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<div class="text-sm text-gray-600" v-if="!loading">
|
||||
Showing {{ logs.length }} entries
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<rs-button @click="clearFilters" variant="secondary-outline" size="sm">
|
||||
<Icon name="ic:outline-clear" class="mr-1"/> Clear
|
||||
</rs-button>
|
||||
<rs-button @click="applyFilters(filters)" variant="primary">
|
||||
<Icon name="ic:outline-search" class="mr-1"/> Apply
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="flex justify-center py-8">
|
||||
<Loading />
|
||||
</div>
|
||||
|
||||
<!-- Log Table -->
|
||||
<rs-table
|
||||
v-else-if="logs && logs.length > 0"
|
||||
:data="logs"
|
||||
:field="logTableFields"
|
||||
:options="{
|
||||
variant: 'default',
|
||||
striped: true,
|
||||
borderless: false,
|
||||
hover: true
|
||||
}"
|
||||
:options-advanced="{
|
||||
sortable: true,
|
||||
responsive: true,
|
||||
outsideBorder: true
|
||||
}"
|
||||
advanced
|
||||
>
|
||||
<template #timestamp="{ value }">
|
||||
<div class="text-sm">
|
||||
<div class="font-medium">{{ formatDate(value.created_at, true) }}</div>
|
||||
<div class="text-gray-500 text-xs">{{ new Date(value.created_at).toLocaleTimeString() }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #status="{ value }">
|
||||
<rs-badge
|
||||
:variant="value.status === 'Sent' ? 'success' :
|
||||
value.status === 'Failed' ? 'danger' :
|
||||
value.status === 'Opened' ? 'info' :
|
||||
value.status === 'Queued' ? 'warning' : 'secondary'"
|
||||
size="sm"
|
||||
>
|
||||
{{ value.status }}
|
||||
</rs-badge>
|
||||
</template>
|
||||
|
||||
<template #actions="{ value }">
|
||||
<rs-button variant="primary-text" size="sm" @click="viewLogDetails(value)">
|
||||
<Icon name="ic:outline-visibility" class="mr-1"/> View
|
||||
</rs-button>
|
||||
</template>
|
||||
</rs-table>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="text-center py-8 text-gray-500">
|
||||
<Icon name="ic:outline-search-off" class="text-4xl mb-2 mx-auto"/>
|
||||
<p>No log entries found matching your filters.</p>
|
||||
<rs-button variant="primary-outline" @click="clearFilters" class="mt-4">
|
||||
Clear Filters
|
||||
</rs-button>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div v-if="logs && logs.length > 0" class="flex justify-center mt-6">
|
||||
<div class="flex items-center gap-1">
|
||||
<rs-button
|
||||
variant="primary-text"
|
||||
size="sm"
|
||||
:disabled="pagination.page === 1"
|
||||
@click="changePage(pagination.page - 1)"
|
||||
>
|
||||
<Icon name="ic:outline-chevron-left"/>
|
||||
</rs-button>
|
||||
|
||||
<div class="flex gap-1">
|
||||
<rs-button
|
||||
v-for="p in paginationButtons"
|
||||
:key="p"
|
||||
variant="primary-text"
|
||||
size="sm"
|
||||
:class="pagination.page === p ? 'bg-primary/10' : ''"
|
||||
@click="changePage(p)"
|
||||
>
|
||||
{{ p }}
|
||||
</rs-button>
|
||||
</div>
|
||||
|
||||
<rs-button
|
||||
variant="primary-text"
|
||||
size="sm"
|
||||
:disabled="pagination.page === pagination.pages"
|
||||
@click="changePage(pagination.page + 1)"
|
||||
>
|
||||
<Icon name="ic:outline-chevron-right"/>
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Log Details Modal -->
|
||||
<rs-modal
|
||||
v-model="isLogDetailModalOpen"
|
||||
title="Log Entry Details"
|
||||
size="lg"
|
||||
:overlay-close="true"
|
||||
>
|
||||
<template #body>
|
||||
<div v-if="selectedLog" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Log ID</label>
|
||||
<p class="font-mono text-sm">{{ selectedLog.id }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Timestamp</label>
|
||||
<p class="text-sm">{{ formatDate(selectedLog.created_at, true) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Action</label>
|
||||
<p class="text-sm">{{ selectedLog.action }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Actor</label>
|
||||
<p class="text-sm">{{ selectedLog.actor }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Channel</label>
|
||||
<p class="text-sm">{{ selectedLog.channel_type }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Status</label>
|
||||
<rs-badge
|
||||
:variant="selectedLog.status === 'Sent' ? 'success' :
|
||||
selectedLog.status === 'Failed' ? 'danger' :
|
||||
selectedLog.status === 'Opened' ? 'info' :
|
||||
selectedLog.status === 'Queued' ? 'warning' : 'secondary'"
|
||||
size="sm"
|
||||
>
|
||||
{{ selectedLog.status }}
|
||||
</rs-badge>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Source IP</label>
|
||||
<p class="text-sm font-mono">{{ selectedLog.source_ip }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Details</label>
|
||||
<p class="text-sm p-3 bg-gray-50 dark:bg-gray-800 rounded">{{ selectedLog.details }}</p>
|
||||
</div>
|
||||
|
||||
<template v-if="selectedLog.status === 'Failed'">
|
||||
<div class="border-t pt-4">
|
||||
<h4 class="text-lg font-medium text-red-600 dark:text-red-400 mb-3">Error Information</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Error Code</label>
|
||||
<p class="text-sm font-mono text-red-600 dark:text-red-400">{{ selectedLog.error_code }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Error Message</label>
|
||||
<p class="text-sm text-red-600 dark:text-red-400">{{ selectedLog.error_message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<rs-button @click="isLogDetailModalOpen = false" variant="primary-outline">
|
||||
Close
|
||||
</rs-button>
|
||||
</template>
|
||||
</rs-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Audit Logs",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/dashboard",
|
||||
},
|
||||
{
|
||||
name: "Notification",
|
||||
path: "/notification",
|
||||
},
|
||||
{
|
||||
name: "Logs & Audit Trail",
|
||||
path: "/notification/log-audit",
|
||||
},
|
||||
{
|
||||
name: "Audit Logs",
|
||||
path: "/notification/log-audit/logs",
|
||||
type: "current"
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
// Use the notification logs composable
|
||||
const {
|
||||
logs,
|
||||
loading,
|
||||
error,
|
||||
pagination,
|
||||
summaryStats,
|
||||
filters,
|
||||
fetchLogs,
|
||||
applyFilters,
|
||||
clearFilters,
|
||||
changePage,
|
||||
formatDate,
|
||||
availableActions,
|
||||
availableChannels,
|
||||
availableStatuses
|
||||
} = useNotificationLogs()
|
||||
|
||||
// Computed property for pagination buttons
|
||||
const paginationButtons = computed(() => {
|
||||
const current = pagination.value.page
|
||||
const total = pagination.value.pages
|
||||
const buttons = []
|
||||
|
||||
if (total <= 7) {
|
||||
// Less than 7 pages, show all
|
||||
for (let i = 1; i <= total; i++) {
|
||||
buttons.push(i)
|
||||
}
|
||||
} else {
|
||||
// Always show first page
|
||||
buttons.push(1)
|
||||
|
||||
if (current > 3) {
|
||||
buttons.push('...')
|
||||
}
|
||||
|
||||
// Pages around current
|
||||
const start = Math.max(2, current - 1)
|
||||
const end = Math.min(current + 1, total - 1)
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
buttons.push(i)
|
||||
}
|
||||
|
||||
if (current < total - 2) {
|
||||
buttons.push('...')
|
||||
}
|
||||
|
||||
// Always show last page
|
||||
buttons.push(total)
|
||||
}
|
||||
|
||||
return buttons
|
||||
})
|
||||
|
||||
// Fields for RsTable
|
||||
const logTableFields = ['timestamp', 'action', 'actor', 'channel_type', 'status', 'actions']
|
||||
|
||||
// Log detail modal
|
||||
const isLogDetailModalOpen = ref(false)
|
||||
const selectedLog = ref(null)
|
||||
|
||||
const viewLogDetails = (log) => {
|
||||
selectedLog.value = log
|
||||
isLogDetailModalOpen.value = true
|
||||
}
|
||||
|
||||
const refreshLogs = async () => {
|
||||
await fetchLogs()
|
||||
}
|
||||
|
||||
// Fetch logs on component mount
|
||||
onMounted(async () => {
|
||||
await fetchLogs()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.formkit-outer) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.formkit-label) {
|
||||
font-weight: 500;
|
||||
color: rgb(107 114 128);
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
:deep(.formkit-input) {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
689
pages/notification/log-audit/monitoring.vue
Normal file
689
pages/notification/log-audit/monitoring.vue
Normal file
@@ -0,0 +1,689 @@
|
||||
<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">Real-Time Monitoring</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="text-gray-600">
|
||||
Live monitoring of notification system performance with real-time alerts and
|
||||
system health indicators. Track ongoing activities, monitor system load, and
|
||||
receive immediate notifications about issues.
|
||||
</p>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- System Status Overview -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
|
||||
<rs-card
|
||||
v-for="(status, index) in systemStatus"
|
||||
: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="
|
||||
status.status === 'healthy'
|
||||
? 'bg-green-100'
|
||||
: status.status === 'warning'
|
||||
? 'bg-yellow-100'
|
||||
: 'bg-red-100'
|
||||
"
|
||||
>
|
||||
<Icon
|
||||
class="text-3xl"
|
||||
:class="
|
||||
status.status === 'healthy'
|
||||
? 'text-green-600'
|
||||
: status.status === 'warning'
|
||||
? 'text-yellow-600'
|
||||
: 'text-red-600'
|
||||
"
|
||||
:name="status.icon"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 truncate">
|
||||
<span class="block font-bold text-2xl leading-tight text-primary">
|
||||
{{ status.value }}
|
||||
</span>
|
||||
<span class="text-sm font-medium text-gray-600">
|
||||
{{ status.title }}
|
||||
</span>
|
||||
<div class="flex items-center mt-1">
|
||||
<div
|
||||
class="w-2 h-2 rounded-full mr-2"
|
||||
:class="
|
||||
status.status === 'healthy'
|
||||
? 'bg-green-500'
|
||||
: status.status === 'warning'
|
||||
? 'bg-yellow-500'
|
||||
: 'bg-red-500'
|
||||
"
|
||||
></div>
|
||||
<span
|
||||
class="text-xs font-medium capitalize"
|
||||
:class="
|
||||
status.status === 'healthy'
|
||||
? 'text-green-600'
|
||||
: status.status === 'warning'
|
||||
? 'text-yellow-600'
|
||||
: 'text-red-600'
|
||||
"
|
||||
>
|
||||
{{ status.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</rs-card>
|
||||
</div>
|
||||
|
||||
<!-- Real-Time Controls -->
|
||||
<rs-card class="mb-6">
|
||||
<template #body>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<h3 class="text-lg font-semibold text-primary">Monitoring Controls</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="w-3 h-3 rounded-full animate-pulse"
|
||||
:class="isMonitoring ? 'bg-green-500' : 'bg-gray-400'"
|
||||
></div>
|
||||
<span class="text-sm font-medium">
|
||||
{{ isMonitoring ? "Live" : "Paused" }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<FormKit
|
||||
type="select"
|
||||
v-model="refreshInterval"
|
||||
:options="refreshOptions"
|
||||
outer-class="mb-0"
|
||||
@input="updateRefreshInterval"
|
||||
/>
|
||||
<rs-button
|
||||
:variant="isMonitoring ? 'danger-outline' : 'primary'"
|
||||
size="sm"
|
||||
@click="toggleMonitoring"
|
||||
>
|
||||
<Icon
|
||||
:name="isMonitoring ? 'ic:outline-pause' : 'ic:outline-play-arrow'"
|
||||
class="mr-1"
|
||||
/>
|
||||
{{ isMonitoring ? "Pause" : "Start" }}
|
||||
</rs-button>
|
||||
<rs-button variant="primary-outline" size="sm" @click="refreshData">
|
||||
<Icon name="ic:outline-refresh" class="mr-1" /> Refresh
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- System Performance Dashboard -->
|
||||
<rs-card class="mb-6">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<Icon name="ic:outline-speed" class="mr-2 text-primary" />
|
||||
<h3 class="text-lg font-semibold text-primary">System Performance</h3>
|
||||
</div>
|
||||
<rs-button variant="outline" size="sm" @click="exportPerformanceData">
|
||||
<Icon name="ic:outline-file-download" class="mr-1" /> Export Data
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
<!-- CPU Usage -->
|
||||
<div class="text-center">
|
||||
<div class="relative mx-auto w-32 h-32 mb-4">
|
||||
<div
|
||||
class="w-full h-full bg-gray-200 rounded-full flex items-center justify-center"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-primary">
|
||||
{{ performanceMetrics.cpu }}%
|
||||
</div>
|
||||
<div class="text-xs text-gray-600">CPU</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Memory Usage -->
|
||||
<div class="text-center">
|
||||
<div class="relative mx-auto w-32 h-32 mb-4">
|
||||
<div
|
||||
class="w-full h-full bg-gray-200 rounded-full flex items-center justify-center"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-primary">
|
||||
{{ performanceMetrics.memory }}%
|
||||
</div>
|
||||
<div class="text-xs text-gray-600">Memory</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Queue Load -->
|
||||
<div class="text-center">
|
||||
<div class="relative mx-auto w-32 h-32 mb-4">
|
||||
<div
|
||||
class="w-full h-full bg-gray-200 rounded-full flex items-center justify-center"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-primary">
|
||||
{{ performanceMetrics.queueLoad }}%
|
||||
</div>
|
||||
<div class="text-xs text-gray-600">Queue Load</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Performance Chart Placeholder -->
|
||||
<div
|
||||
class="h-64 bg-gray-100 dark:bg-gray-800 flex items-center justify-center rounded"
|
||||
>
|
||||
<div class="text-center">
|
||||
<Icon name="ic:outline-show-chart" class="text-4xl text-gray-400 mb-2" />
|
||||
<p class="text-gray-500">Real-time Performance Chart</p>
|
||||
<p class="text-sm text-gray-400 mt-1">
|
||||
Implementation pending for live performance metrics
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Live Activity & Alerts Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<!-- Live Activity Feed -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<Icon name="ic:outline-notifications-active" class="mr-2 text-primary" />
|
||||
<h3 class="text-lg font-semibold text-primary">Live Activity Feed</h3>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<rs-button variant="outline" size="sm" @click="clearActivityFeed">
|
||||
<Icon name="ic:outline-clear" class="mr-1" /> Clear
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="h-96 overflow-y-auto space-y-3">
|
||||
<div
|
||||
v-for="(activity, index) in liveActivityFeed"
|
||||
:key="index"
|
||||
class="flex items-start p-3 bg-gray-50 dark:bg-gray-800 rounded-lg transition-all duration-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<div
|
||||
class="w-3 h-3 rounded-full mr-3 mt-2 flex-shrink-0"
|
||||
:class="{
|
||||
'bg-green-500': activity.type === 'success',
|
||||
'bg-blue-500': activity.type === 'info',
|
||||
'bg-yellow-500': activity.type === 'warning',
|
||||
'bg-red-500': activity.type === 'error',
|
||||
}"
|
||||
></div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium text-sm">{{ activity.action }}</p>
|
||||
<p class="text-xs text-gray-600 mt-1">{{ activity.details }}</p>
|
||||
<div class="flex items-center mt-2 text-xs text-gray-500">
|
||||
<Icon name="ic:outline-access-time" class="mr-1" />
|
||||
<span>{{ activity.timestamp }}</span>
|
||||
<span class="mx-2">•</span>
|
||||
<span>{{ activity.source }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="liveActivityFeed.length === 0"
|
||||
class="text-center py-8 text-gray-500"
|
||||
>
|
||||
<Icon name="ic:outline-wifi-tethering" class="text-3xl mb-2 mx-auto" />
|
||||
<p>Waiting for live activity...</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Error Alerts -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<Icon name="ic:outline-warning" class="mr-2 text-primary" />
|
||||
<h3 class="text-lg font-semibold text-primary">Error Alerts</h3>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<rs-badge
|
||||
:variant="
|
||||
errorAlerts.filter((a) => a.severity === 'critical').length > 0
|
||||
? 'danger'
|
||||
: 'secondary'
|
||||
"
|
||||
size="sm"
|
||||
>
|
||||
{{ errorAlerts.length }} Active
|
||||
</rs-badge>
|
||||
<rs-button variant="outline" size="sm" @click="acknowledgeAllAlerts">
|
||||
<Icon name="ic:outline-check" class="mr-1" /> Acknowledge All
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="h-96 overflow-y-auto space-y-3">
|
||||
<div
|
||||
v-for="(alert, index) in errorAlerts"
|
||||
:key="index"
|
||||
class="p-3 rounded-lg border-l-4 transition-all duration-300 hover:shadow-sm"
|
||||
:class="{
|
||||
'bg-red-50 border-red-400': alert.severity === 'critical',
|
||||
'bg-yellow-50 border-yellow-400': alert.severity === 'warning',
|
||||
'bg-blue-50 border-blue-400': alert.severity === 'info',
|
||||
}"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center">
|
||||
<Icon
|
||||
:name="
|
||||
alert.severity === 'critical'
|
||||
? 'ic:outline-error'
|
||||
: alert.severity === 'warning'
|
||||
? 'ic:outline-warning'
|
||||
: 'ic:outline-info'
|
||||
"
|
||||
:class="{
|
||||
'text-red-600': alert.severity === 'critical',
|
||||
'text-yellow-600': alert.severity === 'warning',
|
||||
'text-blue-600': alert.severity === 'info',
|
||||
}"
|
||||
class="mr-2"
|
||||
/>
|
||||
<span class="font-medium text-sm">{{ alert.title }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 mt-1">{{ alert.description }}</p>
|
||||
<div class="flex items-center mt-2 text-xs text-gray-500">
|
||||
<span>{{ alert.timestamp }}</span>
|
||||
<span class="mx-2">•</span>
|
||||
<span>{{ alert.component }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<rs-button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="acknowledgeAlert(index)"
|
||||
class="ml-2"
|
||||
>
|
||||
<Icon name="ic:outline-check" />
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="errorAlerts.length === 0" class="text-center py-8 text-gray-500">
|
||||
<Icon
|
||||
name="ic:outline-check-circle"
|
||||
class="text-3xl mb-2 mx-auto text-green-500"
|
||||
/>
|
||||
<p>No active alerts</p>
|
||||
<p class="text-sm text-gray-400 mt-1">All systems operating normally</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
|
||||
<!-- Queue Status -->
|
||||
<rs-card class="mb-6">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<Icon name="ic:outline-queue" class="mr-2 text-primary" />
|
||||
<h3 class="text-lg font-semibold text-primary">Queue Status</h3>
|
||||
</div>
|
||||
<rs-button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="navigateTo('/notification/queue-scheduler/monitor')"
|
||||
>
|
||||
View Queue Monitor
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div
|
||||
v-for="queue in queueStatus"
|
||||
:key="queue.name"
|
||||
class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="font-medium">{{ queue.name }}</span>
|
||||
<rs-badge
|
||||
:variant="
|
||||
queue.status === 'active'
|
||||
? 'success'
|
||||
: queue.status === 'warning'
|
||||
? 'warning'
|
||||
: 'danger'
|
||||
"
|
||||
size="sm"
|
||||
>
|
||||
{{ queue.status }}
|
||||
</rs-badge>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-primary mb-1">{{ queue.count }}</div>
|
||||
<div class="text-sm text-gray-600">{{ queue.description }}</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2 mt-2">
|
||||
<div
|
||||
class="h-2 rounded-full transition-all duration-300"
|
||||
:class="
|
||||
queue.status === 'active'
|
||||
? 'bg-green-500'
|
||||
: queue.status === 'warning'
|
||||
? 'bg-yellow-500'
|
||||
: 'bg-red-500'
|
||||
"
|
||||
:style="{ width: queue.utilization + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
{{ queue.utilization }}% utilized
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Recent Logs -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<Icon name="ic:outline-history" class="mr-2 text-primary" />
|
||||
<h3 class="text-lg font-semibold text-primary">Recent Activity Logs</h3>
|
||||
</div>
|
||||
<rs-button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="navigateTo('/notification/log-audit/logs')"
|
||||
>
|
||||
View All Logs
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="(log, index) in recentLogs"
|
||||
:key="index"
|
||||
class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="w-3 h-3 rounded-full mr-3"
|
||||
:class="{
|
||||
'bg-green-500': log.status === 'sent' || log.status === 'created',
|
||||
'bg-yellow-500': log.status === 'queued',
|
||||
'bg-red-500': log.status === 'failed',
|
||||
'bg-blue-500': log.status === 'opened',
|
||||
}"
|
||||
></div>
|
||||
<div>
|
||||
<p class="font-medium">{{ log.action }}</p>
|
||||
<p class="text-sm text-gray-600">{{ log.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-sm font-medium capitalize">{{ log.status }}</p>
|
||||
<p class="text-xs text-gray-500">{{ log.time }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Real-Time Monitoring",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/dashboard",
|
||||
},
|
||||
{
|
||||
name: "Notification",
|
||||
path: "/notification",
|
||||
},
|
||||
{
|
||||
name: "Logs & Audit Trail",
|
||||
path: "/notification/log-audit",
|
||||
},
|
||||
{
|
||||
name: "Monitoring",
|
||||
path: "/notification/log-audit/monitoring",
|
||||
type: "current",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
|
||||
// Use the notification logs composable
|
||||
const {
|
||||
monitoringData,
|
||||
monitoringLoading,
|
||||
monitoringError,
|
||||
fetchMonitoringData,
|
||||
formatTimeAgo,
|
||||
} = useNotificationLogs();
|
||||
|
||||
// Monitoring state
|
||||
const isMonitoring = ref(true);
|
||||
const refreshInterval = ref("5s");
|
||||
const refreshIntervalId = ref(null);
|
||||
|
||||
const refreshOptions = [
|
||||
{ label: "1 second", value: "1s" },
|
||||
{ label: "5 seconds", value: "5s" },
|
||||
{ label: "10 seconds", value: "10s" },
|
||||
{ label: "30 seconds", value: "30s" },
|
||||
{ label: "1 minute", value: "1m" },
|
||||
];
|
||||
|
||||
// System status data - will be updated from API
|
||||
const systemStatus = computed(
|
||||
() =>
|
||||
monitoringData.value?.systemStatus || [
|
||||
{
|
||||
title: "System Health",
|
||||
value: "Healthy",
|
||||
icon: "ic:outline-favorite",
|
||||
status: "healthy",
|
||||
},
|
||||
{
|
||||
title: "Throughput",
|
||||
value: "0/hr",
|
||||
icon: "ic:outline-speed",
|
||||
status: "healthy",
|
||||
},
|
||||
{
|
||||
title: "Error Rate",
|
||||
value: "0.00%",
|
||||
icon: "ic:outline-error-outline",
|
||||
status: "healthy",
|
||||
},
|
||||
{
|
||||
title: "Response Time",
|
||||
value: "0ms",
|
||||
icon: "ic:outline-timer",
|
||||
status: "healthy",
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Performance metrics - will be updated from API
|
||||
const performanceMetrics = computed(
|
||||
() =>
|
||||
monitoringData.value?.performanceMetrics || {
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
queueLoad: 0,
|
||||
}
|
||||
);
|
||||
|
||||
// Live activity feed - will be updated from API
|
||||
const liveActivityFeed = computed(() => monitoringData.value?.recentActivity || []);
|
||||
|
||||
// Error alerts - will be updated from API
|
||||
const errorAlerts = computed(() => monitoringData.value?.errorAlerts || []);
|
||||
|
||||
// Queue status - will be updated from API
|
||||
const queueStatus = computed(() => monitoringData.value?.queueStatus || []);
|
||||
|
||||
// Recent logs - using the same activity feed
|
||||
const recentLogs = computed(() => liveActivityFeed.value);
|
||||
|
||||
// Methods
|
||||
const toggleMonitoring = () => {
|
||||
isMonitoring.value = !isMonitoring.value;
|
||||
if (isMonitoring.value) {
|
||||
startMonitoring();
|
||||
} else {
|
||||
stopMonitoring();
|
||||
}
|
||||
};
|
||||
|
||||
const updateRefreshInterval = () => {
|
||||
if (isMonitoring.value) {
|
||||
stopMonitoring();
|
||||
startMonitoring();
|
||||
}
|
||||
};
|
||||
|
||||
const startMonitoring = () => {
|
||||
const intervalMs =
|
||||
{
|
||||
"1s": 1000,
|
||||
"5s": 5000,
|
||||
"10s": 10000,
|
||||
"30s": 30000,
|
||||
"1m": 60000,
|
||||
}[refreshInterval.value] || 5000;
|
||||
|
||||
// Fetch immediately
|
||||
fetchMonitoringData();
|
||||
|
||||
// Then set up the interval
|
||||
refreshIntervalId.value = setInterval(async () => {
|
||||
await fetchMonitoringData();
|
||||
}, intervalMs);
|
||||
};
|
||||
|
||||
const stopMonitoring = () => {
|
||||
if (refreshIntervalId.value) {
|
||||
clearInterval(refreshIntervalId.value);
|
||||
refreshIntervalId.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshData = async () => {
|
||||
await fetchMonitoringData();
|
||||
};
|
||||
|
||||
const clearActivityFeed = () => {
|
||||
// For now, just refresh the data - in a real app, you might have an API endpoint to clear the feed
|
||||
fetchMonitoringData();
|
||||
};
|
||||
|
||||
const acknowledgeAlert = (index) => {
|
||||
// In a real app, you would call an API to acknowledge the alert
|
||||
errorAlerts.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const acknowledgeAllAlerts = () => {
|
||||
// In a real app, you would call an API to acknowledge all alerts
|
||||
errorAlerts.value = [];
|
||||
};
|
||||
|
||||
const exportPerformanceData = () => {
|
||||
console.log("Exporting performance data...");
|
||||
alert("Exporting performance data. (Implementation pending)");
|
||||
};
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
if (isMonitoring.value) {
|
||||
startMonitoring();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopMonitoring();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// Custom styles for FormKit consistency
|
||||
:deep(.formkit-outer) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.formkit-label) {
|
||||
font-weight: 500;
|
||||
color: rgb(107 114 128);
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
:deep(.formkit-input) {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
// Badge component styles (if RsBadge doesn't exist, these can be adjusted)
|
||||
.rs-badge {
|
||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
|
||||
}
|
||||
|
||||
.rs-badge.variant-success {
|
||||
@apply bg-green-100 text-green-800;
|
||||
}
|
||||
|
||||
.rs-badge.variant-danger {
|
||||
@apply bg-red-100 text-red-800;
|
||||
}
|
||||
|
||||
.rs-badge.variant-warning {
|
||||
@apply bg-yellow-100 text-yellow-800;
|
||||
}
|
||||
|
||||
.rs-badge.variant-info {
|
||||
@apply bg-blue-100 text-blue-800;
|
||||
}
|
||||
|
||||
.rs-badge.variant-secondary {
|
||||
@apply bg-gray-100 text-gray-800;
|
||||
}
|
||||
</style>
|
||||
439
pages/notification/log-audit/reports.vue
Normal file
439
pages/notification/log-audit/reports.vue
Normal file
@@ -0,0 +1,439 @@
|
||||
<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-file-download"></Icon>
|
||||
<h1 class="text-xl font-bold text-primary">Reports & Export</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="text-gray-600">
|
||||
Generate reports and export log data for compliance, auditing, and analysis.
|
||||
</p>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Quick Export Actions -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
|
||||
<rs-card
|
||||
v-for="(action, index) in quickExportActions"
|
||||
:key="index"
|
||||
class="cursor-pointer"
|
||||
@click="quickExport(action.type)"
|
||||
>
|
||||
<div class="pt-5 pb-3 px-5 text-center">
|
||||
<div
|
||||
class="p-5 flex justify-center items-center bg-primary/20 rounded-2xl mx-auto mb-4 w-fit"
|
||||
>
|
||||
<Icon class="text-primary text-3xl" :name="action.icon"></Icon>
|
||||
</div>
|
||||
<span class="block font-bold text-lg leading-tight text-primary mb-2">
|
||||
{{ action.title }}
|
||||
</span>
|
||||
<span class="text-sm text-gray-600">
|
||||
{{ action.description }}
|
||||
</span>
|
||||
</div>
|
||||
</rs-card>
|
||||
</div>
|
||||
|
||||
<!-- Custom Report Builder -->
|
||||
<rs-card class="mb-6">
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<Icon name="ic:outline-build" class="mr-2 text-primary" />
|
||||
<h3 class="text-lg font-semibold text-primary">Custom Report Builder</h3>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Report Configuration -->
|
||||
<div class="space-y-4">
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Report Name"
|
||||
v-model="customReport.name"
|
||||
placeholder="Enter report name"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Report Type"
|
||||
v-model="customReport.type"
|
||||
:options="reportTypeOptions"
|
||||
placeholder="Select report type"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Date Range"
|
||||
v-model="customReport.dateRange"
|
||||
:options="dateRangeOptions"
|
||||
placeholder="Select date range"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Export Format"
|
||||
v-model="customReport.format"
|
||||
:options="exportFormatOptions"
|
||||
placeholder="Select format"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
label="Include Channels"
|
||||
v-model="customReport.channels"
|
||||
:options="channelOptions"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
label="Include Status"
|
||||
v-model="customReport.statuses"
|
||||
:options="statusOptions"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Report Preview -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-lg font-semibold">Report Preview</h4>
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg min-h-64">
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-medium">Report Name:</span>
|
||||
<span class="text-primary">{{
|
||||
customReport.name || "Untitled Report"
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-medium">Type:</span>
|
||||
<span>{{ getReportTypeLabel(customReport.type) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-medium">Date Range:</span>
|
||||
<span>{{ getDateRangeLabel(customReport.dateRange) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-medium">Format:</span>
|
||||
<span>{{ getFormatLabel(customReport.format) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="font-medium">Channels:</span>
|
||||
<div class="text-right">
|
||||
<div v-if="customReport.channels.length === 0" class="text-gray-500">
|
||||
All channels
|
||||
</div>
|
||||
<div v-else class="space-y-1">
|
||||
<div
|
||||
v-for="channel in customReport.channels"
|
||||
:key="channel"
|
||||
class="text-sm"
|
||||
>
|
||||
{{ channel }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="font-medium">Status:</span>
|
||||
<div class="text-right">
|
||||
<div v-if="customReport.statuses.length === 0" class="text-gray-500">
|
||||
All statuses
|
||||
</div>
|
||||
<div v-else class="space-y-1">
|
||||
<div
|
||||
v-for="status in customReport.statuses"
|
||||
:key="status"
|
||||
class="text-sm"
|
||||
>
|
||||
{{ status }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<rs-button
|
||||
@click="generateCustomReport"
|
||||
variant="primary"
|
||||
:disabled="!customReport.name || !customReport.type"
|
||||
class="flex-1"
|
||||
>
|
||||
<Icon name="ic:outline-play-arrow" class="mr-1" /> Generate Report
|
||||
</rs-button>
|
||||
<rs-button @click="saveReportTemplate" variant="secondary-outline">
|
||||
<Icon name="ic:outline-save" class="mr-1" /> Save Template
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Export History -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<Icon name="ic:outline-history" class="mr-2 text-primary" />
|
||||
<h3 class="text-lg font-semibold text-primary">Export History</h3>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="(export_, index) in exportHistory"
|
||||
:key="index"
|
||||
class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<Icon
|
||||
:name="
|
||||
export_.format === 'pdf'
|
||||
? 'ic:outline-picture-as-pdf'
|
||||
: export_.format === 'csv'
|
||||
? 'ic:outline-table-chart'
|
||||
: 'ic:outline-grid-on'
|
||||
"
|
||||
class="mr-3 text-primary text-xl"
|
||||
/>
|
||||
<div>
|
||||
<p class="font-medium">{{ export_.name }}</p>
|
||||
<p class="text-sm text-gray-600">{{ export_.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="text-right">
|
||||
<p class="text-sm font-medium">{{ export_.size }}</p>
|
||||
<p class="text-xs text-gray-500">{{ export_.timestamp }}</p>
|
||||
</div>
|
||||
<rs-badge
|
||||
:variant="
|
||||
export_.status === 'completed'
|
||||
? 'success'
|
||||
: export_.status === 'processing'
|
||||
? 'warning'
|
||||
: 'danger'
|
||||
"
|
||||
size="sm"
|
||||
>
|
||||
{{ export_.status }}
|
||||
</rs-badge>
|
||||
<div class="flex gap-1">
|
||||
<rs-button
|
||||
v-if="export_.status === 'completed'"
|
||||
variant="primary-text"
|
||||
size="sm"
|
||||
@click="downloadExport(export_)"
|
||||
>
|
||||
<Icon name="ic:outline-download" />
|
||||
</rs-button>
|
||||
<rs-button variant="danger-text" size="sm" @click="deleteExport(index)">
|
||||
<Icon name="ic:outline-delete" />
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="exportHistory.length === 0" class="text-center py-8 text-gray-500">
|
||||
<Icon name="ic:outline-folder-open" class="text-4xl mb-2 mx-auto" />
|
||||
<p>No export history available</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Reports & Export",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/dashboard",
|
||||
},
|
||||
{
|
||||
name: "Notification",
|
||||
path: "/notification",
|
||||
},
|
||||
{
|
||||
name: "Logs & Audit Trail",
|
||||
path: "/notification/log-audit",
|
||||
},
|
||||
{
|
||||
name: "Reports",
|
||||
path: "/notification/log-audit/reports",
|
||||
type: "current",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
// Quick export actions
|
||||
const quickExportActions = ref([
|
||||
{
|
||||
title: "CSV Export",
|
||||
description: "Export current data to CSV format",
|
||||
icon: "ic:outline-table-chart",
|
||||
type: "csv",
|
||||
},
|
||||
{
|
||||
title: "PDF Report",
|
||||
description: "Generate comprehensive PDF report",
|
||||
icon: "ic:outline-picture-as-pdf",
|
||||
type: "pdf",
|
||||
},
|
||||
{
|
||||
title: "Excel Export",
|
||||
description: "Export data to Excel spreadsheet",
|
||||
icon: "ic:outline-grid-on",
|
||||
type: "excel",
|
||||
},
|
||||
{
|
||||
title: "JSON Export",
|
||||
description: "Export raw data in JSON format",
|
||||
icon: "ic:outline-code",
|
||||
type: "json",
|
||||
},
|
||||
]);
|
||||
|
||||
// Custom report builder
|
||||
const customReport = ref({
|
||||
name: "",
|
||||
type: "",
|
||||
dateRange: "",
|
||||
format: "",
|
||||
channels: [],
|
||||
statuses: [],
|
||||
});
|
||||
|
||||
const reportTypeOptions = [
|
||||
{ label: "Delivery Report", value: "delivery" },
|
||||
{ label: "Performance Analytics", value: "performance" },
|
||||
{ label: "Error Analysis", value: "errors" },
|
||||
{ label: "User Engagement", value: "engagement" },
|
||||
{ label: "Channel Comparison", value: "channels" },
|
||||
{ label: "Audit Trail", value: "audit" },
|
||||
];
|
||||
|
||||
const dateRangeOptions = [
|
||||
{ label: "Last 24 Hours", value: "1d" },
|
||||
{ label: "Last 7 Days", value: "7d" },
|
||||
{ label: "Last 30 Days", value: "30d" },
|
||||
{ label: "Last 90 Days", value: "90d" },
|
||||
{ label: "Last 12 Months", value: "12m" },
|
||||
{ label: "Custom Range", value: "custom" },
|
||||
];
|
||||
|
||||
const exportFormatOptions = [
|
||||
{ label: "CSV", value: "csv" },
|
||||
{ label: "PDF", value: "pdf" },
|
||||
{ label: "Excel", value: "excel" },
|
||||
{ label: "JSON", value: "json" },
|
||||
];
|
||||
|
||||
const channelOptions = [
|
||||
{ label: "Email", value: "Email" },
|
||||
{ label: "SMS", value: "SMS" },
|
||||
{ label: "Push Notification", value: "Push Notification" },
|
||||
{ label: "Webhook", value: "Webhook" },
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{ label: "Sent", value: "Sent" },
|
||||
{ label: "Failed", value: "Failed" },
|
||||
{ label: "Bounced", value: "Bounced" },
|
||||
{ label: "Opened", value: "Opened" },
|
||||
{ label: "Queued", value: "Queued" },
|
||||
];
|
||||
|
||||
// Export history
|
||||
const exportHistory = ref([
|
||||
{
|
||||
name: "Notification Analytics Report",
|
||||
description: "Monthly performance analysis",
|
||||
format: "pdf",
|
||||
size: "2.4 MB",
|
||||
timestamp: "2 hours ago",
|
||||
status: "completed",
|
||||
},
|
||||
{
|
||||
name: "Delivery Logs Export",
|
||||
description: "Last 30 days delivery data",
|
||||
format: "csv",
|
||||
size: "856 KB",
|
||||
timestamp: "1 day ago",
|
||||
status: "completed",
|
||||
},
|
||||
]);
|
||||
|
||||
// Helper functions
|
||||
const getReportTypeLabel = (value) => {
|
||||
const option = reportTypeOptions.find((opt) => opt.value === value);
|
||||
return option ? option.label : "Not selected";
|
||||
};
|
||||
|
||||
const getDateRangeLabel = (value) => {
|
||||
const option = dateRangeOptions.find((opt) => opt.value === value);
|
||||
return option ? option.label : "Not selected";
|
||||
};
|
||||
|
||||
const getFormatLabel = (value) => {
|
||||
const option = exportFormatOptions.find((opt) => opt.value === value);
|
||||
return option ? option.label : "Not selected";
|
||||
};
|
||||
|
||||
// Methods
|
||||
const quickExport = (type) => {
|
||||
console.log(`Quick export: ${type}`);
|
||||
alert(`Exporting data in ${type} format. (Implementation pending)`);
|
||||
};
|
||||
|
||||
const generateCustomReport = () => {
|
||||
console.log("Generating custom report:", customReport.value);
|
||||
alert(`Generating custom report: ${customReport.value.name}. (Implementation pending)`);
|
||||
};
|
||||
|
||||
const saveReportTemplate = () => {
|
||||
console.log("Saving report template:", customReport.value);
|
||||
alert(`Saving report template: ${customReport.value.name}. (Implementation pending)`);
|
||||
};
|
||||
|
||||
const downloadExport = (export_) => {
|
||||
console.log("Downloading export:", export_);
|
||||
alert(`Downloading ${export_.name}. (Implementation pending)`);
|
||||
};
|
||||
|
||||
const deleteExport = (index) => {
|
||||
exportHistory.value.splice(index, 1);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.formkit-outer) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
:deep(.formkit-label) {
|
||||
font-weight: 500;
|
||||
color: rgb(107 114 128);
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
:deep(.formkit-input) {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user