Qassure-frontend/src/features/dashboard/components/RecentActivity.tsx

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>
);
};