560 lines
20 KiB
JavaScript
560 lines
20 KiB
JavaScript
// assets/js/pages/admin/AdminApp.js - WITH CATEGORIES TAB
|
||
const { useState, useEffect } = React;
|
||
|
||
const AdminApp = () => {
|
||
const state = useAdminState();
|
||
const handlers = useAdminHandlers(state);
|
||
|
||
const {
|
||
user,
|
||
setUser,
|
||
activeTab,
|
||
setActiveTab,
|
||
stats,
|
||
users,
|
||
setFilteredUsers,
|
||
currentPage,
|
||
searchTerm,
|
||
roleFilter,
|
||
statusFilter,
|
||
showEditModal,
|
||
setShowEditModal,
|
||
selectedUser,
|
||
toast,
|
||
setToast,
|
||
loading,
|
||
showItemDetailModal,
|
||
setShowItemDetailModal,
|
||
selectedItemDetail,
|
||
showClaimDetailModal,
|
||
setShowClaimDetailModal,
|
||
selectedClaim,
|
||
showArchiveDetailModal,
|
||
setShowArchiveDetailModal,
|
||
selectedArchive,
|
||
} = state;
|
||
|
||
const { loadData, loadItems, showToast, handleUpdateUser } = handlers;
|
||
|
||
useEffect(() => {
|
||
if (!AuthUtils.checkAuthAndRedirect("admin")) return;
|
||
const currentUser = AuthUtils.getCurrentUser();
|
||
setUser(currentUser);
|
||
loadData();
|
||
handlers.loadCategories(); // ✅ Load categories on mount
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (user) {
|
||
loadData(); // Ini memuat Users menggunakan currentPage (sudah benar di handler)
|
||
loadItems();
|
||
handlers.loadClaims();
|
||
handlers.loadArchives();
|
||
|
||
// ✅ PERBAIKAN: Kirim currentPage agar tidak reset ke 1
|
||
handlers.loadAuditLogs(currentPage);
|
||
}
|
||
}, [user, currentPage]);
|
||
|
||
useEffect(() => {
|
||
if (currentPage !== 1) {
|
||
state.setCurrentPage(1);
|
||
} else {
|
||
filterUsers();
|
||
}
|
||
}, [searchTerm, roleFilter, statusFilter]);
|
||
|
||
useEffect(() => {
|
||
filterUsers();
|
||
}, [users]);
|
||
|
||
useEffect(() => {
|
||
filterItemsData();
|
||
}, [
|
||
state.itemSearchTerm,
|
||
state.itemStatusFilter,
|
||
state.itemCategoryFilter,
|
||
state.items,
|
||
]);
|
||
|
||
const filterUsers = () => {
|
||
let filtered = users.filter((u) => {
|
||
const matchesSearch =
|
||
u.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
u.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
u.nrp.includes(searchTerm);
|
||
const matchesRole = !roleFilter || u.role === roleFilter;
|
||
const matchesStatus =
|
||
!statusFilter || (u.status || "active") === statusFilter;
|
||
return matchesSearch && matchesRole && matchesStatus;
|
||
});
|
||
setFilteredUsers(filtered);
|
||
};
|
||
|
||
const filterItemsData = () => {
|
||
let filtered = state.items.filter((item) => {
|
||
const matchesSearch =
|
||
item.name.toLowerCase().includes(state.itemSearchTerm.toLowerCase()) ||
|
||
item.location
|
||
.toLowerCase()
|
||
.includes(state.itemSearchTerm.toLowerCase());
|
||
const matchesStatus =
|
||
!state.itemStatusFilter || item.status === state.itemStatusFilter;
|
||
const matchesCategory =
|
||
!state.itemCategoryFilter ||
|
||
Helpers.getCategoryValue(item.category_id) === state.itemCategoryFilter;
|
||
return matchesSearch && matchesStatus && matchesCategory;
|
||
});
|
||
state.setFilteredItems(filtered);
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-blue-900 to-slate-900">
|
||
<Navbar user={user} onLogout={handlers.handleLogout} userType="admin" />
|
||
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||
<div className="mb-8">
|
||
<h1 className="text-3xl font-bold text-white mb-2">
|
||
Dashboard Admin
|
||
</h1>
|
||
<p className="text-slate-400">Kelola sistem Lost & Found</p>
|
||
</div>
|
||
|
||
{/* Stats */}
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||
<StatCard
|
||
title="Total User"
|
||
value={stats.users?.total || 0}
|
||
icon="👥"
|
||
/>
|
||
<StatCard
|
||
title="Total Barang"
|
||
value={stats.items?.total || 0}
|
||
icon="📦"
|
||
/>
|
||
<StatCard
|
||
title="Total Klaim"
|
||
value={stats.claims?.total || 0}
|
||
icon="🤝"
|
||
/>
|
||
<StatCard
|
||
title="Kategori"
|
||
value={state.categories?.length || 0}
|
||
icon="🏷️"
|
||
colorClass="text-yellow-400"
|
||
/>
|
||
<StatCard
|
||
title="Di Arsip"
|
||
value={stats.archives?.total || 0}
|
||
icon="📂"
|
||
colorClass="text-green-400"
|
||
/>
|
||
<StatCard
|
||
title="Audit Log"
|
||
value={stats.audit_logs?.total || 0}
|
||
icon="📜"
|
||
colorClass="text-purple-400"
|
||
/>
|
||
</div>
|
||
|
||
{/* Tabs */}
|
||
<div className="flex gap-2 mb-6 flex-wrap">
|
||
<button
|
||
onClick={() => setActiveTab("users")}
|
||
className={`px-6 py-3 rounded-xl font-semibold transition ${
|
||
activeTab === "users"
|
||
? "bg-gradient-to-r from-blue-600 to-blue-700 text-white shadow-lg shadow-blue-500/50"
|
||
: "bg-slate-800 text-slate-300 hover:bg-slate-700 border border-slate-700"
|
||
}`}
|
||
>
|
||
👥 Kelola User
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => {
|
||
setActiveTab("roles");
|
||
handlers.loadRolesAndPermissions();
|
||
}}
|
||
className={`px-6 py-3 rounded-xl font-semibold transition ${
|
||
activeTab === "roles"
|
||
? "bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg"
|
||
: "bg-slate-800 text-slate-300 hover:bg-slate-700 border border-slate-700"
|
||
}`}
|
||
>
|
||
🔑 Role & Akses
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setActiveTab("items")}
|
||
className={`px-6 py-3 rounded-xl font-semibold transition ${
|
||
activeTab === "items"
|
||
? "bg-gradient-to-r from-blue-600 to-blue-700 text-white shadow-lg shadow-blue-500/50"
|
||
: "bg-slate-800 text-slate-300 hover:bg-slate-700 border border-slate-700"
|
||
}`}
|
||
>
|
||
📦 Kelola Barang
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setActiveTab("lostitems")}
|
||
className={`px-6 py-3 rounded-t-xl font-semibold transition ${
|
||
activeTab === "lostitems"
|
||
? "bg-gradient-to-br from-slate-800 to-slate-900 text-white border-t-2 border-x-2 border-slate-700"
|
||
: "bg-slate-700/50 text-slate-300 hover:bg-slate-700 hover:text-white"
|
||
}`}
|
||
>
|
||
🔍 Laporan Hilang
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setActiveTab("claims")}
|
||
className={`px-6 py-3 rounded-xl font-semibold transition ${
|
||
activeTab === "claims"
|
||
? "bg-gradient-to-r from-blue-600 to-blue-700 text-white shadow-lg shadow-blue-500/50"
|
||
: "bg-slate-800 text-slate-300 hover:bg-slate-700 border border-slate-700"
|
||
}`}
|
||
>
|
||
🤝 Kelola Klaim
|
||
</button>
|
||
|
||
{/* ✅ NEW: Categories Tab */}
|
||
<button
|
||
onClick={() => {
|
||
setActiveTab("categories");
|
||
handlers.loadCategories();
|
||
}}
|
||
className={`px-6 py-3 rounded-xl font-semibold transition ${
|
||
activeTab === "categories"
|
||
? "bg-gradient-to-r from-yellow-600 to-yellow-700 text-white shadow-lg shadow-yellow-500/50"
|
||
: "bg-slate-800 text-slate-300 hover:bg-slate-700 border border-slate-700"
|
||
}`}
|
||
>
|
||
🏷️ Kelola Kategori
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => {
|
||
setActiveTab("archives");
|
||
handlers.loadArchives();
|
||
}}
|
||
className={`px-6 py-3 rounded-xl font-semibold transition ${
|
||
activeTab === "archives"
|
||
? "bg-gradient-to-r from-green-600 to-green-700 text-white shadow-lg shadow-green-500/50"
|
||
: "bg-slate-800 text-slate-300 hover:bg-slate-700 border border-slate-700"
|
||
}`}
|
||
>
|
||
📂 Kelola Arsip
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => {
|
||
setActiveTab("audit-log");
|
||
handlers.loadAuditLogs();
|
||
}}
|
||
className={`px-6 py-3 rounded-xl font-semibold transition ${
|
||
activeTab === "audit-log"
|
||
? "bg-gradient-to-r from-purple-600 to-purple-700 text-white shadow-lg shadow-purple-500/50"
|
||
: "bg-slate-800 text-slate-300 hover:bg-slate-700 border border-slate-700"
|
||
}`}
|
||
>
|
||
📜 Audit Log
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setActiveTab("reports")}
|
||
className={`px-6 py-3 rounded-xl font-semibold transition ${
|
||
activeTab === "reports"
|
||
? "bg-gradient-to-r from-blue-600 to-blue-700 text-white shadow-lg shadow-blue-500/50"
|
||
: "bg-slate-800 text-slate-300 hover:bg-slate-700 border border-slate-700"
|
||
}`}
|
||
>
|
||
📊 Laporan
|
||
</button>
|
||
</div>
|
||
|
||
{/* Tab Content */}
|
||
{activeTab === "users" && window.UsersTab && (
|
||
<UsersTab state={state} handlers={handlers} />
|
||
)}
|
||
{activeTab === "roles" && window.RolesTab && (
|
||
<RolesTab state={state} handlers={handlers} />
|
||
)}
|
||
{activeTab === "items" && window.ItemsTab && (
|
||
<ItemsTab state={state} handlers={handlers} />
|
||
)}
|
||
{activeTab === "lostitems" && window.LostItemsTabAdmin && (
|
||
<>
|
||
<LostItemsTabAdmin state={state} handlers={handlers} />
|
||
{window.CreateLostItemModal && (
|
||
<CreateLostItemModal state={state} handlers={handlers} />
|
||
)}
|
||
{window.EditLostItemModal && (
|
||
<EditLostItemModal state={state} handlers={handlers} />
|
||
)}
|
||
{window.LostItemDetailModal && (
|
||
<LostItemDetailModal state={state} />
|
||
)}
|
||
</>
|
||
)}
|
||
{activeTab === "claims" && window.ClaimsTabAdmin && (
|
||
<ClaimsTabAdmin state={state} handlers={handlers} />
|
||
)}
|
||
{activeTab === "categories" && window.CategoriesTab && (
|
||
<CategoriesTab state={state} handlers={handlers} />
|
||
)}
|
||
{activeTab === "archives" && window.ArchivesTabAdmin && (
|
||
<ArchivesTabAdmin state={state} handlers={handlers} />
|
||
)}
|
||
{activeTab === "audit-log" && window.AuditLogTab && (
|
||
<AuditLogTab state={state} handlers={handlers} />
|
||
)}
|
||
{activeTab === "reports" && window.ReportsTab && (
|
||
<ReportsTab handlers={handlers} loading={loading} />
|
||
)}
|
||
</div>
|
||
|
||
{/* Modals */}
|
||
{window.ClaimDetailModalAdmin && (
|
||
<ClaimDetailModalAdmin
|
||
isOpen={showClaimDetailModal}
|
||
onClose={() => setShowClaimDetailModal(false)}
|
||
claim={selectedClaim}
|
||
/>
|
||
)}
|
||
{window.ArchiveDetailModal && (
|
||
<ArchiveDetailModal
|
||
isOpen={showArchiveDetailModal}
|
||
onClose={() => setShowArchiveDetailModal(false)}
|
||
archive={selectedArchive}
|
||
/>
|
||
)}
|
||
{window.RoleModal && (
|
||
<>
|
||
<RoleModal
|
||
isOpen={state.showCreateRoleModal}
|
||
onClose={() => state.setShowCreateRoleModal(false)}
|
||
allPermissions={state.permissions}
|
||
onSubmit={handlers.handleCreateRole}
|
||
loading={loading}
|
||
/>
|
||
<RoleModal
|
||
isOpen={state.showEditRoleModal}
|
||
onClose={() => {
|
||
state.setShowEditRoleModal(false);
|
||
state.setSelectedRole(null);
|
||
}}
|
||
role={state.selectedRole}
|
||
allPermissions={state.permissions}
|
||
onSubmit={handlers.handleUpdateRole}
|
||
loading={loading}
|
||
/>
|
||
</>
|
||
)}
|
||
{window.CreateClaimModal && (
|
||
<CreateClaimModal state={state} handlers={handlers} />
|
||
)}
|
||
{window.EditClaimModal && (
|
||
<EditClaimModal state={state} handlers={handlers} />
|
||
)}
|
||
|
||
{/* Edit User Modal */}
|
||
<Modal
|
||
isOpen={showEditModal}
|
||
onClose={() => setShowEditModal(false)}
|
||
title="Edit User"
|
||
>
|
||
{selectedUser && (
|
||
<form onSubmit={handleUpdateUser} className="space-y-4">
|
||
<div>
|
||
<label className="block font-semibold mb-2 text-slate-300">
|
||
Nama
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={selectedUser.name}
|
||
disabled
|
||
className="w-full px-4 py-3 bg-slate-700 border-2 border-slate-600 rounded-xl text-slate-400"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block font-semibold mb-2 text-slate-300">
|
||
Email
|
||
</label>
|
||
<input
|
||
type="email"
|
||
value={selectedUser.email}
|
||
disabled
|
||
className="w-full px-4 py-3 bg-slate-700 border-2 border-slate-600 rounded-xl text-slate-400"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block font-semibold mb-2 text-slate-300">
|
||
Role *
|
||
</label>
|
||
<select
|
||
name="role"
|
||
defaultValue={selectedUser.role}
|
||
required
|
||
className="w-full px-4 py-3 bg-slate-700 border-2 border-slate-600 rounded-xl text-white focus:border-blue-500 focus:outline-none"
|
||
>
|
||
<option value="user">User</option>
|
||
<option value="manager">Manager</option>
|
||
<option value="admin">Admin</option>
|
||
</select>
|
||
</div>
|
||
<button
|
||
type="submit"
|
||
disabled={loading}
|
||
className="w-full px-4 py-3 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-xl hover:from-blue-700 hover:to-blue-800 transition font-semibold disabled:from-slate-600 disabled:to-slate-700 shadow-lg"
|
||
>
|
||
{loading ? "Menyimpan..." : "Update User"}
|
||
</button>
|
||
</form>
|
||
)}
|
||
</Modal>
|
||
|
||
{/* Item Detail Modal */}
|
||
<Modal
|
||
isOpen={showItemDetailModal}
|
||
onClose={() => setShowItemDetailModal(false)}
|
||
title="Detail Barang"
|
||
>
|
||
{selectedItemDetail && (
|
||
<div>
|
||
<img
|
||
src={
|
||
selectedItemDetail.photo_url ||
|
||
"https://via.placeholder.com/600x400"
|
||
}
|
||
alt={selectedItemDetail.name}
|
||
className="w-full h-64 object-cover rounded-xl mb-4 border-2 border-slate-600"
|
||
/>
|
||
<h3 className="text-2xl font-bold mb-4 text-white">
|
||
{selectedItemDetail.name}
|
||
</h3>
|
||
<div className="space-y-3 text-slate-300">
|
||
<div className="bg-slate-700/50 p-3 rounded-lg">
|
||
<strong className="text-blue-400">Kategori:</strong>
|
||
<span className="ml-2">
|
||
{selectedItemDetail.category ||
|
||
Helpers.getCategoryName(selectedItemDetail.category_id)}
|
||
</span>
|
||
</div>
|
||
<div className="bg-slate-700/50 p-3 rounded-lg">
|
||
<strong className="text-blue-400">Lokasi:</strong>
|
||
<span className="ml-2">{selectedItemDetail.location}</span>
|
||
</div>
|
||
<div className="bg-slate-700/50 p-3 rounded-lg">
|
||
<strong className="text-blue-400">Tanggal Ditemukan:</strong>
|
||
<span className="ml-2">
|
||
{Helpers.formatDate(selectedItemDetail.date_found)}
|
||
</span>
|
||
</div>
|
||
<div className="bg-slate-700/50 p-3 rounded-lg">
|
||
<strong className="text-blue-400">Status:</strong>
|
||
<span
|
||
className={`ml-2 px-3 py-1 rounded-full text-xs font-semibold ${Helpers.getStatusBadgeClass(
|
||
selectedItemDetail.status
|
||
)}`}
|
||
>
|
||
{selectedItemDetail.status}
|
||
</span>
|
||
</div>
|
||
<div className="bg-slate-700/50 p-3 rounded-lg">
|
||
<strong className="text-blue-400">Deskripsi Umum:</strong>
|
||
<p className="text-slate-300 mt-2">
|
||
{selectedItemDetail.description || "Tidak ada deskripsi"}
|
||
</p>
|
||
</div>
|
||
<div className="bg-slate-700/50 p-3 rounded-lg">
|
||
<strong className="text-blue-400">Pelapor:</strong>
|
||
<span className="ml-2">
|
||
{selectedItemDetail.reporter_name || "Tidak ada data"}
|
||
</span>
|
||
</div>
|
||
<div className="bg-slate-700/50 p-3 rounded-lg">
|
||
<strong className="text-blue-400">Kontak Pelapor:</strong>
|
||
<span className="ml-2">
|
||
{selectedItemDetail.reporter_contact || "Tidak ada data"}
|
||
</span>
|
||
</div>
|
||
<div className="bg-yellow-500/10 border-2 border-yellow-500/30 p-4 rounded-xl">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<span className="text-2xl">🔒</span>
|
||
<strong className="text-yellow-400 text-lg">
|
||
Ciri Khusus Rahasia (Untuk Verifikasi)
|
||
</strong>
|
||
</div>
|
||
<p className="text-slate-300 leading-relaxed">
|
||
{selectedItemDetail.secret_details ||
|
||
"Tidak ada deskripsi rahasia"}
|
||
</p>
|
||
<p className="text-xs text-yellow-400 mt-2">
|
||
⚠️ Info ini RAHASIA - gunakan untuk verifikasi klaim
|
||
</p>
|
||
</div>
|
||
{selectedItemDetail.case_closed_at && (
|
||
<div className="bg-green-500/10 border-2 border-green-500/30 p-4 rounded-xl">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<span className="text-2xl">📋</span>
|
||
<strong className="text-green-400">Case Closed</strong>
|
||
</div>
|
||
<div className="space-y-1 text-sm text-slate-300">
|
||
{selectedItemDetail.berita_acara_no && (
|
||
<div>
|
||
No. BA:{" "}
|
||
<strong className="text-white">
|
||
{selectedItemDetail.berita_acara_no}
|
||
</strong>
|
||
</div>
|
||
)}
|
||
<div>
|
||
Ditutup:{" "}
|
||
{Helpers.formatDateTime(
|
||
selectedItemDetail.case_closed_at
|
||
)}
|
||
</div>
|
||
{selectedItemDetail.case_closed_by_name && (
|
||
<div>Oleh: {selectedItemDetail.case_closed_by_name}</div>
|
||
)}
|
||
{selectedItemDetail.case_closed_notes && (
|
||
<div className="mt-2 pt-2 border-t border-green-500/30">
|
||
Catatan: {selectedItemDetail.case_closed_notes}
|
||
</div>
|
||
)}
|
||
</div>
|
||
{selectedItemDetail.bukti_serah_terima && (
|
||
<a
|
||
href={selectedItemDetail.bukti_serah_terima}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="inline-block mt-2 px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700 transition"
|
||
>
|
||
📄 Lihat Bukti
|
||
</a>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</Modal>
|
||
|
||
{/* Toast */}
|
||
{toast && (
|
||
<Toast
|
||
message={toast.message}
|
||
type={toast.type}
|
||
onClose={() => setToast(null)}
|
||
/>
|
||
)}
|
||
|
||
{/* AI Chatbot */}
|
||
<AIChatbot />
|
||
</div>
|
||
);
|
||
};
|
||
|
||
|
||
// Render
|
||
ReactDOM.render(<AdminApp />, document.getElementById("root")); |