Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/ActivityLogPage.tsx
yaschalew 697799b00d fix
2026-07-10 14:24:58 +03:00

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