266 lines
9.8 KiB
Vue
266 lines
9.8 KiB
Vue
<template>
|
|
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
|
<!-- Header -->
|
|
<div class="bg-white dark:bg-gray-800 shadow">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex justify-between items-center py-6">
|
|
<div class="flex items-center">
|
|
<v-icon class="mr-3 text-red-600" size="large">mdi-shield-crown</v-icon>
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Admin Console</h1>
|
|
<p class="text-sm text-gray-600 dark:text-gray-300">System administration and monitoring</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- User info -->
|
|
<div class="flex items-center space-x-4">
|
|
<div class="text-right">
|
|
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ currentUser?.name || currentUser?.email }}</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
|
<v-chip size="x-small" :color="getRoleColor(getHighestRole())" variant="tonal">
|
|
{{ getRoleDisplayName(getHighestRole()) }}
|
|
</v-chip>
|
|
</div>
|
|
</div>
|
|
<v-avatar size="40" color="red">
|
|
<span class="text-white font-bold">{{ getInitials(currentUser?.name || currentUser?.email || 'A') }}</span>
|
|
</v-avatar>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main content -->
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<!-- Quick Stats -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
|
<v-card class="p-6">
|
|
<div class="flex items-center">
|
|
<div class="flex-1">
|
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Total Audit Events</p>
|
|
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ stats?.totalEvents || '...' }}</p>
|
|
</div>
|
|
<v-icon color="blue" size="large">mdi-chart-line</v-icon>
|
|
</div>
|
|
</v-card>
|
|
|
|
<v-card class="p-6">
|
|
<div class="flex items-center">
|
|
<div class="flex-1">
|
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Success Rate</p>
|
|
<p class="text-2xl font-bold text-green-600">{{ successRate }}%</p>
|
|
</div>
|
|
<v-icon color="green" size="large">mdi-check-circle</v-icon>
|
|
</div>
|
|
</v-card>
|
|
|
|
<v-card class="p-6">
|
|
<div class="flex items-center">
|
|
<div class="flex-1">
|
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Active Users</p>
|
|
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ stats?.topUsers?.length || '...' }}</p>
|
|
</div>
|
|
<v-icon color="purple" size="large">mdi-account-multiple</v-icon>
|
|
</div>
|
|
</v-card>
|
|
|
|
<v-card class="p-6">
|
|
<div class="flex items-center">
|
|
<div class="flex-1">
|
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">System Status</p>
|
|
<p class="text-2xl font-bold text-green-600">Healthy</p>
|
|
</div>
|
|
<v-icon color="green" size="large">mdi-heart-pulse</v-icon>
|
|
</div>
|
|
</v-card>
|
|
</div>
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
|
<v-card class="p-6 hover:shadow-lg transition-shadow cursor-pointer" @click="navigateTo('/dashboard/admin/audit-logs')">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Audit Logs</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-300">View system audit trail and user activities</p>
|
|
</div>
|
|
<v-icon color="blue" size="large">mdi-file-document-multiple</v-icon>
|
|
</div>
|
|
</v-card>
|
|
|
|
<v-card class="p-6 hover:shadow-lg transition-shadow cursor-pointer" @click="navigateTo('/dashboard/admin/system-logs')">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">System Logs</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-300">Real-time system logs and monitoring</p>
|
|
</div>
|
|
<v-icon color="green" size="large">mdi-console</v-icon>
|
|
</div>
|
|
</v-card>
|
|
|
|
<v-card class="p-6 hover:shadow-lg transition-shadow cursor-pointer" @click="navigateTo('/dashboard/admin/duplicates')">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Duplicate Management</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-300">Find and merge duplicate interest records</p>
|
|
<v-chip v-if="duplicateCount > 0" size="small" color="warning" class="mt-2">
|
|
{{ duplicateCount }} found
|
|
</v-chip>
|
|
</div>
|
|
<v-icon color="orange" size="large">mdi-content-duplicate</v-icon>
|
|
</div>
|
|
</v-card>
|
|
</div>
|
|
|
|
<!-- Recent Activity -->
|
|
<v-card class="mb-8">
|
|
<v-card-title class="flex items-center justify-between">
|
|
<span>Recent Audit Activity</span>
|
|
<v-btn size="small" variant="text" @click="navigateTo('/dashboard/admin/audit-logs')">
|
|
View All
|
|
<v-icon right>mdi-arrow-right</v-icon>
|
|
</v-btn>
|
|
</v-card-title>
|
|
|
|
<v-card-text v-if="loading" class="text-center py-8">
|
|
<v-progress-circular indeterminate></v-progress-circular>
|
|
<p class="mt-4 text-gray-600">Loading recent activity...</p>
|
|
</v-card-text>
|
|
|
|
<v-card-text v-else-if="recentLogs.length === 0" class="text-center py-8">
|
|
<v-icon size="48" color="gray">mdi-history</v-icon>
|
|
<p class="mt-4 text-gray-600">No recent activity</p>
|
|
</v-card-text>
|
|
|
|
<v-card-text v-else class="pa-0">
|
|
<v-list>
|
|
<v-list-item
|
|
v-for="log in recentLogs"
|
|
:key="log.id"
|
|
class="border-b border-gray-100 dark:border-gray-700 last:border-b-0"
|
|
>
|
|
<template #prepend>
|
|
<v-avatar size="small" :color="log.status === 'success' ? 'green' : 'red'">
|
|
<v-icon color="white" size="small">
|
|
{{ log.status === 'success' ? 'mdi-check' : 'mdi-alert' }}
|
|
</v-icon>
|
|
</v-avatar>
|
|
</template>
|
|
|
|
<v-list-item-title>{{ log.action.replace(/_/g, ' ') }}</v-list-item-title>
|
|
<v-list-item-subtitle class="flex items-center space-x-4">
|
|
<span>{{ log.user_email }}</span>
|
|
<span>•</span>
|
|
<span>{{ formatRelativeTime(log.timestamp) }}</span>
|
|
<span v-if="log.resource_type">•</span>
|
|
<v-chip v-if="log.resource_type" size="x-small" variant="tonal">
|
|
{{ log.resource_type }}
|
|
</v-chip>
|
|
</v-list-item-subtitle>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-card-text>
|
|
</v-card>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue';
|
|
|
|
// Set page metadata for admin access
|
|
definePageMeta({
|
|
middleware: 'authentication',
|
|
layout: 'default',
|
|
roles: ['admin']
|
|
});
|
|
|
|
// Page head
|
|
useHead({
|
|
title: 'Admin Console - Port Nimara'
|
|
});
|
|
|
|
// Get authorization composable
|
|
const { getCurrentUser, getHighestRole, getRoleDisplayName, getRoleColor } = useAuthorization();
|
|
|
|
// Reactive state
|
|
const loading = ref(true);
|
|
const stats = ref(null);
|
|
const recentLogs = ref([]);
|
|
const duplicateCount = ref(0);
|
|
|
|
// Get current user info
|
|
const currentUser = computed(() => getCurrentUser());
|
|
|
|
// Calculate success rate
|
|
const successRate = computed(() => {
|
|
if (!stats.value) return 0;
|
|
const total = stats.value.totalEvents;
|
|
const success = stats.value.successEvents;
|
|
return total > 0 ? Math.round((success / total) * 100) : 100;
|
|
});
|
|
|
|
// Helper functions
|
|
const getInitials = (name: string) => {
|
|
if (!name) return 'A';
|
|
const parts = name.split(' ');
|
|
if (parts.length >= 2) {
|
|
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
}
|
|
return name.substring(0, 2).toUpperCase();
|
|
};
|
|
|
|
const formatRelativeTime = (timestamp: string) => {
|
|
const now = new Date();
|
|
const time = new Date(timestamp);
|
|
const diffMs = now.getTime() - time.getTime();
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
const diffHours = Math.floor(diffMins / 60);
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
|
|
if (diffMins < 1) return 'Just now';
|
|
if (diffMins < 60) return `${diffMins}m ago`;
|
|
if (diffHours < 24) return `${diffHours}h ago`;
|
|
if (diffDays < 7) return `${diffDays}d ago`;
|
|
return time.toLocaleDateString();
|
|
};
|
|
|
|
// Load dashboard data
|
|
const loadDashboardData = async () => {
|
|
loading.value = true;
|
|
|
|
try {
|
|
// Load audit stats
|
|
const statsResponse = await $fetch('/api/admin/audit-logs/stats', {
|
|
params: { days: 30 }
|
|
});
|
|
|
|
if (statsResponse.success) {
|
|
stats.value = statsResponse.data;
|
|
}
|
|
|
|
// Load recent audit logs
|
|
const logsResponse = await $fetch('/api/admin/audit-logs/list', {
|
|
params: { limit: 10, offset: 0 }
|
|
});
|
|
|
|
if (logsResponse.success) {
|
|
recentLogs.value = logsResponse.data;
|
|
}
|
|
|
|
// TODO: Load duplicate count when duplicate detection is implemented
|
|
// const duplicatesResponse = await $fetch('/api/admin/duplicates/check');
|
|
// duplicateCount.value = duplicatesResponse.count || 0;
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load dashboard data:', error);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
loadDashboardData();
|
|
});
|
|
</script>
|