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

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

View File

@@ -0,0 +1,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>

View 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>

View 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>

View 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>

View 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>