Initial commit - Lost and Found Revisi

This commit is contained in:
bambang-code1
2025-12-20 00:01:08 +07:00
commit f4c838471c
210 changed files with 30706 additions and 0 deletions
@@ -0,0 +1,448 @@
// 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;