Basdat/web/js/pages/admin/tabs/AuditLogTab.js
2025-12-20 00:01:08 +07:00

311 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// assets/js/pages/admin/tabs/AuditLogTab.js
const AuditLogTab = ({ state, handlers }) => {
const {
filteredAuditLogs, // Data yang ditampilkan (sekarang langsung dari API)
auditLogSearchTerm,
setAuditLogSearchTerm,
auditLogActionFilter,
setAuditLogActionFilter,
auditLogUserFilter,
setAuditLogUserFilter,
auditLogDateFilter,
setAuditLogDateFilter,
// State Pagination
currentPage,
totalPages,
totalRecords,
loading,
} = state;
const { loadAuditLogs } = handlers;
// ✅ UPDATED: Trigger load data saat halaman atau filter berubah
// Menggunakan debounce untuk search text agar tidak spam API
React.useEffect(() => {
const timer = setTimeout(() => {
// Reset ke halaman 1 jika filter berubah, tapi tetap di halaman x jika hanya navigasi
loadAuditLogs(currentPage);
}, 500); // Delay 500ms
return () => clearTimeout(timer);
}, [
currentPage,
auditLogActionFilter,
auditLogUserFilter,
auditLogDateFilter,
]);
// Catatan: auditLogSearchTerm bisa dimasukkan dependency jika backend support text search
// Handler ganti halaman
const handlePageChange = (newPage) => {
state.setCurrentPage(newPage);
// useEffect di atas akan otomatis memanggil API karena currentPage berubah
};
// Helper styles (Tetap sama seperti sebelumnya)
const getActionStyle = (action) => {
const styles = {
login: {
icon: "🔐",
color: "text-blue-400",
bg: "bg-blue-500/10",
border: "border-blue-500/30",
},
logout: {
icon: "🚪",
color: "text-gray-400",
bg: "bg-gray-500/10",
border: "border-gray-500/30",
},
create_item: {
icon: "",
color: "text-green-400",
bg: "bg-green-500/10",
border: "border-green-500/30",
},
update_item: {
icon: "✏️",
color: "text-yellow-400",
bg: "bg-yellow-500/10",
border: "border-yellow-500/30",
},
delete_item: {
icon: "🗑️",
color: "text-red-400",
bg: "bg-red-500/10",
border: "border-red-500/30",
},
create_claim: {
icon: "🤝",
color: "text-blue-400",
bg: "bg-blue-500/10",
border: "border-blue-500/30",
},
verify_claim: {
icon: "✅",
color: "text-green-400",
bg: "bg-green-500/10",
border: "border-green-500/30",
},
reject_claim: {
icon: "❌",
color: "text-red-400",
bg: "bg-red-500/10",
border: "border-red-500/30",
},
close_case: {
icon: "📋",
color: "text-purple-400",
bg: "bg-purple-500/10",
border: "border-purple-500/30",
},
reopen_case: {
icon: "🔄",
color: "text-orange-400",
bg: "bg-orange-500/10",
border: "border-orange-500/30",
},
update_user_role: {
icon: "👤",
color: "text-yellow-400",
bg: "bg-yellow-500/10",
border: "border-yellow-500/30",
},
block_user: {
icon: "🚫",
color: "text-red-400",
bg: "bg-red-500/10",
border: "border-red-500/30",
},
unblock_user: {
icon: "✅",
color: "text-green-400",
bg: "bg-green-500/10",
border: "border-green-500/30",
},
export_report: {
icon: "📊",
color: "text-cyan-400",
bg: "bg-cyan-500/10",
border: "border-cyan-500/30",
},
};
return (
styles[action] || {
icon: "📌",
color: "text-slate-400",
bg: "bg-slate-500/10",
border: "border-slate-500/30",
}
);
};
const formatActionName = (action) => {
return action
.split("_")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
};
// Daftar aksi unik untuk dropdown (bisa hardcode atau ambil dari API/konstanta jika perlu)
// Karena pagination server-side, kita tidak bisa ambil unique dari 'all data' di frontend.
// Sebaiknya sediakan list statis atau endpoint khusus. Disini kita pakai statis umum.
const actionOptions = [
"login",
"logout",
"create_item",
"update_item",
"delete_item",
"create_claim",
"verify_claim",
"reject_claim",
"close_case",
"reopen_case",
"update_user_role",
"block_user",
"unblock_user",
"export_report",
];
return (
<div className="bg-gradient-to-br from-slate-800 to-slate-900 rounded-xl shadow-xl border border-slate-700">
<div className="p-6 border-b border-slate-700">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-white">📜 Audit Log</h2>
<div className="flex gap-3">
<button
onClick={() => loadAuditLogs(1)} // Reset ke halaman 1 saat refresh manual
className="px-4 py-2 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg font-semibold hover:from-blue-700 hover:to-blue-800 transition shadow-lg"
>
🔄 Refresh
</button>
</div>
</div>
{/* Filters */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
{/* Note: Search text frontend mungkin tidak jalan sempurna jika backend belum support LIKE query global */}
<input
type="text"
placeholder="Cari aktivitas..."
value={auditLogSearchTerm}
onChange={(e) => setAuditLogSearchTerm(e.target.value)}
disabled // Sementara disable jika backend belum support full-text search global
className="px-4 py-2 bg-slate-800 border-2 border-slate-600 rounded-xl text-slate-500 cursor-not-allowed"
title="Pencarian teks belum aktif"
/>
<select
value={auditLogActionFilter}
onChange={(e) => {
setAuditLogActionFilter(e.target.value);
state.setCurrentPage(1); // Reset page saat filter berubah
}}
className="px-4 py-2 bg-slate-700 border-2 border-slate-600 rounded-xl text-white focus:border-blue-500 focus:outline-none"
>
<option value="">Semua Aksi</option>
{actionOptions.map((action) => (
<option key={action} value={action}>
{formatActionName(action)}
</option>
))}
</select>
<input
type="text"
placeholder="Filter User ID..."
value={auditLogUserFilter}
onChange={(e) => {
setAuditLogUserFilter(e.target.value);
// state.setCurrentPage(1); // Optional: reset page immediately
}}
className="px-4 py-2 bg-slate-700 border-2 border-slate-600 rounded-xl text-white placeholder-slate-400 focus:border-blue-500 focus:outline-none"
/>
<input
type="date"
value={auditLogDateFilter}
onChange={(e) => setAuditLogDateFilter(e.target.value)}
className="px-4 py-2 bg-slate-700 border-2 border-slate-600 rounded-xl text-white focus:border-blue-500 focus:outline-none"
/>
</div>
</div>
<div className="p-6">
{loading ? (
<div className="text-center py-12 text-slate-400">
<div className="text-6xl mb-4 animate-pulse"></div>
<p>Memuat data...</p>
</div>
) : (
<div className="space-y-4">
{filteredAuditLogs.map((log, index) => {
const style = getActionStyle(log.action);
return (
<div
key={log.id}
className={`${style.bg} border-2 ${style.border} rounded-xl p-4 hover:shadow-xl transition-all relative`}
>
{/* ... (Isi Card Log tetap sama seperti sebelumnya) ... */}
<div className="flex items-start gap-4">
<div className={`text-3xl ${style.color} flex-shrink-0`}>
{style.icon}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-4 mb-2">
<div className="flex-1">
<h3 className={`text-lg font-bold ${style.color}`}>
{formatActionName(log.action)}
</h3>
<div className="flex items-center gap-2 text-sm text-slate-400 mt-1">
<span className="text-white font-medium">
{log.user_name || "System"}
</span>
<span></span>
<span>
{Helpers.formatDateTime(log.created_at)}
</span>
</div>
</div>
</div>
{log.details && (
<div className="bg-slate-900/50 p-3 rounded-lg border border-slate-700 mt-3">
<p className="text-sm text-slate-300">
{log.details}
</p>
</div>
)}
</div>
</div>
</div>
);
})}
</div>
)}
{/* Empty State */}
{!loading && filteredAuditLogs.length === 0 && (
<div className="text-center py-12 text-slate-400">
<div className="text-6xl mb-4">📜</div>
<p>Tidak ada data log.</p>
</div>
)}
{/* ✅ ADDED: Pagination Component */}
{!loading && filteredAuditLogs.length > 0 && (
<Pagination
currentPage={currentPage}
totalPages={totalPages}
totalRecords={totalRecords}
onPageChange={handlePageChange}
itemsPerPage={10} // Sesuai limit backend
/>
)}
</div>
</div>
);
};
window.AuditLogTab = AuditLogTab;