import { useState, useEffect } from 'react'; import type { ReactElement } from 'react'; import { Layout } from '@/components/layout/Layout'; import { ViewAuditLogModal, DataTable, Pagination, FilterDropdown, StatusBadge, type Column, } from '@/components/shared'; import { Download, ArrowUpDown, Search } from 'lucide-react'; import { auditLogService } from '@/services/audit-log-service'; import { moduleService } from '@/services/module-service'; import type { AuditLog } from '@/types/audit-log'; import { useAppTheme } from '@/hooks/useAppTheme'; import { PrimaryButton } from '@/components/shared'; import { useAppSelector } from '@/hooks/redux-hooks'; // Helper function to format date const formatDate = (dateString: string): string => { const date = new Date(dateString); return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit', }); }; // Helper function to get action badge variant 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'; }; // Helper function to get method badge variant const getMethodVariant = (method: string | null): 'success' | 'failure' | 'info' | 'process' => { if (!method) return 'info'; const upperMethod = method.toUpperCase(); if (upperMethod === 'GET') return 'success'; if (upperMethod === 'POST') return 'info'; if (upperMethod === 'PUT' || upperMethod === 'PATCH') return 'process'; if (upperMethod === 'DELETE') return 'failure'; return 'info'; }; // Helper function to get status badge color based on response status const getStatusColor = (status: number | null): string => { if (!status) return 'text-[#6b7280]'; if (status >= 200 && status < 300) return 'text-[#10b981]'; if (status >= 300 && status < 400) return 'text-[#f59e0b]'; if (status >= 400) return 'text-[#ef4444]'; return 'text-[#6b7280]'; }; const AuditLogs = (): ReactElement => { const { primaryColor } = useAppTheme(); const roles = useAppSelector((state) => state.auth.roles); const tenantId = useAppSelector((state) => state.auth.tenantId); const isTenantAdmin = roles?.includes('tenant_admin'); const [auditLogs, setAuditLogs] = useState([]); const [resourceTypes, setResourceTypes] = useState<{ value: string; label: string }[]>([]); const [modules, setModules] = useState<{ value: string; label: string }[]>([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); // Pagination state const [currentPage, setCurrentPage] = useState(1); const [limit, setLimit] = useState(10); const [pagination, setPagination] = useState<{ page: number; limit: number; total: number; totalPages: number; hasMore: boolean; }>({ page: 1, limit: 10, total: 0, totalPages: 1, hasMore: false, }); // Filter state const [methodFilter, setMethodFilter] = useState(null); const [actionFilter, setActionFilter] = useState(null); const [resourceTypeFilter, setResourceTypeFilter] = useState(null); const [moduleIdFilter, setModuleIdFilter] = useState(null); // New module_id filter const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [orderBy, setOrderBy] = useState(null); const [search, setSearch] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); // View modal const [viewModalOpen, setViewModalOpen] = useState(false); const [selectedAuditLogId, setSelectedAuditLogId] = useState(null); const fetchResourceTypes = async (): Promise => { try { const response = await auditLogService.getResourceTypesDropdown(tenantId as string | undefined); if (response.success) { const options = response.data.map((rt: any) => ({ value: rt.value, label: rt.label })); setResourceTypes(options); } } catch (err) { console.error('Failed to load resource types', err); } }; const fetchModules = async (): Promise => { try { const response = await moduleService.getMyModules(tenantId as string | undefined); if (response.success) { const options = response.data.map((m: any) => ({ value: m.id, label: m.name })); setModules(options); } } catch (err) { console.error('Failed to load modules', err); } }; const fetchAuditLogs = async (): Promise => { try { setIsLoading(true); setError(null); let response; if (isTenantAdmin) { response = await auditLogService.getAll( currentPage, limit, { method: methodFilter, action: actionFilter, resource_type: resourceTypeFilter, module_id: moduleIdFilter, // Pass module_id filter startDate: startDate || null, endDate: endDate || null, tenant_id: tenantId as string | null, search: debouncedSearch || null }, orderBy ); } else { response = await auditLogService.getMyLogs(currentPage, limit, moduleIdFilter, debouncedSearch || null); } if (response.success) { setAuditLogs(response.data); setPagination(response.pagination); } else { setError('Failed to load audit logs'); } } catch (err: any) { setError(err?.response?.data?.error?.message || 'Failed to load audit logs'); } finally { setIsLoading(false); } }; // Fetch resource types and modules on mount useEffect(() => { fetchResourceTypes(); fetchModules(); }, [tenantId]); // Debouncing for Search useEffect(() => { const timer = setTimeout(() => { setDebouncedSearch(search); setCurrentPage(1); }, 500); return () => clearTimeout(timer); }, [search]); // Fetch audit logs on mount and when pagination/filters change useEffect(() => { fetchAuditLogs(); }, [currentPage, limit, methodFilter, actionFilter, resourceTypeFilter, moduleIdFilter, startDate, endDate, orderBy, tenantId, debouncedSearch]); // View audit log handler const handleViewAuditLog = (auditLogId: string): void => { setSelectedAuditLogId(auditLogId); setViewModalOpen(true); }; // Export handler const handleExport = async (): Promise => { try { const response = await auditLogService.export({ format: 'json', startDate: startDate || undefined, endDate: endDate || undefined, tenantId: tenantId || undefined }); if (response.success) { // In a real app, we'd trigger a file download here. // For now, we'll just log and show a message since the response is JSON. console.log('Export data:', response.data.records); const blob = new Blob([JSON.stringify(response.data.records, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `audit-logs-${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(link); link.click(); document.body.removeChild(link); } } catch (err: any) { alert('Failed to export audit logs: ' + (err.message || 'Unknown error')); } }; // Load audit log for view const loadAuditLog = async (id: string): Promise => { const response = await auditLogService.getById(id); return response.data; }; // Define table columns const columns: Column[] = [ { key: 'created_at', label: 'Timestamp', render: (log) => ( {formatDate(log.created_at)} ), mobileLabel: 'Time', }, { key: 'resource_type', label: 'Resource Type', render: (log) => ( {log.resource_type} ), }, { key: 'module', label: 'Module', render: (log) => ( {log.module?.name || 'Platform'} ), }, { key: 'action', label: 'Action', render: (log) => ( {log.action} ), }, ...(isTenantAdmin ? [{ key: 'user', label: 'User', render: (log: AuditLog) => ( {log.user ? `${log.user.first_name} ${log.user.last_name}` : (log.user_email || 'N/A')} ), }] : []), { key: 'request_method', label: 'Method', render: (log) => ( {log.request_method ? log.request_method.toUpperCase() : 'N/A'} ), }, { key: 'response_status', label: 'Status', render: (log) => ( {log.response_status || 'N/A'} ), }, { key: 'ip_address', label: 'IP Address', render: (log) => ( {log.ip_address || 'N/A'} ), }, { key: 'actions', label: 'Actions', align: 'right', render: (log) => (
), }, ]; // Mobile card renderer const mobileCardRenderer = (log: AuditLog) => (

