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

449 lines
19 KiB
JavaScript
Raw 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.

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.

// web/js/pages/manager/tabs/LostItemsTabManager.js
const LostItemsTabManager = ({ state, handlers }) => {
const { lostItems, items, loading } = state;
const {
showToast,
loadLostItems,
handleDeleteLostItem,
handleMatchLostItem,
handleViewLostItemDetail,
} = handlers;
return (
<div className="bg-gradient-to-br from-slate-800 to-slate-900 rounded-xl p-6 shadow-xl border border-slate-700">
<div className="flex justify-between items-center mb-6">
<div>
<h2 className="text-xl font-semibold text-white">
Kelola Laporan Barang Hilang
</h2>
<p className="text-slate-400 text-sm mt-1">
Lihat semua laporan barang hilang dan hubungkan dengan barang
ditemukan
</p>
</div>
<button
onClick={() => loadLostItems()}
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>
{/* Stats Summary */}
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-yellow-500/10 border border-yellow-500/30 p-4 rounded-lg text-center">
<div className="text-yellow-400 font-bold text-2xl">
{lostItems.filter((i) => i.status === "active").length}
</div>
<div className="text-slate-400 text-sm">Aktif</div>
</div>
<div className="bg-green-500/10 border border-green-500/30 p-4 rounded-lg text-center">
<div className="text-green-400 font-bold text-2xl">
{lostItems.filter((i) => i.status === "found").length}
</div>
<div className="text-slate-400 text-sm">Ditemukan</div>
</div>
<div className="bg-slate-500/10 border border-slate-500/30 p-4 rounded-lg text-center">
<div className="text-slate-400 font-bold text-2xl">
{lostItems.length}
</div>
<div className="text-slate-400 text-sm">Total</div>
</div>
</div>
{loading ? (
<div className="text-center py-12 text-slate-400">
<div className="text-6xl mb-4"></div>
<p>Memuat data...</p>
</div>
) : lostItems.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{lostItems.map((item) => (
<div
key={item.id}
className={`bg-gradient-to-br from-slate-700 to-slate-800 border-2 rounded-xl p-5 hover:shadow-xl transition-all ${
item.status === "found"
? "border-green-500/50 hover:border-green-500"
: "border-slate-600 hover:border-blue-500 hover:shadow-blue-500/20"
}`}
>
{/* Header */}
<div className="flex justify-between items-start mb-4 border-b border-slate-600 pb-3">
<div className="flex-1">
<h3 className="text-lg font-bold text-white">{item.name}</h3>
<p className="text-xs text-slate-400 mt-1">
👤 {item.user_name}
</p>
</div>
<span
className={`px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider ${Helpers.getStatusBadgeClass(
item.status
)}`}
>
{item.status === "active" ? "😢 Hilang" : "✅ Ditemukan"}
</span>
</div>
{/* Details */}
<div className="space-y-2 mb-4">
<div className="flex items-center gap-2 text-sm text-slate-300">
<span className="text-blue-400">🏷</span>
<span>{item.category || item.category_name}</span>
</div>
{item.color && (
<div className="flex items-center gap-2 text-sm text-slate-300">
<span className="text-blue-400">🎨</span>
<span>{item.color}</span>
</div>
)}
{item.location && (
<div className="flex items-center gap-2 text-sm text-slate-300">
<span className="text-blue-400">📍</span>
<span>{item.location}</span>
</div>
)}
<div className="flex items-center gap-2 text-sm text-slate-300">
<span className="text-blue-400">📅</span>
<span>{Helpers.formatDate(item.date_lost)}</span>
</div>
<div className="flex items-center gap-2 text-sm text-slate-300">
<span className="text-blue-400">📞</span>
<span>{item.user_contact || "Tidak ada kontak"}</span>
</div>
</div>
{/* Description */}
<div className="bg-slate-900/50 p-3 rounded-lg border border-slate-600 mb-4">
<strong className="text-xs text-slate-400 uppercase block mb-1">
Deskripsi:
</strong>
<p className="text-sm text-slate-300 line-clamp-3">
{item.description}
</p>
</div>
{/* Matched Item (if found) */}
{item.status === "found" && item.matched_item_id && (
<div className="bg-green-500/10 border-2 border-green-500/30 p-3 rounded-lg mb-4">
<div className="flex items-center gap-2 mb-2">
<span className="text-xl">🎉</span>
<strong className="text-green-400">Sudah Ditemukan</strong>
</div>
<p className="text-xs text-slate-300">
Dihubungkan dengan barang:{" "}
<strong>
{item.matched_item_name || "ID #" + item.matched_item_id}
</strong>
</p>
{item.matched_at && (
<p className="text-xs text-slate-400 mt-1">
Pada: {Helpers.formatDateTime(item.matched_at)}
</p>
)}
</div>
)}
{/* AI Suggestions - Description Based */}
{item.status === "active" &&
item.ai_suggestions &&
item.ai_suggestions.length > 0 && (
<div className="bg-blue-500/10 border border-blue-500/30 p-3 rounded-lg mb-4">
<div className="flex items-center gap-2 mb-2">
<span className="text-lg">🤖</span>
<strong className="text-blue-400 text-sm">
AI Suggestions (Description Similarity)
</strong>
</div>
<p className="text-[10px] text-slate-400 mb-2">
Berdasarkan kesamaan deskripsi barang hilang dengan ciri
khusus barang ditemukan
</p>
<div className="space-y-2">
{item.ai_suggestions
.slice(0, 3)
.map((suggestion, idx) => (
<div
key={idx}
className="text-xs text-slate-300 bg-slate-900/50 p-2 rounded"
>
<div className="flex justify-between items-center mb-1">
<span className="font-semibold">
{suggestion.item_name}
</span>
<span
className={`px-2 py-0.5 rounded text-[10px] font-bold ${
suggestion.match_score >= 70
? "bg-green-500/20 text-green-400"
: suggestion.match_score >= 50
? "bg-yellow-500/20 text-yellow-400"
: "bg-orange-500/20 text-orange-400"
}`}
>
{suggestion.match_score}% Match
</span>
</div>
<p className="text-slate-400 text-[10px] mb-1">
📍 {suggestion.location} 📅{" "}
{Helpers.formatDateShort(suggestion.date_found)}
</p>
{suggestion.preview_text && (
<p className="text-slate-500 text-[10px] italic">
"{suggestion.preview_text}"
</p>
)}
</div>
))}
</div>
</div>
)}
{/* Actions */}
<div className="flex gap-2">
{item.status === "active" && (
<button
onClick={() => handleMatchLostItem(item)}
className="flex-1 px-3 py-2 bg-gradient-to-r from-green-600 to-green-700 text-white text-sm rounded-lg hover:from-green-700 hover:to-green-800 transition shadow-lg"
>
🔗 Hubungkan
</button>
)}
<button
onClick={() => handleViewLostItemDetail(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 flex items-center justify-center gap-1"
>
<span>📋</span> Detail
</button>
<button
onClick={() => {
if (
confirm(
`⚠️ Yakin ingin menghapus laporan "${item.name}" dari ${item.user_name}?\n\nTindakan ini tidak dapat dibatalkan.`
)
) {
handleDeleteLostItem(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"
title="Hapus Laporan"
>
🗑
</button>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-12 text-slate-400">
<div className="text-6xl mb-4">😢</div>
<p>Belum ada laporan barang hilang</p>
</div>
)}
<LostItemDetailModal
isOpen={state.showLostDetailModal}
onClose={() => state.setShowLostDetailModal(false)}
item={state.selectedLostDetail}
/>
</div>
);
};
// Modal untuk menghubungkan barang hilang dengan barang ditemukan
const MatchLostItemModal = ({
isOpen,
onClose,
lostItem,
items,
onSubmit,
loading,
}) => {
const [selectedItemId, setSelectedItemId] = React.useState("");
const [searchTerm, setSearchTerm] = React.useState("");
const filteredItems = items.filter(
(item) =>
item.status === "unclaimed" &&
(item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.location.toLowerCase().includes(searchTerm.toLowerCase()))
);
const handleSubmit = (e) => {
e.preventDefault();
if (!selectedItemId) {
alert("Pilih barang yang ditemukan!");
return;
}
onSubmit(lostItem.id, parseInt(selectedItemId));
};
if (!isOpen) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-70 p-4"
onClick={onClose}
>
<div
className="bg-gradient-to-br from-slate-800 to-slate-900 rounded-2xl w-full max-h-[90vh] overflow-y-auto border border-slate-700 shadow-2xl max-w-3xl"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex justify-between items-center p-6 border-b border-slate-700 sticky top-0 bg-slate-800/95 backdrop-blur">
<h3 className="text-xl font-semibold text-white">
🔗 Hubungkan Barang Hilang dengan Barang Ditemukan
</h3>
<button
onClick={onClose}
className="text-2xl text-slate-400 hover:text-white transition"
>
×
</button>
</div>
{/* Body */}
<div className="p-6">
{/* Lost Item Info */}
<div className="bg-yellow-500/10 border-2 border-yellow-500/30 p-4 rounded-xl mb-6">
<strong className="text-yellow-400 block mb-2">
😢 Barang Hilang:
</strong>
<div className="grid grid-cols-2 gap-3 text-sm text-slate-300">
<div>
<strong className="text-slate-200">Nama:</strong>{" "}
{lostItem.name}
</div>
<div>
<strong className="text-slate-200">Kategori:</strong>{" "}
{lostItem.category || "-"}
</div>
<div>
<strong className="text-slate-200">Warna:</strong>{" "}
{lostItem.color || "-"}
</div>
<div>
<strong className="text-slate-200">Lokasi:</strong>{" "}
{lostItem.location || "-"}
</div>
<div className="col-span-2">
<strong className="text-slate-200">Deskripsi:</strong>{" "}
{lostItem.description}
</div>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Search */}
<div>
<label className="block font-semibold mb-2 text-slate-300">
Cari Barang Ditemukan
</label>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Cari berdasarkan nama atau lokasi..."
className="w-full px-4 py-3 bg-slate-700 border-2 border-slate-600 rounded-xl text-white placeholder-slate-400 focus:border-blue-500 focus:outline-none"
/>
</div>
{/* Items List */}
<div>
<label className="block font-semibold mb-2 text-slate-300">
Pilih Barang yang Sesuai *
</label>
<div className="max-h-96 overflow-y-auto space-y-2 bg-slate-900/50 p-3 rounded-xl border border-slate-600">
{filteredItems.length > 0 ? (
filteredItems.map((item) => (
<div
key={item.id}
onClick={() => setSelectedItemId(item.id.toString())}
className={`p-4 rounded-lg cursor-pointer transition border-2 ${
selectedItemId === item.id.toString()
? "bg-blue-500/20 border-blue-500"
: "bg-slate-800 border-slate-700 hover:border-blue-400"
}`}
>
<div className="flex items-start gap-3">
{/* Radio */}
<input
type="radio"
name="item_id"
value={item.id}
checked={selectedItemId === item.id.toString()}
onChange={(e) => setSelectedItemId(e.target.value)}
className="mt-1"
/>
{/* Photo */}
{item.photo_url && (
<img
src={item.photo_url}
alt={item.name}
className="w-16 h-16 object-cover rounded-lg border border-slate-600"
onError={(e) =>
(e.target.src =
"https://via.placeholder.com/64?text=No+Image")
}
/>
)}
{/* Info */}
<div className="flex-1">
<h4 className="text-white font-semibold">
{item.name}
</h4>
<div className="text-xs text-slate-400 space-y-1 mt-1">
<div>🏷 {item.category || "-"}</div>
<div>📍 {item.location}</div>
<div>📅 {Helpers.formatDate(item.date_found)}</div>
<div>👤 Ditemukan oleh: {item.reporter_name}</div>
</div>
{/* Show secret details preview for matching */}
{item.secret_details && (
<div className="mt-2 p-2 bg-yellow-500/10 border border-yellow-500/30 rounded">
<div className="text-[10px] text-yellow-400 font-bold mb-1">
🔒 Ciri Khusus:
</div>
<p className="text-[10px] text-slate-300 italic line-clamp-2">
"{item.secret_details}"
</p>
</div>
)}
</div>
</div>
</div>
))
) : (
<div className="text-center py-8 text-slate-400">
<p>Tidak ada barang ditemukan yang sesuai</p>
</div>
)}
</div>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={loading || !selectedItemId}
className="w-full px-4 py-3 bg-gradient-to-r from-green-600 to-green-700 text-white rounded-xl hover:from-green-700 hover:to-green-800 transition font-semibold shadow-lg disabled:from-slate-600 disabled:to-slate-700 disabled:cursor-not-allowed"
>
{loading ? "⏳ Menghubungkan..." : "✅ Hubungkan Barang"}
</button>
</form>
</div>
</div>
</div>
);
};
// Export
window.LostItemsTabManager = LostItemsTabManager;
window.MatchLostItemModal = MatchLostItemModal;