diff --git a/frontend/app/components/admin/audit/ActionBadge.tsx b/frontend/app/components/admin/audit/ActionBadge.tsx new file mode 100644 index 0000000..95b429f --- /dev/null +++ b/frontend/app/components/admin/audit/ActionBadge.tsx @@ -0,0 +1,100 @@ +"use client"; + +import React from "react"; +import { cn } from "@/lib/utils"; + +interface ActionBadgeProps { + action: string; + className?: string; +} + +interface ActionConfig { + icon: string; + bgColor: string; + textColor: string; + borderColor: string; +} + +function getActionConfig(action: string): ActionConfig { + const lowerAction = action.toLowerCase(); + + if (lowerAction.includes("create")) { + return { + icon: "✨", + bgColor: "bg-green-100", + textColor: "text-green-800", + borderColor: "border-green-200", + }; + } + + if (lowerAction.includes("update") || lowerAction.includes("edit")) { + return { + icon: "📝", + bgColor: "bg-blue-100", + textColor: "text-blue-800", + borderColor: "border-blue-200", + }; + } + + if (lowerAction.includes("delete")) { + return { + icon: "🗑️", + bgColor: "bg-red-100", + textColor: "text-red-800", + borderColor: "border-red-200", + }; + } + + if (lowerAction.includes("login")) { + return { + icon: "🔐", + bgColor: "bg-purple-100", + textColor: "text-purple-800", + borderColor: "border-purple-200", + }; + } + + if (lowerAction.includes("logout")) { + return { + icon: "🚪", + bgColor: "bg-gray-100", + textColor: "text-gray-800", + borderColor: "border-gray-200", + }; + } + + if (lowerAction.includes("toggle") || lowerAction.includes("status")) { + return { + icon: "🔄", + bgColor: "bg-orange-100", + textColor: "text-orange-800", + borderColor: "border-orange-200", + }; + } + + return { + icon: "📌", + bgColor: "bg-gray-100", + textColor: "text-gray-800", + borderColor: "border-gray-200", + }; +} + +export function ActionBadge({ action, className }: ActionBadgeProps) { + const config = getActionConfig(action); + + return ( + + {config.icon} + {action} + + ); +} diff --git a/frontend/app/components/admin/audit/AuditLogsTable.tsx b/frontend/app/components/admin/audit/AuditLogsTable.tsx new file mode 100644 index 0000000..71ff016 --- /dev/null +++ b/frontend/app/components/admin/audit/AuditLogsTable.tsx @@ -0,0 +1,321 @@ +"use client"; + +import React, { useState, useMemo } from "react"; +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + getFilteredRowModel, + getPaginationRowModel, + flexRender, + SortingState, + ColumnDef, +} from "@tanstack/react-table"; +import { ArrowUpDown, Search } from "lucide-react"; +import { motion } from "framer-motion"; +import { formatDistanceToNow } from "date-fns"; +import { AuditLog, AuditEntity } from "@/lib/types/audit"; +import { ActionBadge } from "./ActionBadge"; +import { Button } from "../../ui/Button"; +import { cn } from "@/lib/utils"; + +interface AuditLogsTableProps { + logs: AuditLog[]; +} + +export function AuditLogsTable({ logs }: AuditLogsTableProps) { + const [sorting, setSorting] = useState([ + { id: "timestamp", desc: true }, + ]); + const [globalFilter, setGlobalFilter] = useState(""); + const [entityFilter, setEntityFilter] = useState("all"); + const [actionFilter, setActionFilter] = useState("all"); + + // Extract unique action types (first word of each action) + const actionTypes = useMemo(() => { + const actions = new Set(); + logs.forEach((log) => { + const firstWord = log.action.split(" ")[0].toLowerCase(); + actions.add(firstWord); + }); + return Array.from(actions).sort(); + }, [logs]); + + // Filter data based on entity and action filters + const filteredData = useMemo(() => { + return logs.filter((log) => { + const matchesEntity = + entityFilter === "all" || log.entityType === entityFilter; + const matchesAction = + actionFilter === "all" || + log.action.toLowerCase().startsWith(actionFilter.toLowerCase()); + return matchesEntity && matchesAction; + }); + }, [logs, entityFilter, actionFilter]); + + const columns = useMemo[]>( + () => [ + { + accessorKey: "action", + header: ({ column }) => ( + + ), + cell: ({ row }) => , + }, + { + accessorKey: "entityType", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {row.original.entityType} + + ), + }, + { + accessorKey: "admin", + header: "Admin", + cell: ({ row }) => { + const admin = row.original.admin; + if (!admin) { + return ( + System/Unknown + ); + } + return ( +
+
+ {admin.username} +
+
{admin.email}
+
+ ); + }, + enableSorting: false, + }, + { + accessorKey: "details", + header: "Details", + cell: ({ row }) => { + const details = row.original.details; + if (!details) { + return ; + } + return ( +
+ + {details.length > 50 ? `${details.slice(0, 50)}...` : details} + +
+ ); + }, + enableSorting: false, + }, + { + accessorKey: "timestamp", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {formatDistanceToNow(new Date(row.original.timestamp), { + addSuffix: true, + })} + + ), + }, + ], + [] + ); + + const table = useReactTable({ + data: filteredData, + columns, + state: { + sorting, + globalFilter, + }, + onSortingChange: setSorting, + onGlobalFilterChange: setGlobalFilter, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + initialState: { + pagination: { + pageSize: 15, + }, + }, + }); + + return ( +
+ {/* Filters */} +
+ {/* Search */} +
+ + setGlobalFilter(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-transparent" + /> +
+ + {/* Entity Type Filter */} + + + {/* Action Type Filter */} + +
+ + {/* Table */} +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.length === 0 ? ( + + + + ) : ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + )) + )} + +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ No audit logs found +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
+
+ + {/* Pagination */} + {table.getPageCount() > 1 && ( +
+
+ Showing{" "} + + {table.getState().pagination.pageIndex * + table.getState().pagination.pageSize + + 1} + {" "} + to{" "} + + {Math.min( + (table.getState().pagination.pageIndex + 1) * + table.getState().pagination.pageSize, + table.getFilteredRowModel().rows.length + )} + {" "} + of{" "} + + {table.getFilteredRowModel().rows.length} + {" "} + results +
+ +
+ + +
+
+ )} +
+
+ ); +}