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

560 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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"));