311 lines
10 KiB
JavaScript
311 lines
10 KiB
JavaScript
// 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;
|