845 lines
26 KiB
Vue
845 lines
26 KiB
Vue
<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">
|
|
This module provides comprehensive logging and auditing capabilities for all
|
|
notification activities. Track deliveries, monitor performance, analyze trends,
|
|
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"
|
|
class="transition-all duration-300 hover:shadow-lg"
|
|
>
|
|
<div class="pt-5 pb-3 px-5 flex items-center gap-4">
|
|
<div
|
|
class="p-5 flex justify-center items-center bg-primary/20 rounded-2xl transition-all duration-300 hover:bg-primary/30"
|
|
>
|
|
<Icon class="text-primary text-3xl" :name="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>
|
|
|
|
<!-- Feature 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="transition-all duration-300 hover:shadow-lg cursor-pointer"
|
|
@click="navigateTo(feature.path)"
|
|
>
|
|
<template #header>
|
|
<div class="flex items-center">
|
|
<Icon class="mr-2 text-primary" :name="feature.icon"></Icon>
|
|
<h3 class="text-lg font-semibold text-primary">{{ feature.title }}</h3>
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<p class="text-gray-600 mb-4">{{ feature.description }}</p>
|
|
<div class="space-y-2">
|
|
<div
|
|
v-for="(useCase, idx) in feature.useCases"
|
|
:key="idx"
|
|
class="flex items-start"
|
|
>
|
|
<Icon
|
|
class="mr-2 mt-1 text-green-500 text-sm"
|
|
name="ic:outline-check-circle"
|
|
></Icon>
|
|
<span class="text-sm text-gray-700">{{ useCase }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #footer>
|
|
<div class="flex justify-end">
|
|
<rs-button variant="outline" size="sm">
|
|
<Icon class="mr-1" name="ic:outline-arrow-forward"></Icon>
|
|
Open
|
|
</rs-button>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
</div>
|
|
|
|
<!-- Audit Logs Section -->
|
|
<rs-card class="mb-6">
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<Icon class="mr-2 text-primary" name="ic:outline-list-alt"></Icon>
|
|
<h3 class="text-lg font-semibold text-primary">Recent Audit Logs</h3>
|
|
</div>
|
|
<rs-button
|
|
variant="outline"
|
|
size="sm"
|
|
@click="navigateTo('/notification/log-audit/logs')"
|
|
>
|
|
View All
|
|
</rs-button>
|
|
</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-6 gap-4"
|
|
>
|
|
<div class="xl:col-span-2">
|
|
<label
|
|
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
|
>Date Range</label
|
|
>
|
|
<rs-button variant="primary-outline" class="w-full justify-start">
|
|
<Icon name="ic:outline-date-range" class="mr-2" />
|
|
{{
|
|
selectedDateRange.start && selectedDateRange.end
|
|
? `${formatDate(selectedDateRange.start)} - ${formatDate(
|
|
selectedDateRange.end
|
|
)}`
|
|
: "Select date range"
|
|
}}
|
|
</rs-button>
|
|
</div>
|
|
|
|
<FormKit
|
|
type="text"
|
|
label="User or 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="channelOptionsForFormKit"
|
|
outer-class="mb-0"
|
|
/>
|
|
|
|
<FormKit
|
|
type="select"
|
|
label="Status"
|
|
v-model="filters.status"
|
|
placeholder="Select status"
|
|
:options="statusOptionsForFormKit"
|
|
outer-class="mb-0"
|
|
/>
|
|
|
|
<FormKit
|
|
type="search"
|
|
label="Search Content"
|
|
v-model="filters.keyword"
|
|
placeholder="Search in logs..."
|
|
outer-class="mb-0"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex justify-end mt-4">
|
|
<rs-button @click="applyFilters" variant="primary">
|
|
<Icon name="ic:outline-search" class="mr-1" /> Apply Filters
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Log Table -->
|
|
<rs-table
|
|
v-if="filteredLogs && filteredLogs.length > 0"
|
|
:data="filteredLogs.slice(0, 10)"
|
|
:field="logTableFields"
|
|
:options="{
|
|
variant: 'default',
|
|
striped: true,
|
|
borderless: false,
|
|
hover: true,
|
|
}"
|
|
:options-advanced="{
|
|
sortable: true,
|
|
filterable: true,
|
|
responsive: true,
|
|
outsideBorder: true,
|
|
}"
|
|
advanced
|
|
>
|
|
<template #timestamp="{ value }">
|
|
<div class="text-sm">
|
|
<div class="font-medium">{{ formatDate(value.timestamp, true) }}</div>
|
|
<div class="text-gray-500 text-xs">
|
|
{{ new Date(value.timestamp).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 Details
|
|
</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 matching your filters.</p>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
|
|
<!-- Recent Activity -->
|
|
<rs-card>
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<Icon class="mr-2 text-primary" name="ic:outline-history"></Icon>
|
|
<h3 class="text-lg font-semibold text-primary">Recent Activity</h3>
|
|
</div>
|
|
<rs-button
|
|
variant="outline"
|
|
size="sm"
|
|
@click="navigateTo('/notification/log-audit/monitoring')"
|
|
>
|
|
View All
|
|
</rs-button>
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<div class="space-y-3">
|
|
<div
|
|
v-for="(activity, index) in recentActivity"
|
|
:key="index"
|
|
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<div
|
|
class="w-3 h-3 rounded-full mr-3"
|
|
:class="{
|
|
'bg-green-500':
|
|
activity.status === 'sent' ||
|
|
activity.status === 'created' ||
|
|
activity.status === 'opened',
|
|
'bg-yellow-500':
|
|
activity.status === 'queued' || activity.status === 'updated',
|
|
'bg-red-500': activity.status === 'failed',
|
|
'bg-blue-500': activity.status === 'bounced',
|
|
}"
|
|
></div>
|
|
<div>
|
|
<p class="font-medium">{{ activity.action }}</p>
|
|
<p class="text-sm text-gray-600">{{ activity.description }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="text-right">
|
|
<p class="text-sm font-medium capitalize">{{ activity.status }}</p>
|
|
<p class="text-xs text-gray-500">{{ activity.time }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
|
|
<!-- 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.timestamp, 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 }}</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.sourceIp }}</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.errorCode }}
|
|
</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.errorMessage }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<label class="block text-sm font-medium text-gray-500 mb-2"
|
|
>Failed Payload</label
|
|
>
|
|
<pre
|
|
class="bg-gray-100 dark:bg-gray-800 p-3 rounded text-xs max-h-32 overflow-y-auto"
|
|
>{{ selectedLog.failedPayload }}</pre
|
|
>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<label class="block text-sm font-medium text-gray-500 mb-1"
|
|
>Troubleshooting</label
|
|
>
|
|
<div
|
|
class="p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded border-l-4 border-yellow-400"
|
|
>
|
|
<p class="text-sm text-yellow-800 dark:text-yellow-200">
|
|
{{ selectedLog.troubleshooting }}
|
|
</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 } from "vue";
|
|
|
|
// --- Log Filters ---
|
|
const selectedDateRange = ref({ start: null, end: null });
|
|
const filters = ref({
|
|
actor: "",
|
|
channel: null,
|
|
status: null,
|
|
keyword: "",
|
|
});
|
|
|
|
const channelOptions = ["Email", "SMS", "Push Notification", "Webhook"];
|
|
const statusOptions = [
|
|
"Sent",
|
|
"Failed",
|
|
"Bounced",
|
|
"Opened",
|
|
"Queued",
|
|
"Created",
|
|
"Updated",
|
|
];
|
|
|
|
// Prepare options for FormKit select
|
|
const channelOptionsForFormKit = computed(() => [
|
|
{ label: "All Channels", value: null },
|
|
...channelOptions.map((opt) => ({ label: opt, value: opt })),
|
|
]);
|
|
const statusOptionsForFormKit = computed(() => [
|
|
{ label: "All Status", value: null },
|
|
...statusOptions.map((opt) => ({ label: opt, value: opt })),
|
|
]);
|
|
|
|
const formatDate = (date, includeTime = false) => {
|
|
if (!date) return "";
|
|
const options = { year: "numeric", month: "short", day: "numeric" };
|
|
if (includeTime) {
|
|
options.hour = "2-digit";
|
|
options.minute = "2-digit";
|
|
}
|
|
return new Date(date).toLocaleDateString(undefined, options);
|
|
};
|
|
|
|
// --- Log Data & Table ---
|
|
const dummyLogs = ref([
|
|
{
|
|
id: "1",
|
|
timestamp: new Date("2023-10-01T10:00:00Z"),
|
|
action: "Notification Created",
|
|
actor: "Admin User",
|
|
channel: "Email",
|
|
status: "Created",
|
|
sourceIp: "192.168.1.100",
|
|
details: "Welcome email template for new user signup.",
|
|
errorCode: null,
|
|
errorMessage: null,
|
|
failedPayload: null,
|
|
troubleshooting: null,
|
|
},
|
|
{
|
|
id: "2",
|
|
timestamp: new Date("2023-10-01T10:05:00Z"),
|
|
action: "Notification Sent",
|
|
actor: "System",
|
|
channel: "Email",
|
|
status: "Sent",
|
|
sourceIp: "N/A",
|
|
details: "To: user@example.com, Subject: Important Account Update",
|
|
errorCode: null,
|
|
errorMessage: null,
|
|
failedPayload: null,
|
|
troubleshooting: null,
|
|
},
|
|
{
|
|
id: "3",
|
|
timestamp: new Date("2023-10-01T10:15:00Z"),
|
|
action: "Delivery Failed",
|
|
actor: "System",
|
|
channel: "SMS",
|
|
status: "Failed",
|
|
sourceIp: "N/A",
|
|
details: "To: +1234560000 (Invalid Number)",
|
|
errorCode: "SMS-404",
|
|
errorMessage: "The recipient phone number is not valid or not reachable.",
|
|
failedPayload: JSON.stringify({
|
|
to: "+1234560000",
|
|
message: "Your verification code is 789012.",
|
|
}),
|
|
troubleshooting:
|
|
"Check number validity and country code. Ensure the number can receive SMS.",
|
|
},
|
|
{
|
|
id: "4",
|
|
timestamp: new Date("2023-10-02T11:00:00Z"),
|
|
action: "Notification Opened",
|
|
actor: "user@example.com",
|
|
channel: "Email",
|
|
status: "Opened",
|
|
sourceIp: "203.0.113.45",
|
|
details: "Subject: Important Account Update - Opened via Webmail on Chrome.",
|
|
errorCode: null,
|
|
errorMessage: null,
|
|
failedPayload: null,
|
|
troubleshooting: null,
|
|
},
|
|
{
|
|
id: "5",
|
|
timestamp: new Date("2023-10-03T14:30:00Z"),
|
|
action: "Template Updated",
|
|
actor: "marketing_mgr",
|
|
channel: "N/A",
|
|
status: "Updated",
|
|
sourceIp: "192.168.1.150",
|
|
details: 'Promotional Email Template "SummerSale2023" updated with new banner.',
|
|
errorCode: null,
|
|
errorMessage: null,
|
|
failedPayload: null,
|
|
troubleshooting: null,
|
|
},
|
|
{
|
|
id: "6",
|
|
timestamp: new Date("2023-10-04T09:00:00Z"),
|
|
action: "Notification Queued",
|
|
actor: "System",
|
|
channel: "Push Notification",
|
|
status: "Queued",
|
|
sourceIp: "N/A",
|
|
details: 'Targeting segment: "Active Android Users"',
|
|
errorCode: null,
|
|
errorMessage: null,
|
|
failedPayload: null,
|
|
troubleshooting: null,
|
|
},
|
|
{
|
|
id: "7",
|
|
timestamp: new Date("2023-10-04T09:05:00Z"),
|
|
action: "Notification Sent",
|
|
actor: "System",
|
|
channel: "Push Notification",
|
|
status: "Sent",
|
|
sourceIp: "N/A",
|
|
details: "Successfully sent to 5,032 devices.",
|
|
errorCode: null,
|
|
errorMessage: null,
|
|
failedPayload: null,
|
|
troubleshooting: null,
|
|
},
|
|
{
|
|
id: "8",
|
|
timestamp: new Date("2023-10-05T16:20:00Z"),
|
|
action: "Delivery Bounced",
|
|
actor: "System",
|
|
channel: "Email",
|
|
status: "Bounced",
|
|
sourceIp: "N/A",
|
|
details: "To: nonexist@example.com, Hard Bounce",
|
|
errorCode: "SMTP-550",
|
|
errorMessage: "User mailbox does not exist.",
|
|
failedPayload: JSON.stringify({
|
|
to: "nonexist@example.com",
|
|
subject: "Your Weekly Digest",
|
|
}),
|
|
troubleshooting: "Remove email from list. Verify email collection process.",
|
|
},
|
|
|
|
// Add more dummy logs for pagination and filtering
|
|
...Array.from({ length: 22 }, (_, i) => ({
|
|
id: (i + 9).toString(),
|
|
timestamp: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000),
|
|
action: [
|
|
"Notification Created",
|
|
"Notification Sent",
|
|
"Delivery Failed",
|
|
"Notification Opened",
|
|
"Template Updated",
|
|
"Notification Queued",
|
|
"Delivery Bounced",
|
|
][Math.floor(Math.random() * 7)],
|
|
actor: [
|
|
"Admin User",
|
|
"System",
|
|
"john.doe@example.com",
|
|
"jane.doe@example.com",
|
|
"support_agent",
|
|
"marketing_mgr",
|
|
][Math.floor(Math.random() * 6)],
|
|
channel: channelOptions[Math.floor(Math.random() * channelOptions.length)],
|
|
status: statusOptions[Math.floor(Math.random() * statusOptions.length)],
|
|
sourceIp: `192.168.1.${Math.floor(Math.random() * 254) + 1}`,
|
|
details: `Log entry detail for event id ${
|
|
i + 9
|
|
}. This notification was processed with standard parameters.`,
|
|
errorCode:
|
|
Math.random() > 0.85 ? `ERR-${Math.floor(Math.random() * 100) + 200}` : null,
|
|
errorMessage:
|
|
Math.random() > 0.85
|
|
? "A simulated intermittent error occurred during processing."
|
|
: null,
|
|
failedPayload:
|
|
Math.random() > 0.85
|
|
? JSON.stringify({
|
|
data: "some example payload data",
|
|
attempt: Math.floor(Math.random() * 3) + 1,
|
|
})
|
|
: null,
|
|
troubleshooting:
|
|
Math.random() > 0.85
|
|
? "Retry the operation. If persistent, check downstream service status."
|
|
: null,
|
|
})),
|
|
]);
|
|
|
|
// This computed property will be passed to RsTable's `data` prop
|
|
const filteredLogs = computed(() => {
|
|
return dummyLogs.value.filter((log) => {
|
|
const logDate = new Date(log.timestamp);
|
|
|
|
let dateMatch = true;
|
|
if (
|
|
selectedDateRange.value &&
|
|
selectedDateRange.value.start &&
|
|
selectedDateRange.value.end
|
|
) {
|
|
const startDate = new Date(selectedDateRange.value.start);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
const endDate = new Date(selectedDateRange.value.end);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
dateMatch = logDate >= startDate && logDate <= endDate;
|
|
} else if (selectedDateRange.value && selectedDateRange.value.start) {
|
|
const startDate = new Date(selectedDateRange.value.start);
|
|
startDate.setHours(0, 0, 0, 0);
|
|
dateMatch = logDate >= startDate;
|
|
} else if (selectedDateRange.value && selectedDateRange.value.end) {
|
|
const endDate = new Date(selectedDateRange.value.end);
|
|
endDate.setHours(23, 59, 59, 999);
|
|
dateMatch = logDate <= endDate;
|
|
}
|
|
|
|
const actorMatch =
|
|
!filters.value.actor ||
|
|
(log.actor && log.actor.toLowerCase().includes(filters.value.actor.toLowerCase()));
|
|
const channelMatch = !filters.value.channel || log.channel === filters.value.channel;
|
|
const statusMatch = !filters.value.status || log.status === filters.value.status;
|
|
const keywordMatch =
|
|
!filters.value.keyword ||
|
|
(log.details &&
|
|
log.details.toLowerCase().includes(filters.value.keyword.toLowerCase())) ||
|
|
(log.action &&
|
|
log.action.toLowerCase().includes(filters.value.keyword.toLowerCase())) ||
|
|
(log.errorMessage &&
|
|
log.errorMessage.toLowerCase().includes(filters.value.keyword.toLowerCase()));
|
|
|
|
return dateMatch && actorMatch && channelMatch && statusMatch && keywordMatch;
|
|
});
|
|
});
|
|
|
|
// Fields for RsTable
|
|
const logTableFields = ["timestamp", "action", "actor", "channel", "status", "actions"];
|
|
|
|
const applyFilters = () => {
|
|
console.log("Applying filters with values:", filters.value, selectedDateRange.value);
|
|
};
|
|
|
|
// --- Log Detail Modal ---
|
|
const isLogDetailModalOpen = ref(false);
|
|
const selectedLog = ref(null);
|
|
|
|
const viewLogDetails = (log) => {
|
|
selectedLog.value = log;
|
|
isLogDetailModalOpen.value = true;
|
|
};
|
|
|
|
// Summary stats for quick overview
|
|
const summaryStats = computed(() => [
|
|
{
|
|
title: "Total Log Entries",
|
|
value: filteredLogs.value.length.toLocaleString(),
|
|
icon: "ic:outline-list-alt",
|
|
},
|
|
{
|
|
title: "Failed Deliveries",
|
|
value: filteredLogs.value
|
|
.filter((log) => log.status === "Failed")
|
|
.length.toLocaleString(),
|
|
icon: "ic:outline-error",
|
|
},
|
|
{
|
|
title: "Successful Deliveries",
|
|
value: filteredLogs.value
|
|
.filter((log) => log.status === "Sent")
|
|
.length.toLocaleString(),
|
|
icon: "ic:outline-check-circle",
|
|
},
|
|
{
|
|
title: "Success Rate",
|
|
value:
|
|
filteredLogs.value.length > 0
|
|
? Math.round(
|
|
(filteredLogs.value.filter((log) => log.status === "Sent").length /
|
|
filteredLogs.value.length) *
|
|
100
|
|
) + "%"
|
|
: "0%",
|
|
icon: "ic:outline-trending-up",
|
|
},
|
|
]);
|
|
|
|
// Feature cards data for navigation
|
|
const features = ref([
|
|
{
|
|
title: "Analytics Dashboard",
|
|
description:
|
|
"Visual analytics and metrics for notification performance, delivery rates, and user engagement trends.",
|
|
icon: "ic:outline-bar-chart",
|
|
path: "/notification/log-audit/analytics",
|
|
useCases: [
|
|
"Track delivery rates and success metrics",
|
|
"Monitor open rates and click-through rates",
|
|
"Analyze engagement trends over time",
|
|
],
|
|
},
|
|
{
|
|
title: "Real-Time Monitoring",
|
|
description:
|
|
"Live monitoring of notification system performance with real-time alerts and system health indicators.",
|
|
icon: "ic:outline-monitor",
|
|
path: "/notification/log-audit/monitoring",
|
|
useCases: [
|
|
"Monitor ongoing notification activity",
|
|
"Track current system load and performance",
|
|
"Receive alerts for error spikes and failures",
|
|
],
|
|
},
|
|
{
|
|
title: "Reports & Export",
|
|
description:
|
|
"Generate comprehensive reports and export log data for compliance, auditing, and analysis purposes.",
|
|
icon: "ic:outline-file-download",
|
|
path: "/notification/log-audit/reports",
|
|
useCases: [
|
|
"Export logs in CSV, PDF, and Excel formats",
|
|
"Generate GDPR and SOC2 compliance reports",
|
|
"Create custom audit reports for stakeholders",
|
|
],
|
|
},
|
|
]);
|
|
|
|
// Recent activity data (top 5 most recent logs)
|
|
const recentActivity = computed(() => {
|
|
return filteredLogs.value
|
|
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
|
|
.slice(0, 5)
|
|
.map((log) => ({
|
|
action: log.action,
|
|
description: log.details,
|
|
status: log.status.toLowerCase(),
|
|
time: formatTimeAgo(log.timestamp),
|
|
actor: log.actor,
|
|
}));
|
|
});
|
|
|
|
const formatTimeAgo = (timestamp) => {
|
|
const now = new Date();
|
|
const time = new Date(timestamp);
|
|
const diffInMinutes = Math.floor((now - time) / (1000 * 60));
|
|
|
|
if (diffInMinutes < 1) return "Just now";
|
|
if (diffInMinutes < 60) return `${diffInMinutes} minutes ago`;
|
|
if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)} hours ago`;
|
|
return `${Math.floor(diffInMinutes / 1440)} days ago`;
|
|
};
|
|
</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>
|