267 lines
10 KiB
JavaScript
267 lines
10 KiB
JavaScript
// assets/js/pages/admin/tabs/ItemsTab.js
|
||
|
||
const ItemsTab = ({ state, handlers }) => {
|
||
const {
|
||
showExpired,
|
||
setShowExpired,
|
||
itemSearchTerm,
|
||
setItemSearchTerm,
|
||
itemStatusFilter,
|
||
setItemStatusFilter,
|
||
itemCategoryFilter,
|
||
setItemCategoryFilter,
|
||
filteredItems,
|
||
items,
|
||
showCreateItemModal,
|
||
setShowCreateItemModal,
|
||
photoPreview,
|
||
itemPage,
|
||
itemTotalPages,
|
||
itemTotalRecords,
|
||
loading,
|
||
} = state;
|
||
|
||
const {
|
||
handleViewItemDetail,
|
||
handleCreateItem,
|
||
handleEditItemClick,
|
||
handleDeleteItem,
|
||
handlePhotoChange,
|
||
loadItems,
|
||
} = handlers;
|
||
|
||
// ✅ Gunakan useEffect untuk reload data saat Filter / Page berubah
|
||
// Kita menggunakan debounce (tunda) untuk search agar tidak spam API
|
||
React.useEffect(() => {
|
||
const timer = setTimeout(() => {
|
||
// Reset ke halaman 1 jika filter berubah
|
||
// Jika hanya halaman yang berubah, load halaman tersebut
|
||
loadItems(itemPage);
|
||
}, 500);
|
||
|
||
return () => clearTimeout(timer);
|
||
}, [
|
||
itemPage,
|
||
itemSearchTerm,
|
||
itemStatusFilter,
|
||
itemCategoryFilter,
|
||
showExpired,
|
||
]);
|
||
|
||
// Reset page ke 1 jika filter berubah (kecuali itemPage itu sendiri)
|
||
React.useEffect(() => {
|
||
state.setItemPage(1);
|
||
}, [itemSearchTerm, itemStatusFilter, itemCategoryFilter, showExpired]);
|
||
|
||
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">
|
||
{showExpired ? "📦 Barang Expired" : "📦 Kelola Barang"}
|
||
</h2>
|
||
|
||
<div className="flex gap-3">
|
||
<button
|
||
onClick={() => setShowCreateItemModal(true)}
|
||
className="px-4 py-2 bg-gradient-to-r from-green-600 to-green-700 text-white rounded-lg font-semibold hover:from-green-700 hover:to-green-800 transition shadow-lg"
|
||
>
|
||
➕ Tambah Barang
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setShowExpired(false)}
|
||
className={`px-4 py-2 rounded-lg font-semibold transition ${
|
||
!showExpired
|
||
? "bg-gradient-to-r from-blue-600 to-blue-700 text-white shadow-lg"
|
||
: "bg-slate-700 text-slate-300 hover:bg-slate-600"
|
||
}`}
|
||
>
|
||
📦 Aktif
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setShowExpired(true)}
|
||
className={`px-4 py-2 rounded-lg font-semibold transition ${
|
||
showExpired
|
||
? "bg-gradient-to-r from-red-600 to-red-700 text-white shadow-lg"
|
||
: "bg-slate-700 text-slate-300 hover:bg-slate-600"
|
||
}`}
|
||
>
|
||
⚠️ Expired
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex gap-3 flex-wrap">
|
||
<input
|
||
type="text"
|
||
placeholder="Cari barang (nama/lokasi)..."
|
||
value={itemSearchTerm}
|
||
onChange={(e) => setItemSearchTerm(e.target.value)}
|
||
className="flex-1 min-w-[200px] 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"
|
||
/>
|
||
<select
|
||
value={itemStatusFilter}
|
||
onChange={(e) => setItemStatusFilter(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"
|
||
>
|
||
<option value="">Semua Status</option>
|
||
<option value="unclaimed">Unclaimed</option>
|
||
<option value="pending_claim">Pending Claim</option>
|
||
<option value="verified">Verified</option>
|
||
<option value="case_closed">Case Closed</option>
|
||
<option value="expired">Expired</option>
|
||
</select>
|
||
<select
|
||
value={itemCategoryFilter}
|
||
onChange={(e) => setItemCategoryFilter(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"
|
||
>
|
||
<option value="">Semua Kategori</option>
|
||
{state.categories &&
|
||
state.categories.map((cat) => (
|
||
<option key={cat.id} value={cat.slug || cat.id}>
|
||
{" "}
|
||
{/* Sesuaikan value dengan slug/id dari DB */}
|
||
{cat.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
{/* ✅ PERUBAHAN UTAMA:
|
||
Kita me-map 'items' secara langsung.
|
||
Tidak perlu filteredItems.filter() lagi karena backend sudah mengirimkan data
|
||
yang sudah terfilter (hanya 10 biji) sesuai halaman aktif.
|
||
*/}
|
||
{items.map((item) => (
|
||
<div
|
||
key={item.id}
|
||
className={`bg-gradient-to-br from-slate-700 to-slate-800 border-2 rounded-xl overflow-hidden hover:shadow-2xl transition-all ${
|
||
item.status === "expired"
|
||
? "border-red-500/50 hover:border-red-500"
|
||
: "border-slate-600 hover:border-blue-500 hover:shadow-blue-500/20"
|
||
}`}
|
||
>
|
||
{item.status === "expired" && (
|
||
<div className="bg-red-600/90 text-white text-center py-1 text-xs font-bold">
|
||
⚠️ EXPIRED
|
||
</div>
|
||
)}
|
||
|
||
<img
|
||
src={
|
||
item.photo_url ||
|
||
"https://via.placeholder.com/280x200?text=No+Image"
|
||
}
|
||
alt={item.name}
|
||
className={`w-full h-48 object-cover cursor-pointer ${
|
||
item.status === "expired" ? "opacity-50" : ""
|
||
}`}
|
||
onClick={() => handleViewItemDetail(item)}
|
||
onError={(e) =>
|
||
(e.target.src =
|
||
"https://via.placeholder.com/280x200?text=No+Image")
|
||
}
|
||
/>
|
||
|
||
<div className="p-4">
|
||
<h3 className="text-lg font-semibold text-white mb-2">
|
||
{item.name}
|
||
</h3>
|
||
<div className="flex flex-col gap-1 text-sm text-slate-400 mb-3">
|
||
<span>📍 {item.location}</span>
|
||
<span>📅 {Helpers.formatDate(item.date_found)}</span>
|
||
<span>👤 {item.reporter_name}</span>
|
||
</div>
|
||
|
||
<span
|
||
className={`inline-block px-3 py-1 rounded-full text-xs font-semibold mb-3 ${Helpers.getStatusBadgeClass(
|
||
item.status
|
||
)}`}
|
||
>
|
||
{item.status}
|
||
</span>
|
||
|
||
<div className="flex gap-2">
|
||
<button
|
||
onClick={() => handleViewItemDetail(item)}
|
||
className="flex-1 px-3 py-2 bg-gradient-to-r from-blue-600 to-blue-700 text-white text-sm rounded-lg hover:from-blue-700 hover:to-blue-800 transition shadow-lg"
|
||
>
|
||
📋 Detail
|
||
</button>
|
||
|
||
{/* Tombol Edit/Hapus hanya muncul jika bukan case_closed */}
|
||
{item.status !== "case_closed" && (
|
||
<>
|
||
<button
|
||
onClick={() => handleEditItemClick(item)}
|
||
className="flex-1 px-3 py-2 bg-gradient-to-r from-yellow-600 to-yellow-700 text-white text-sm rounded-lg hover:from-yellow-700 hover:to-yellow-800 transition shadow-lg"
|
||
>
|
||
✏️ Edit
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => {
|
||
if (
|
||
confirm(
|
||
`⚠️ Yakin ingin menghapus "${item.name}"?`
|
||
)
|
||
) {
|
||
handleDeleteItem(item.id);
|
||
}
|
||
}}
|
||
className="px-3 py-2 bg-gradient-to-r from-red-600 to-red-700 text-white text-sm rounded-lg hover:from-red-700 hover:to-red-800 transition shadow-lg"
|
||
>
|
||
🗑️
|
||
</button>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{items.length === 0 && (
|
||
<div className="text-center py-12 text-slate-400">
|
||
<div className="text-6xl mb-4">{showExpired ? "⚠️" : "📦"}</div>
|
||
<p>
|
||
{showExpired
|
||
? "Belum ada barang expired"
|
||
: "Tidak ada barang ditemukan"}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* ✅ KOMPONEN PAGINATION */}
|
||
{items.length > 0 && (
|
||
<Pagination
|
||
currentPage={itemPage}
|
||
totalPages={itemTotalPages}
|
||
totalRecords={itemTotalRecords}
|
||
onPageChange={(page) => state.setItemPage(page)}
|
||
itemsPerPage={10}
|
||
/>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
<CreateItemModal state={state} handlers={handlers} />
|
||
<EditItemModal state={state} handlers={handlers} />
|
||
</div>
|
||
);
|
||
};
|