Yaltopia-Ticket-Admin/src/pages/activity-log/index.tsx
2026-04-08 09:28:01 +03:00

195 lines
7.6 KiB
TypeScript

import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Search,
ChevronLeft,
ChevronRight,
Filter,
Terminal,
} from "lucide-react";
import { auditService, type AuditLog } from "@/services";
import { format } from "date-fns";
import { cn } from "@/lib/utils";
export default function ActivityLogPage() {
const [page, setPage] = useState(1);
const [limit] = useState(15);
const [search, setSearch] = useState("");
const { data: auditData, isLoading } = useQuery({
queryKey: ["activity-log", page, limit, search],
queryFn: async () => {
const params: Record<string, string | number> = { page, limit };
if (search) params.search = search;
return await auditService.getAuditLogs(params);
},
});
const getActionColor = (action: string) => {
const act = action.toUpperCase();
if (act.includes("CREATE"))
return "text-blue-600 bg-blue-50 border-blue-100";
if (act.includes("UPDATE"))
return "text-emerald-600 bg-emerald-50 border-emerald-100";
if (act.includes("DELETE"))
return "text-rose-600 bg-rose-50 border-rose-100";
if (act.includes("LOGIN"))
return "text-purple-600 bg-purple-50 border-purple-100";
return "text-slate-600 bg-slate-50 border-slate-100";
};
return (
<div className="space-y-8 max-w-7xl mx-auto bg-white p-4 min-h-screen">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-gray-900 tracking-tight">
Activity Log
</h1>
<p className="text-gray-500 mt-1">
Audit trail of all administrative actions.
</p>
</div>
<div className="flex items-center gap-2">
{/* View only access: Export button removed */}
</div>
</div>
<Card className="border shadow-none rounded-none">
<CardHeader className="border-b pb-4 flex flex-row items-center justify-between space-y-0">
<CardTitle className="text-xs font-bold uppercase tracking-widest text-gray-400">
System Audit
</CardTitle>
<div className="flex items-center gap-2">
<div className="relative w-64">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<Input
className="pl-10 h-9 rounded-none border-gray-200 text-xs"
placeholder="Search activity or user..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<Button
variant="outline"
size="sm"
className="h-9 rounded-none border-gray-200"
>
<Filter className="w-4 h-4 mr-2" /> Filter
</Button>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead className="bg-gray-50 border-b">
<tr>
<th className="px-6 py-4 text-[10px] font-bold text-gray-400 uppercase tracking-widest">
Action
</th>
<th className="px-6 py-4 text-[10px] font-bold text-gray-400 uppercase tracking-widest">
User ID
</th>
<th className="px-6 py-4 text-[10px] font-bold text-gray-400 uppercase tracking-widest">
Resource
</th>
<th className="px-6 py-4 text-[10px] font-bold text-gray-400 uppercase tracking-widest">
IP Address
</th>
<th className="px-6 py-4 text-[10px] font-bold text-gray-400 uppercase tracking-widest text-right">
Timestamp
</th>
</tr>
</thead>
<tbody className="divide-y">
{isLoading ? (
<tr>
<td
colSpan={5}
className="px-6 py-20 text-center text-gray-400 animate-pulse font-medium uppercase tracking-widest text-[10px]"
>
Synchronizing audit records...
</td>
</tr>
) : auditData?.data && auditData.data.length > 0 ? (
auditData.data.map((log: AuditLog) => (
<tr
key={log.id}
className="hover:bg-gray-50 transition-colors group"
>
<td className="px-6 py-4">
<span
className={cn(
"px-2 py-0.5 text-[9px] font-bold uppercase tracking-widest rounded-none border",
getActionColor(log.action),
)}
>
{log.action}
</span>
</td>
<td className="px-6 py-4 text-sm font-bold text-gray-900 tracking-tighter">
{log.userId || "SYSTEM"}
</td>
<td className="px-6 py-4 text-[10px] font-bold text-gray-400 uppercase flex items-center gap-1.5 mt-4">
<Terminal className="w-3 h-3" /> {log.resourceType}:{" "}
{log.resourceId.substring(0, 8)}...
</td>
<td className="px-6 py-4 text-xs font-mono text-gray-500">
{log.ipAddress || "--"}
</td>
<td className="px-6 py-4 text-right text-xs text-gray-500">
{format(new Date(log.timestamp), "MMM dd, HH:mm:ss")}
</td>
</tr>
))
) : (
<tr>
<td
colSpan={5}
className="px-6 py-20 text-center text-gray-400 italic"
>
No activity logs recorded.
</td>
</tr>
)}
</tbody>
</table>
</div>
</CardContent>
{auditData && auditData.totalPages > 1 && (
<div className="p-4 border-t flex items-center justify-between bg-gray-50/30">
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">
Page {auditData.page} of {auditData.totalPages} ({auditData.total}{" "}
records)
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-none"
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
>
<ChevronLeft className="w-4 h-4" />
</Button>
<Button
variant="outline"
size="icon"
className="h-8 w-8 rounded-none"
onClick={() =>
setPage((p) => Math.min(auditData.totalPages, p + 1))
}
disabled={page === auditData.totalPages}
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
</div>
)}
</Card>
</div>
);
}