383 lines
11 KiB
Vue
383 lines
11 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">
|
|
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>
|