mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 23:00:57 +00:00
269 lines
9.5 KiB
TypeScript
269 lines
9.5 KiB
TypeScript
import { useState, useEffect, useCallback } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import i18n from "i18next";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/shared/common/ui/card";
|
|
import {
|
|
Table,
|
|
TableHeader,
|
|
TableRow,
|
|
TableHead,
|
|
TableBody,
|
|
TableCell,
|
|
} from "@/shared/common/ui/table";
|
|
import { Input } from "@/shared/common/ui/input";
|
|
import { Button } from "@/shared/common/ui/button";
|
|
import { Search, RefreshCw, ChevronLeft, ChevronRight } from "lucide-react";
|
|
import { listAuditLogExtensions } from "@/shared/services/audit/audit.api";
|
|
|
|
interface ActivityLog {
|
|
id: string;
|
|
createdAt: string;
|
|
entityName: string;
|
|
queryMethod: "INSERT" | "UPDATE" | "DELETE" | string;
|
|
user: {
|
|
id: string;
|
|
name: {
|
|
am: string;
|
|
en: string;
|
|
};
|
|
email: string;
|
|
username?: string;
|
|
};
|
|
}
|
|
|
|
const ITEMS_PER_PAGE = 10;
|
|
|
|
export default function ActivityLogPage() {
|
|
const { t } = useTranslation();
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [allLogs, setAllLogs] = useState<ActivityLog[]>([]);
|
|
const [filteredLogs, setFilteredLogs] = useState<ActivityLog[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
|
|
const fetchAuditLogs = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
// Fetch all 1000 records from super admin endpoint
|
|
const data = await listAuditLogExtensions(
|
|
"/audit-log-extensions/audit/superAdmin",
|
|
{
|
|
skip: 0,
|
|
take: 1000,
|
|
orderBy: "createdAt:DESC",
|
|
}
|
|
);
|
|
|
|
const logs = (data.items || []) as ActivityLog[];
|
|
setAllLogs(logs);
|
|
setCurrentPage(1);
|
|
} catch (error: any) {
|
|
console.error("Error fetching audit logs:", error);
|
|
setAllLogs([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
fetchAuditLogs();
|
|
// Refresh every 30 seconds
|
|
const interval = setInterval(fetchAuditLogs, 30000);
|
|
return () => clearInterval(interval);
|
|
}, [fetchAuditLogs]);
|
|
|
|
// Paginate the logs whenever currentPage changes
|
|
useEffect(() => {
|
|
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
|
const endIndex = startIndex + ITEMS_PER_PAGE;
|
|
let filtered = allLogs.slice(startIndex, endIndex);
|
|
|
|
// Apply search filter
|
|
if (searchQuery) {
|
|
filtered = filtered.filter(
|
|
(log) =>
|
|
log.entityName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
log.user.name.am.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
log.user.name.en.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
log.user.email.toLowerCase().includes(searchQuery.toLowerCase()),
|
|
);
|
|
}
|
|
|
|
setFilteredLogs(filtered);
|
|
}, [allLogs, currentPage, searchQuery]);
|
|
|
|
const totalPages = Math.ceil(allLogs.length / ITEMS_PER_PAGE);
|
|
const hasNextPage = currentPage < totalPages;
|
|
const hasPrevPage = currentPage > 1;
|
|
|
|
return (
|
|
<div className="p-6 space-y-6">
|
|
<Card className="shadow-lg border-gray-200 dark:border-gray-700">
|
|
<CardHeader>
|
|
<div className="flex justify-between items-center">
|
|
<CardTitle className="text-xl font-semibold">
|
|
{t("activityLogPage.title")}
|
|
</CardTitle>
|
|
<Button
|
|
variant="outline"
|
|
onClick={fetchAuditLogs}
|
|
disabled={loading}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<RefreshCw className={`w-4 h-4 ${loading ? "animate-spin" : ""}`} />
|
|
{t("common.refresh", "Refresh")}
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex items-center gap-4 mb-6">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400 dark:text-gray-500" />
|
|
<Input
|
|
placeholder={t("activityLogPage.searchPlaceholder")}
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-md border border-gray-200 dark:border-gray-700">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-[180px]">
|
|
{t("activityLogPage.table.time")}
|
|
</TableHead>
|
|
<TableHead className="w-[400px]">
|
|
{t("activityLogPage.table.description")}
|
|
</TableHead>
|
|
<TableHead>
|
|
{t("activityLogPage.table.performedBy")}
|
|
</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{loading ? (
|
|
<TableRow>
|
|
<TableCell colSpan={5} className="h-24 text-center">
|
|
<div className="flex items-center justify-center gap-2">
|
|
<RefreshCw className="h-4 w-4 animate-spin" />
|
|
<span>{t("common.loading", "Loading...")}</span>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
) : filteredLogs.length > 0 ? (
|
|
filteredLogs.map((log) => {
|
|
const userName =
|
|
i18n.language === "am" ? log.user.name.am : log.user.name.en;
|
|
const entityLabel = log.entityName.replace(/_/g, " ");
|
|
const actionVerb =
|
|
log.queryMethod.toLowerCase() === "insert"
|
|
? "created"
|
|
: log.queryMethod.toLowerCase() === "update"
|
|
? "updated"
|
|
: log.queryMethod.toLowerCase() === "delete"
|
|
? "deleted"
|
|
: log.queryMethod.toLowerCase();
|
|
const timestamp = new Date(log.createdAt).toLocaleString(
|
|
i18n.language,
|
|
{
|
|
year: "numeric",
|
|
month: "short",
|
|
day: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
}
|
|
);
|
|
|
|
return (
|
|
<TableRow key={log.id}>
|
|
<TableCell className="font-medium">{timestamp}</TableCell>
|
|
<TableCell>
|
|
<p className="text-sm text-gray-900 dark:text-gray-100">
|
|
<span className="font-semibold">{userName}</span>
|
|
{" "}
|
|
<span className="text-gray-600 dark:text-gray-300">
|
|
{actionVerb}
|
|
</span>
|
|
{" "}
|
|
<span className="text-gray-900 dark:text-gray-100 font-medium">
|
|
{entityLabel}
|
|
</span>
|
|
{" "}
|
|
<span className="text-gray-500 dark:text-gray-400">at {timestamp}</span>
|
|
</p>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div>
|
|
<p className="font-medium">{userName}</p>
|
|
<p className="text-xs text-gray-500">{log.user.email}</p>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})
|
|
) : (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={3}
|
|
className="h-24 text-center text-muted-foreground"
|
|
>
|
|
{t("activityLogPage.table.noLogs")}
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{/* Pagination Footer */}
|
|
{allLogs.length > 0 && (
|
|
<div className="flex items-center justify-between mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
<div className="text-xs text-gray-600 dark:text-gray-400">
|
|
{t("common.showing", "Showing")} {(currentPage - 1) * ITEMS_PER_PAGE + 1}-
|
|
{Math.min(currentPage * ITEMS_PER_PAGE, allLogs.length)}{" "}
|
|
{t("common.of", "of")} {allLogs.length}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setCurrentPage(currentPage - 1)}
|
|
disabled={!hasPrevPage || loading}
|
|
className="h-8 w-8 p-0"
|
|
>
|
|
<ChevronLeft className="h-4 w-4" />
|
|
</Button>
|
|
|
|
<div className="flex items-center gap-1">
|
|
<span className="text-xs font-medium text-gray-700 dark:text-gray-300 px-2">
|
|
{currentPage} / {totalPages || 1}
|
|
</span>
|
|
</div>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setCurrentPage(currentPage + 1)}
|
|
disabled={!hasNextPage || loading}
|
|
className="h-8 w-8 p-0"
|
|
>
|
|
<ChevronRight className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|