174 lines
8.8 KiB
TypeScript
174 lines
8.8 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { ChevronDown, Filter, Loader2, User } from 'lucide-react';
|
|
import { auditLogService } from '@/services/audit-log-service';
|
|
import type { AuditLog } from '@/types/audit-log';
|
|
import { useAppSelector } from '@/hooks/redux-hooks';
|
|
import { cn } from '@/lib/utils';
|
|
import { Card, CardHeader, CardContent } from '@/components/ui/card';
|
|
import { StatusBadge } from '@/components/shared';
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
// Helper functions (kept from original)
|
|
const getActionVariant = (action: string): 'success' | 'failure' | 'info' | 'process' => {
|
|
const lowerAction = action.toLowerCase();
|
|
if (lowerAction.includes('create') || lowerAction.includes('register')) return 'success';
|
|
if (lowerAction.includes('update') || lowerAction.includes('version_update') || lowerAction.includes('login')) return 'info';
|
|
if (lowerAction.includes('delete') || lowerAction.includes('deregister')) return 'failure';
|
|
if (lowerAction.includes('read') || lowerAction.includes('get') || lowerAction.includes('status_change')) return 'process';
|
|
return 'info';
|
|
};
|
|
|
|
const formatRelativeTime = (dateString: string): string => {
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
|
if (diffInSeconds < 60) return 'Just now';
|
|
const diffInMinutes = Math.floor(diffInSeconds / 60);
|
|
if (diffInMinutes < 60) return `${diffInMinutes} min ago`;
|
|
const diffInHours = Math.floor(diffInMinutes / 60);
|
|
if (diffInHours < 24) return `${diffInHours} hours ago`;
|
|
const diffInDays = Math.floor(diffInHours / 24);
|
|
if (diffInDays === 1) return 'Yesterday';
|
|
return date.toLocaleDateString();
|
|
};
|
|
|
|
export interface RecentActivityProps {
|
|
variant?: 'list' | 'table';
|
|
}
|
|
|
|
export const RecentActivity = ({ variant }: RecentActivityProps) => {
|
|
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
|
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
const { tenantId } = useAppSelector((state) => state.auth);
|
|
|
|
// Default to table variant for a more professional look
|
|
const activeVariant = variant || 'table';
|
|
|
|
useEffect(() => {
|
|
const fetchRecentActivity = async (): Promise<void> => {
|
|
try {
|
|
setIsLoading(true);
|
|
// Pass tenantId if we're on a tenant route to get tenant-specific logs
|
|
const filterTenantId = window.location.pathname.startsWith('/tenant') ? tenantId : null;
|
|
const response = await auditLogService.getAll(1, 5, { tenant_id: filterTenantId }, ['created_at', 'desc']);
|
|
if (response.success) {
|
|
setAuditLogs(response.data);
|
|
}
|
|
} catch (err: any) {
|
|
// Fallback or log if needed
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchRecentActivity();
|
|
}, [tenantId, activeVariant]);
|
|
|
|
if (activeVariant === 'list') {
|
|
return (
|
|
<div className="bg-white rounded-xl flex flex-col h-full border border-[#e5e7eb] shadow-sm overflow-hidden">
|
|
<div className="px-6 py-5 border-b border-[#f1f5f9] flex justify-between items-center">
|
|
<h2 className="text-[16px] font-bold text-[#111827] tracking-tight">Recent Activity</h2>
|
|
<span className="text-[11px] font-bold text-gray-400">Last 24 hours</span>
|
|
</div>
|
|
|
|
<div className="flex-1">
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center py-20">
|
|
<Loader2 className="w-6 h-6 text-[#084cc8] animate-spin" />
|
|
</div>
|
|
) : auditLogs.length === 0 ? (
|
|
<div className="p-12 text-center text-[12px] text-gray-400 font-medium">No recent activity</div>
|
|
) : (
|
|
<div className="flex flex-col">
|
|
{auditLogs.map((log, index) => (
|
|
<div key={log.id} className={cn("px-6 py-4 flex items-center gap-4 transition-colors hover:bg-gray-50", index !== auditLogs.length - 1 && "border-b border-[#f1f5f9 ]")}>
|
|
<span className="text-[11px] font-medium text-gray-400 w-20 shrink-0">{formatRelativeTime(log.created_at)}</span>
|
|
<div className="w-8 h-8 rounded-full bg-gray-100 border border-gray-200 flex items-center justify-center shrink-0 overflow-hidden">
|
|
<User className="w-4 h-4 text-gray-400" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-1.5 flex-wrap">
|
|
<span className="text-[12px] font-bold text-[#111827]">{log.user?.email?.split('@')[0] || 'System User'}</span>
|
|
<span className="text-[12px] font-medium text-gray-500 whitespace-nowrap">{log.action.toLowerCase().includes('create') ? 'created' : 'performed'}</span>
|
|
<span className="text-[12px] font-bold text-[#084cc8] hover:underline cursor-pointer truncate">{log.resource_type}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// TABLE VARIANT (Original design for superadmin dashboard)
|
|
return (
|
|
<Card className="flex-1 border-[rgba(0,0,0,0.12)] w-full">
|
|
<CardHeader className="border-b border-[rgba(0,0,0,0.08)] pb-3 md:pb-4 pt-3 md:pt-4 px-4 md:px-5 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-2">
|
|
<h2 className="text-sm md:text-[15px] font-semibold text-[#0f1724]">Recent Activity</h2>
|
|
<div className="flex items-center gap-2 w-full sm:w-auto">
|
|
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded px-2.5 py-1.5 flex items-center gap-1.5 flex-1 sm:flex-initial">
|
|
<span className="text-[11px] font-medium text-[#0f1724]">Action</span>
|
|
<span className="text-[11px] font-normal text-[#6b7280]">All</span>
|
|
<ChevronDown className="w-3 h-3" />
|
|
</div>
|
|
<Button variant="ghost" size="sm" className="gap-1 px-1 min-h-[44px]">
|
|
<Filter className="w-3 h-3" />
|
|
<span className="text-[11px] font-normal text-[#6b7280] hidden md:inline">Filters</span>
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="w-5 h-5 text-[#112868] animate-spin" />
|
|
</div>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full border-collapse">
|
|
<thead>
|
|
<tr className="bg-white border-b border-[rgba(0,0,0,0.08)]">
|
|
<th className="px-5 py-4 text-left text-xs font-bold text-[#374151] uppercase tracking-wider">User Profile</th>
|
|
<th className="px-5 py-4 text-left text-xs font-bold text-[#374151] uppercase tracking-wider">Timestamp</th>
|
|
<th className="px-5 py-4 text-left text-xs font-bold text-[#374151] uppercase tracking-wider">Resource Type</th>
|
|
<th className="px-5 py-4 text-left text-xs font-bold text-[#374151] uppercase tracking-wider">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-[rgba(0,0,0,0.04)]">
|
|
{auditLogs.map((log) => (
|
|
<tr key={log.id} className="hover:bg-gray-50/50 transition-colors">
|
|
<td className="px-5 py-4 whitespace-nowrap">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center shrink-0 border border-gray-200">
|
|
<User className="w-4 h-4 text-gray-400" />
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<span className="text-[13px] font-bold text-[#111827]">
|
|
{log.user ? `${log.user.first_name || ''} ${log.user.last_name || ''}`.trim() || log.user.email.split('@')[0] : 'System User'}
|
|
</span>
|
|
{log.user && <span className="text-[10px] text-gray-400 font-medium">{log.user.email}</span>}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-5 py-4 whitespace-nowrap">
|
|
<span className="text-[12px] font-medium text-gray-500">{formatRelativeTime(log.created_at)}</span>
|
|
</td>
|
|
<td className="px-5 py-4 whitespace-nowrap">
|
|
<span className="text-[12px] font-bold text-[#084cc8]">{log.resource_type}</span>
|
|
</td>
|
|
<td className="px-5 py-4 whitespace-nowrap">
|
|
<StatusBadge variant={getActionVariant(log.action)}>{log.action}</StatusBadge>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|