{log.resource_type}

{log.action}
Timestamp:

{formatDate(log.created_at)}

Module:

{log.module?.name || 'Platform'}

User:

{log.user ? `${log.user.first_name} ${log.user.last_name}` : (log.user_email || 'N/A')}

Method:
{log.request_method ? log.request_method.toUpperCase() : 'N/A'}
Status:

{log.response_status || 'N/A'}

); return ( {/* Table Container */}
{/* Table Header with Filters */}
{/* Search and Filters */}
{/* Global Search */}
setSearch(e.target.value)} className="w-full pl-9 pr-4 py-2 border border-[rgba(0,0,0,0.08)] rounded-md text-sm focus:outline-none focus:ring-2 transition-all bg-gray-50/30" onFocus={(e) => { e.currentTarget.style.borderColor = primaryColor; e.currentTarget.style.boxShadow = `0 0 0 2px ${primaryColor}1A`; }} onBlur={(e) => { e.currentTarget.style.borderColor = 'rgba(0,0,0,0.08)'; e.currentTarget.style.boxShadow = 'none'; }} />
{isTenantAdmin && ( <> {/* Action Filter */} { setActionFilter(value as string | null); setCurrentPage(1); }} placeholder="All Actions" /> {/* Resource Type Filter */} { setResourceTypeFilter(value as string | null); setCurrentPage(1); }} placeholder="All Resources" isSearchable /> )} {/* Module Filter */} { setModuleIdFilter(value as string | null); setCurrentPage(1); }} placeholder="All Modules" /> {/* Sort Filter - Always show as it's useful for everyone */} { setOrderBy(value as string[] | null); setCurrentPage(1); }} placeholder="Newest" showIcon icon={} />
{/* Actions */}
{isTenantAdmin && ( Export )}
{/* Date Filters - Separated row for better spacing */}
From: { setStartDate(e.target.value); setCurrentPage(1); }} className="text-xs px-2 py-1.5 border border-[rgba(0,0,0,0.08)] rounded focus:outline-none focus:ring-1 focus:ring-[#112868]/20" />
To: { setEndDate(e.target.value); setCurrentPage(1); }} className="text-xs px-2 py-1.5 border border-[rgba(0,0,0,0.08)] rounded focus:outline-none focus:ring-1 focus:ring-[#112868]/20" />
{(startDate || endDate || actionFilter || resourceTypeFilter || methodFilter || moduleIdFilter || search) && ( )}
{/* Table */} log.id} isLoading={isLoading} error={error} mobileCardRenderer={mobileCardRenderer} emptyMessage={isTenantAdmin ? "No audit logs found matching your criteria" : "No activity recorded for your account yet"} /> {/* Pagination */} {pagination.total > 0 && (
setCurrentPage(page)} onLimitChange={(newLimit: number) => { setLimit(newLimit); setCurrentPage(1); }} />
)}
{/* View Audit Log Modal */} { setViewModalOpen(false); setSelectedAuditLogId(null); }} auditLogId={selectedAuditLogId} onLoadAuditLog={loadAuditLog} />
); }; export default AuditLogs;