Initial commit - Lost and Found Revisi
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
const { useState, useEffect } = React;
|
||||
|
||||
const UserApp = () => {
|
||||
const state = useUserState();
|
||||
const handlers = useUserHandlers(state);
|
||||
|
||||
const {
|
||||
user,
|
||||
setUser,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
stats,
|
||||
items,
|
||||
setFilteredItems,
|
||||
searchTerm,
|
||||
categoryFilter,
|
||||
showDetailModal,
|
||||
setShowDetailModal,
|
||||
selectedItem,
|
||||
showClaimModal,
|
||||
setShowClaimModal,
|
||||
showReportLostModal,
|
||||
setShowReportLostModal,
|
||||
showReportFoundModal,
|
||||
setShowReportFoundModal,
|
||||
toast,
|
||||
setToast,
|
||||
loading,
|
||||
photoPreview,
|
||||
setPhotoPreview,
|
||||
showSelectLostItemModal,
|
||||
setShowSelectLostItemModal,
|
||||
suggestedLostItems,
|
||||
claimInitialData,
|
||||
categories,
|
||||
} = state;
|
||||
|
||||
useEffect(() => {
|
||||
if (!AuthUtils.checkAuthAndRedirect("user")) return;
|
||||
const currentUser = AuthUtils.getCurrentUser();
|
||||
setUser(currentUser);
|
||||
handlers.loadData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
filterItems();
|
||||
}, [searchTerm, categoryFilter, items]);
|
||||
|
||||
const filterItems = () => {
|
||||
let filtered = items.filter((item) => {
|
||||
const matchesSearch =
|
||||
item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.location.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesCategory =
|
||||
!categoryFilter ||
|
||||
Helpers.getCategoryValue(item.category_id) === categoryFilter;
|
||||
return matchesSearch && matchesCategory;
|
||||
});
|
||||
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="user" />
|
||||
|
||||
<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 User</h1>
|
||||
<p className="text-slate-400">
|
||||
Selamat datang di Lost & Found System
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<StatCard
|
||||
title="Barang Hilang Saya"
|
||||
value={stats.lost_items || 0}
|
||||
icon="😢"
|
||||
/>
|
||||
<StatCard
|
||||
title="Barang yang Saya Temukan"
|
||||
value={stats.found_items || 0}
|
||||
icon="🎉"
|
||||
/>
|
||||
<StatCard title="Klaim Saya" value={stats.claims || 0} icon="🤝" />
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 mb-6 flex-wrap">
|
||||
<button
|
||||
onClick={() => setActiveTab("browse")}
|
||||
className={`px-6 py-3 rounded-xl font-semibold transition ${
|
||||
activeTab === "browse"
|
||||
? "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"
|
||||
}`}
|
||||
>
|
||||
📦 Barang Ditemukan
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("public-lost")}
|
||||
className={`px-6 py-3 rounded-xl font-semibold transition ${
|
||||
activeTab === "public-lost"
|
||||
? "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"
|
||||
}`}
|
||||
>
|
||||
😢 Barang Hilang
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("lost")}
|
||||
className={`px-6 py-3 rounded-xl font-semibold transition ${
|
||||
activeTab === "lost"
|
||||
? "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"
|
||||
}`}
|
||||
>
|
||||
😢 Barang Hilang Saya
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("found")}
|
||||
className={`px-6 py-3 rounded-xl font-semibold transition ${
|
||||
activeTab === "found"
|
||||
? "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"
|
||||
}`}
|
||||
>
|
||||
🎉 Barang yang Saya Temukan
|
||||
</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"
|
||||
}`}
|
||||
>
|
||||
🤝 Klaim Saya
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeTab === "browse" && (
|
||||
<BrowseTab state={state} handlers={handlers} />
|
||||
)}
|
||||
|
||||
{activeTab === "public-lost" && (
|
||||
<PublicLostItemsTab state={state} handlers={handlers} />
|
||||
)}
|
||||
|
||||
{activeTab === "lost" && (
|
||||
<MyLostItemsTab
|
||||
state={state}
|
||||
handlers={handlers}
|
||||
myClaims={state.myClaims}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === "found" && (
|
||||
<MyFoundItemsTab state={state} handlers={handlers} />
|
||||
)}
|
||||
|
||||
{activeTab === "claims" && (
|
||||
<MyClaimsTab state={state} handlers={handlers} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SelectLostItemModal
|
||||
isOpen={showSelectLostItemModal}
|
||||
onClose={() => state.setShowSelectLostItemModal(false)}
|
||||
lostItems={suggestedLostItems}
|
||||
onSelect={handlers.handleSelectLostItemForClaim}
|
||||
onManual={handlers.handleManualClaimEntry}
|
||||
/>
|
||||
|
||||
<FoundOptionModal
|
||||
isOpen={state.showFoundOptionModal}
|
||||
onClose={() => state.setShowFoundOptionModal(false)}
|
||||
onSelectOption={handlers.handleSelectFoundOption}
|
||||
/>
|
||||
|
||||
<DirectClaimModal
|
||||
isOpen={state.showDirectClaimModal}
|
||||
onClose={() => state.setShowDirectClaimModal(false)}
|
||||
onSubmit={handlers.handleDirectClaimSubmit}
|
||||
loading={loading}
|
||||
lostItem={state.selectedLostItemForFound}
|
||||
/>
|
||||
|
||||
<DetailModalUser
|
||||
isOpen={state.showDetailModal}
|
||||
onClose={() => state.setShowDetailModal(false)}
|
||||
item={state.selectedItem}
|
||||
user={state.user}
|
||||
onClaim={handlers.handleClaim}
|
||||
/>
|
||||
|
||||
<ClaimModalUser
|
||||
isOpen={showClaimModal}
|
||||
onClose={() => state.setShowClaimModal(false)}
|
||||
onSubmit={handlers.submitClaim}
|
||||
loading={loading}
|
||||
initialData={claimInitialData}
|
||||
categories={categories}
|
||||
/>
|
||||
|
||||
<ReportLostModalUser
|
||||
isOpen={showReportLostModal}
|
||||
onClose={() => state.setShowReportLostModal(false)}
|
||||
onSubmit={handlers.submitReportLost}
|
||||
loading={loading}
|
||||
categories={categories}
|
||||
/>
|
||||
|
||||
<ReportFoundModalUser
|
||||
isOpen={showReportFoundModal}
|
||||
onClose={() => state.setShowReportFoundModal(false)}
|
||||
onSubmit={handlers.submitReportFound}
|
||||
loading={loading}
|
||||
photoPreview={photoPreview}
|
||||
onPhotoChange={handlers.handlePhotoChange}
|
||||
user={user}
|
||||
categories={categories}
|
||||
/>
|
||||
|
||||
{state.toast && (
|
||||
<Toast
|
||||
message={state.toast.message}
|
||||
type={state.toast.type}
|
||||
onClose={() => state.setToast(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AIChatbot />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.render(<UserApp />, document.getElementById("root"));
|
||||
@@ -0,0 +1,523 @@
|
||||
// assets/js/pages/user/UserModals.js
|
||||
|
||||
// ✅ [BARU] Modal untuk memilih Laporan Hilang yang cocok
|
||||
const SelectLostItemModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
lostItems,
|
||||
onSelect,
|
||||
onManual,
|
||||
}) => (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="💡 Ditemukan Laporan Cocok">
|
||||
<div className="space-y-4">
|
||||
<div className="bg-blue-500/10 border border-blue-500/30 p-4 rounded-xl">
|
||||
<p className="text-sm text-blue-200">
|
||||
Kami menemukan laporan <strong>Barang Hilang Anda</strong> dengan
|
||||
kategori yang sama. Pilih salah satu untuk mengisi form klaim secara
|
||||
otomatis.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 max-h-80 overflow-y-auto pr-1">
|
||||
{lostItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => onSelect(item)}
|
||||
className="bg-slate-800 border border-slate-600 hover:border-blue-500 hover:bg-slate-700/50 p-4 rounded-xl cursor-pointer transition group"
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h4 className="font-bold text-white group-hover:text-blue-400 transition">
|
||||
{item.name}
|
||||
</h4>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
📅 {Helpers.formatDate(item.date_lost)} • 📍{" "}
|
||||
{item.location || "?"}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-2xl opacity-50 group-hover:opacity-100 transition">
|
||||
👉
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-300 mt-2 line-clamp-2 italic">
|
||||
"{item.description}"
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-slate-700">
|
||||
<button
|
||||
onClick={onManual}
|
||||
className="w-full py-3 bg-slate-700 hover:bg-slate-600 text-white rounded-xl font-semibold transition"
|
||||
>
|
||||
📝 Tidak, Saya Ingin Input Manual
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const DetailModalUser = ({ isOpen, onClose, item, user, onClaim }) => (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="Detail Barang">
|
||||
{item && (
|
||||
<div>
|
||||
<img
|
||||
src={item.photo_url || "https://via.placeholder.com/600x400"}
|
||||
alt={item.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">{item.name}</h3>
|
||||
<div className="space-y-3 text-slate-200">
|
||||
<div className="bg-slate-700/50 p-3 rounded-lg">
|
||||
<strong className="text-blue-400">Kategori:</strong>
|
||||
<span className="ml-2">
|
||||
{item.category || Helpers.getCategoryName(item.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">{item.location}</span>
|
||||
</div>
|
||||
<div className="bg-slate-700/50 p-3 rounded-lg">
|
||||
<strong className="text-blue-400">Tanggal:</strong>
|
||||
<span className="ml-2">{Helpers.formatDate(item.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(
|
||||
item.status
|
||||
)}`}
|
||||
>
|
||||
{item.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{item.status === "unclaimed" && item.reporter_id !== user?.id && (
|
||||
<button
|
||||
onClick={() => {
|
||||
onClose();
|
||||
onClaim(item);
|
||||
}}
|
||||
className="w-full mt-6 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 shadow-lg"
|
||||
>
|
||||
Klaim Barang Ini
|
||||
</button>
|
||||
)}
|
||||
|
||||
{item.reporter_id === user?.id && (
|
||||
<div className="mt-6 p-4 bg-green-500/10 border-2 border-green-500/30 rounded-xl text-center">
|
||||
<div className="text-green-400 font-semibold text-lg mb-2">
|
||||
✓ Ini adalah barang yang Anda laporkan
|
||||
</div>
|
||||
<p className="text-slate-400 text-sm">
|
||||
Anda tidak dapat mengklaim barang yang Anda sendiri temukan
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const ClaimModalUser = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSubmit,
|
||||
loading,
|
||||
initialData,
|
||||
categories, // Pastikan prop ini diterima
|
||||
}) => {
|
||||
const [formData, setFormData] = React.useState({
|
||||
description: "",
|
||||
contact: "",
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (initialData) {
|
||||
setFormData({
|
||||
description: initialData.description || "",
|
||||
contact: initialData.contact || "",
|
||||
});
|
||||
} else {
|
||||
setFormData({ description: "", contact: "" });
|
||||
}
|
||||
}
|
||||
}, [isOpen, initialData]);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="Klaim Barang">
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
{initialData && (
|
||||
<div className="bg-green-500/10 border border-green-500/30 p-3 rounded-lg mb-2">
|
||||
<p className="text-xs text-green-300">
|
||||
✅ Data terisi otomatis dari laporan hilang Anda. Silakan lengkapi
|
||||
bukti.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Deskripsi */}
|
||||
<div>
|
||||
<label className="block font-semibold mb-2 text-slate-300">
|
||||
Deskripsi Barang *
|
||||
</label>
|
||||
<textarea
|
||||
name="description"
|
||||
required
|
||||
rows="4"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
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"
|
||||
placeholder="Jelaskan ciri khusus barang yang hanya Anda tahu..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Kontak */}
|
||||
<div>
|
||||
<label className="block font-semibold mb-2 text-slate-300">
|
||||
Kontak yang bisa dihubungi *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="contact"
|
||||
required
|
||||
value={formData.contact}
|
||||
onChange={handleChange}
|
||||
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"
|
||||
placeholder="No HP / WhatsApp / Line"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bukti */}
|
||||
<div>
|
||||
<label className="block font-semibold mb-2 text-slate-300">
|
||||
Bukti Pendukung (Opsional)
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
name="proof"
|
||||
accept="image/*,.pdf"
|
||||
className="w-full px-4 py-3 bg-slate-700 border-2 border-slate-600 rounded-xl text-slate-300 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-600 file:text-white hover:file:bg-blue-700 transition"
|
||||
/>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
Foto struk, foto lama barang, atau bukti kepemilikan lainnya.
|
||||
</p>
|
||||
</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 shadow-lg disabled:from-slate-600 disabled:to-slate-700"
|
||||
>
|
||||
{loading ? "Mengirim..." : "Ajukan Klaim"}
|
||||
</button>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const ReportLostModalUser = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSubmit,
|
||||
loading,
|
||||
categories,
|
||||
}) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="📢 Lapor Kehilangan Barang">
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
{/* Nama Barang */}
|
||||
<div>
|
||||
<label className="block font-semibold mb-2 text-slate-300">
|
||||
Nama Barang Hilang *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
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"
|
||||
placeholder="Contoh: Dompet Hitam Merk X"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Kategori Dropdown */}
|
||||
<div>
|
||||
<label className="block font-semibold mb-2 text-slate-300">
|
||||
Kategori *
|
||||
</label>
|
||||
<select
|
||||
name="category_id"
|
||||
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="">-- Pilih Kategori --</option>
|
||||
{categories && categories.length > 0 ? (
|
||||
categories.map((cat) => (
|
||||
<option key={cat.id} value={cat.id}>
|
||||
{cat.name}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value="" disabled>
|
||||
Gagal memuat kategori (Cek Database)
|
||||
</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Warna (Opsional) */}
|
||||
<div>
|
||||
<label className="block font-semibold mb-2 text-slate-300">
|
||||
Warna Dominan
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="color"
|
||||
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"
|
||||
placeholder="Contoh: Hitam"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Lokasi */}
|
||||
<div>
|
||||
<label className="block font-semibold mb-2 text-slate-300">
|
||||
Perkiraan Lokasi Hilang
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="location"
|
||||
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"
|
||||
placeholder="Contoh: Kantin, Lab Komputer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Deskripsi */}
|
||||
<div>
|
||||
<label className="block font-semibold mb-2 text-slate-300">
|
||||
Ciri-ciri Detail *
|
||||
</label>
|
||||
<textarea
|
||||
name="description"
|
||||
required
|
||||
rows="3"
|
||||
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"
|
||||
placeholder="Jelaskan ciri-ciri unik, isi dompet, stiker laptop, dll..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tanggal Hilang */}
|
||||
<div>
|
||||
<label className="block font-semibold mb-2 text-slate-300">
|
||||
Tanggal Hilang *
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
name="date_lost"
|
||||
required
|
||||
max={new Date().toISOString().split("T")[0]}
|
||||
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"
|
||||
/>
|
||||
</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 shadow-lg disabled:from-slate-600 disabled:to-slate-700"
|
||||
>
|
||||
{loading ? "Mengirim..." : "Submit Laporan"}
|
||||
</button>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const ReportFoundModalUser = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSubmit,
|
||||
loading,
|
||||
photoPreview,
|
||||
onPhotoChange,
|
||||
user,
|
||||
categories,
|
||||
}) => (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="Lapor Barang Temuan">
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
{/* Input Nama Barang */}
|
||||
<div>
|
||||
<label className="block font-semibold mb-2 text-slate-300">
|
||||
Nama Barang *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
required
|
||||
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"
|
||||
placeholder="Contoh: Botol Minum Biru"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Input Kategori - BAGIAN YANG DIPERBAIKI */}
|
||||
<div>
|
||||
<label className="block font-semibold mb-2 text-slate-300">
|
||||
Kategori *
|
||||
</label>
|
||||
<select
|
||||
name="category_id"
|
||||
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="">-- Pilih Kategori --</option>
|
||||
{categories &&
|
||||
categories.map((cat) => (
|
||||
<option key={cat.id} value={cat.id}>
|
||||
{cat.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Input Foto Barang */}
|
||||
<div>
|
||||
<label className="block font-semibold mb-2 text-slate-300">
|
||||
Foto Barang
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
name="photo"
|
||||
accept="image/*"
|
||||
onChange={onPhotoChange}
|
||||
className="w-full text-slate-300 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-600 file:text-white hover:file:bg-blue-700 transition"
|
||||
/>
|
||||
{photoPreview && (
|
||||
<div className="mt-3">
|
||||
<p className="text-xs text-slate-400 mb-1">Preview:</p>
|
||||
<img
|
||||
src={photoPreview}
|
||||
alt="Preview"
|
||||
className="w-full h-48 object-cover rounded-xl border-2 border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input Lokasi */}
|
||||
<div>
|
||||
<label className="block font-semibold mb-2 text-slate-300">
|
||||
Lokasi Ditemukan *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="location"
|
||||
required
|
||||
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"
|
||||
placeholder="Contoh: Perpustakaan Lantai 2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Input Deskripsi */}
|
||||
<div>
|
||||
<label className="block font-semibold mb-2 text-slate-300">
|
||||
Deskripsi Umum *
|
||||
</label>
|
||||
<textarea
|
||||
name="description"
|
||||
required
|
||||
rows="3"
|
||||
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"
|
||||
placeholder="Deskripsi umum barang yang akan ditampilkan..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Input Ciri Rahasia */}
|
||||
<div>
|
||||
<label className="block font-semibold mb-2 text-slate-300">
|
||||
Ciri Khusus Rahasia * 🔒
|
||||
</label>
|
||||
<textarea
|
||||
name="secret_details"
|
||||
required
|
||||
rows="3"
|
||||
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"
|
||||
placeholder="Jelaskan ciri khusus yang HANYA pemilik asli yang tahu (contoh: ada goresan di bagian belakang, ada tulisan nama di dalam, dll)"
|
||||
/>
|
||||
<p className="text-xs text-yellow-400 mt-1">
|
||||
⚠️ Info ini RAHASIA dan hanya digunakan untuk verifikasi klaim
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Input Tanggal */}
|
||||
<div>
|
||||
<label className="block font-semibold mb-2 text-slate-300">
|
||||
Tanggal Ditemukan *
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
name="date_found"
|
||||
required
|
||||
max={new Date().toISOString().split("T")[0]}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Input Pelapor (Read Only) */}
|
||||
<div>
|
||||
<label className="block font-semibold mb-2 text-slate-300">
|
||||
Nama Pelapor *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="reporter_name"
|
||||
required
|
||||
defaultValue={user?.name}
|
||||
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"
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Input Kontak */}
|
||||
<div>
|
||||
<label className="block font-semibold mb-2 text-slate-300">
|
||||
Kontak Pelapor *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="reporter_contact"
|
||||
required
|
||||
defaultValue={user?.phone}
|
||||
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"
|
||||
placeholder="08123456789"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tombol Submit */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
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"
|
||||
>
|
||||
{loading ? "Mengirim..." : "Submit Laporan"}
|
||||
</button>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
// Export ke window
|
||||
window.DetailModalUser = DetailModalUser;
|
||||
window.ClaimModalUser = ClaimModalUser;
|
||||
window.SelectLostItemModal = SelectLostItemModal;
|
||||
window.ReportLostModalUser = ReportLostModalUser; // Pastikan ini ada
|
||||
window.ReportFoundModalUser = ReportFoundModalUser;
|
||||
@@ -0,0 +1,350 @@
|
||||
// web/js/pages/user/UserTabsBrowse.js
|
||||
|
||||
// ✅ 1. Komponen BrowseTab (Barang Ditemukan)
|
||||
const BrowseTab = ({ state, handlers }) => {
|
||||
const {
|
||||
filteredItems,
|
||||
loading,
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
categoryFilter,
|
||||
setCategoryFilter,
|
||||
categories,
|
||||
user,
|
||||
} = state;
|
||||
|
||||
const { handleViewDetail, handleClaim } = 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="p-6 border-b border-slate-700">
|
||||
<h2 className="text-xl font-semibold mb-4 text-white">
|
||||
📦 Barang Ditemukan
|
||||
</h2>
|
||||
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cari barang (nama/lokasi)..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(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={categoryFilter}
|
||||
onChange={(e) => setCategoryFilter(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>
|
||||
{categories &&
|
||||
categories.map((cat) => (
|
||||
<option key={cat.id} value={cat.slug || cat.id}>
|
||||
{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">
|
||||
{filteredItems.map((item) => (
|
||||
<ItemCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
currentUserId={user?.id}
|
||||
onViewDetail={handleViewDetail}
|
||||
onClaim={handleClaim}
|
||||
showActions={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredItems.length === 0 && (
|
||||
<div className="text-center py-12 text-slate-400">
|
||||
<div className="text-6xl mb-4">📦</div>
|
||||
<p>Tidak ada barang ditemukan sesuai pencarian.</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ✅ 2. Komponen PublicLostItemsTab (Barang Hilang Publik)
|
||||
const PublicLostItemsTab = ({ state, handlers }) => {
|
||||
const { publicLostItems, loading, user } = state;
|
||||
const [showContactModal, setShowContactModal] = React.useState(false);
|
||||
const [selectedLostItem, setSelectedLostItem] = React.useState(null);
|
||||
const [showDetailModal, setShowDetailModal] = React.useState(false);
|
||||
const [selectedDetailItem, setSelectedDetailItem] = React.useState(null);
|
||||
|
||||
const handleShowContact = (item) => {
|
||||
setSelectedLostItem(item);
|
||||
setShowContactModal(true);
|
||||
};
|
||||
|
||||
const handleShowDetail = (item) => {
|
||||
setSelectedDetailItem(item);
|
||||
setShowDetailModal(true);
|
||||
};
|
||||
|
||||
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-4">
|
||||
<h2 className="text-xl font-semibold text-white">
|
||||
Semua Laporan Barang Hilang
|
||||
</h2>
|
||||
<div className="text-slate-400 text-sm">
|
||||
{publicLostItems.length} laporan
|
||||
</div>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-slate-400">
|
||||
<div className="text-6xl mb-4">⏳</div>
|
||||
<p>Memuat data...</p>
|
||||
</div>
|
||||
) : publicLostItems.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{publicLostItems.map((item) => {
|
||||
const isMine = user && user.id === item.user_id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-gradient-to-br from-slate-700 to-slate-800 border-2 border-slate-600 rounded-xl p-5 hover:border-blue-500 hover:shadow-xl hover:shadow-blue-500/20 transition-all"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<h3 className="text-lg font-semibold text-white flex-1">
|
||||
{item.name}
|
||||
</h3>
|
||||
|
||||
{/* ✅ Tampilkan Badge jika barang sendiri */}
|
||||
{isMine ? (
|
||||
<span className="px-2 py-1 rounded text-[10px] font-bold bg-blue-500/20 text-blue-400 border border-blue-500/50">
|
||||
BARANG SAYA
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className={`px-3 py-1 rounded-full text-xs font-semibold ${Helpers.getStatusBadgeClass(
|
||||
item.status
|
||||
)}`}
|
||||
>
|
||||
{item.status}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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.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>{item.user_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-900/50 p-3 rounded-lg border border-slate-700 mb-4">
|
||||
<p className="text-sm text-slate-300 line-clamp-3">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleShowDetail(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>
|
||||
{item.status === "active" && !isMine && (
|
||||
<button
|
||||
onClick={() => handlers.handleOpenFoundOption(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"
|
||||
>
|
||||
✅ Saya Menemukan
|
||||
</button>
|
||||
)}
|
||||
{isMine && (
|
||||
<div className="flex-1 px-3 py-2 text-center text-xs text-slate-400 border border-slate-600 rounded-lg">
|
||||
Kelola di tab "Barang Hilang Saya"
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Modal Detail Barang Hilang */}
|
||||
<Modal
|
||||
isOpen={showDetailModal}
|
||||
onClose={() => setShowDetailModal(false)}
|
||||
title="📋 Detail Barang Hilang"
|
||||
>
|
||||
{selectedDetailItem && (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gradient-to-r from-slate-700 to-slate-800 p-4 rounded-xl border border-slate-600">
|
||||
<h3 className="text-xl font-bold text-white mb-1">
|
||||
{selectedDetailItem.name}
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2 text-sm">
|
||||
<span className="text-blue-400 font-medium">
|
||||
🏷️{" "}
|
||||
{selectedDetailItem.category ||
|
||||
selectedDetailItem.category_name ||
|
||||
"-"}
|
||||
</span>
|
||||
<span className="text-slate-500">•</span>
|
||||
<span className="text-slate-300">
|
||||
📅 {Helpers.formatDate(selectedDetailItem.date_lost)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-slate-700/30 p-3 rounded-lg border border-slate-600">
|
||||
<p className="text-xs text-slate-400 uppercase font-bold mb-1">
|
||||
📍 Lokasi Hilang
|
||||
</p>
|
||||
<p className="text-white text-sm">
|
||||
{selectedDetailItem.location || "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-slate-700/30 p-3 rounded-lg border border-slate-600">
|
||||
<p className="text-xs text-slate-400 uppercase font-bold mb-1">
|
||||
🎨 Warna/Ciri
|
||||
</p>
|
||||
<p className="text-white text-sm">
|
||||
{selectedDetailItem.color || "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-900/50 p-4 rounded-xl border border-slate-600">
|
||||
<p className="text-xs text-slate-400 uppercase font-bold mb-2">
|
||||
📝 Deskripsi Lengkap
|
||||
</p>
|
||||
<p className="text-slate-300 text-sm leading-relaxed italic">
|
||||
"{selectedDetailItem.description}"
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-500/10 p-3 rounded-lg border border-blue-500/20 flex items-center gap-3">
|
||||
<div className="bg-blue-600 w-10 h-10 rounded-full flex items-center justify-center text-lg shadow">
|
||||
👤
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-blue-300 font-bold uppercase">
|
||||
Dilaporkan Oleh
|
||||
</p>
|
||||
<p className="text-white font-medium">
|
||||
{selectedDetailItem.user_name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowDetailModal(false)}
|
||||
className="w-full py-3 bg-slate-700 hover:bg-slate-600 text-white rounded-xl font-semibold transition"
|
||||
>
|
||||
Tutup
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* Modal Kontak (Jika diperlukan) */}
|
||||
<Modal
|
||||
isOpen={showContactModal}
|
||||
onClose={() => setShowContactModal(false)}
|
||||
title="👋 Anda Menemukan Barang Ini?"
|
||||
>
|
||||
{selectedLostItem && (
|
||||
<div className="text-center space-y-6">
|
||||
<div className="bg-slate-700/50 p-6 rounded-2xl border border-slate-600">
|
||||
<div className="w-20 h-20 bg-gradient-to-br from-blue-500 to-blue-600 rounded-full mx-auto flex items-center justify-center text-3xl shadow-lg mb-4">
|
||||
👤
|
||||
</div>
|
||||
<p className="text-slate-400 mb-1">Silakan hubungi pemilik:</p>
|
||||
<h3 className="text-2xl font-bold text-white mb-2">
|
||||
{selectedLostItem.user_name}
|
||||
</h3>
|
||||
{selectedLostItem.user_contact ? (
|
||||
<a
|
||||
href={`https://wa.me/${selectedLostItem.user_contact
|
||||
.replace(/^0/, "62")
|
||||
.replace(/\D/g, "")}`}
|
||||
target="_blank"
|
||||
className="inline-flex items-center gap-2 px-6 py-2 bg-green-600 hover:bg-green-700 text-white rounded-full font-semibold transition shadow-lg"
|
||||
>
|
||||
<span>📞</span> Hubungi via WhatsApp
|
||||
</a>
|
||||
) : (
|
||||
<div className="text-yellow-400 text-sm bg-yellow-400/10 py-2 px-4 rounded-lg inline-block">
|
||||
Kontak tidak ditampilkan secara publik
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-slate-700"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-slate-800 text-slate-400">Atau</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-blue-500/10 border border-blue-500/30 p-4 rounded-xl text-left flex gap-4">
|
||||
<div className="text-3xl">📝</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-blue-400 mb-1">
|
||||
Laporkan Temuan
|
||||
</h4>
|
||||
<p className="text-sm text-slate-300 mb-2">
|
||||
Agar lebih aman, Anda bisa melaporkan barang ini sebagai
|
||||
"Ditemukan" di sistem kami.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowContactModal(false);
|
||||
}}
|
||||
className="text-xs text-white bg-blue-600 hover:bg-blue-700 px-3 py-1 rounded transition"
|
||||
>
|
||||
Buka Menu "Barang yang Saya Temukan"
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Export ke window agar bisa diakses oleh UserApp
|
||||
window.BrowseTab = BrowseTab;
|
||||
window.PublicLostItemsTab = PublicLostItemsTab;
|
||||
@@ -0,0 +1,284 @@
|
||||
// assets/js/pages/user/UserTabsMy.js
|
||||
|
||||
const MyLostItemsTab = ({ state, handlers, myClaims }) => {
|
||||
const { myLostItems, loading } = state;
|
||||
const { handleUserRespondClaim, handleUserCompleteCase, loadData } = handlers;
|
||||
|
||||
// ✅ PERBAIKAN: Gunakan lost_item_id yang dikirim backend, jangan pakai Regex deskripsi
|
||||
const findMatchingClaim = (lostItemId) => {
|
||||
if (!myClaims || myClaims.length === 0) return null;
|
||||
|
||||
// Cari claim yang memiliki lost_item_id sama dengan item ini
|
||||
// DAN statusnya relevan (menunggu owner, approved/verified)
|
||||
return myClaims.find((c) => c.lost_item_id === lostItemId);
|
||||
};
|
||||
|
||||
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-4">
|
||||
<h2 className="text-xl font-semibold text-white">Barang Hilang Saya</h2>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => loadData()}
|
||||
className="px-4 py-2 bg-slate-700 text-white rounded-lg hover:bg-slate-600 transition border border-slate-600"
|
||||
>
|
||||
🔄 Refresh
|
||||
</button>
|
||||
<button
|
||||
onClick={() => state.setShowReportLostModal(true)}
|
||||
className="px-4 py-2 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg hover:from-blue-700 hover:to-blue-800 transition shadow-lg"
|
||||
>
|
||||
+ Lapor Kehilangan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-slate-400">
|
||||
<div className="text-6xl mb-4 animate-pulse">⏳</div>Memuat data...
|
||||
</div>
|
||||
) : myLostItems.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{myLostItems.map((item) => {
|
||||
// ✅ PERBAIKAN UTAMA: Ambil klaim langsung dari objek item (dikirim backend)
|
||||
// Bukan mencari dari list myClaims
|
||||
const matchingClaim = item.direct_claim;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`bg-gradient-to-br from-slate-700 to-slate-800 border-2 rounded-xl p-4 transition-all ${
|
||||
matchingClaim
|
||||
? "border-yellow-500 shadow-yellow-500/20"
|
||||
: "border-slate-600"
|
||||
}`}
|
||||
>
|
||||
<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">
|
||||
{/* Tampilkan Badge Status Item */}
|
||||
<span
|
||||
className={`px-2 py-1 rounded text-xs font-bold w-fit ${Helpers.getStatusBadgeClass(
|
||||
item.status
|
||||
)}`}
|
||||
>
|
||||
{item.status.replace("_", " ").toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* --- LOGIKA TOMBOL APPROVE/REJECT --- */}
|
||||
{matchingClaim && (
|
||||
<div className="mt-3 p-3 rounded-lg border bg-slate-900/50 border-slate-600">
|
||||
{/* KASUS 1: STATUS WAITING_OWNER (Munculkan Tombol) */}
|
||||
{matchingClaim.status === "waiting_owner" && (
|
||||
<>
|
||||
<div className="mb-3">
|
||||
<p className="text-yellow-400 text-xs font-bold flex items-center gap-1 mb-1">
|
||||
🔔 DITEMUKAN OLEH:{" "}
|
||||
{matchingClaim.reporter_name || "Seseorang"}
|
||||
</p>
|
||||
<p className="text-xs text-slate-300 italic">
|
||||
"{matchingClaim.description}"
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
handleUserRespondClaim(matchingClaim.id, "reject")
|
||||
}
|
||||
className="flex-1 py-2 bg-red-900/30 text-red-400 border border-red-500/30 rounded hover:bg-red-600 hover:text-white text-xs font-bold transition"
|
||||
>
|
||||
Tolak
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleUserRespondClaim(
|
||||
matchingClaim.id,
|
||||
"approve"
|
||||
)
|
||||
}
|
||||
className="flex-1 py-2 bg-green-600 hover:bg-green-700 text-white rounded text-xs font-bold shadow-lg transition"
|
||||
>
|
||||
Terima Barang
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* KASUS 2: STATUS VERIFIED (Munculkan Tombol Selesai) */}
|
||||
{(matchingClaim.status === "verified" ||
|
||||
matchingClaim.status === "approved") && (
|
||||
<>
|
||||
<p className="text-green-400 text-xs font-bold mb-2">
|
||||
✅ SUDAH ANDA SETUJUI
|
||||
</p>
|
||||
<p className="text-xs text-slate-400 mb-3">
|
||||
Hubungi: {matchingClaim.contact}
|
||||
</p>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleUserCompleteCase(matchingClaim.id)
|
||||
}
|
||||
className="w-full py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs font-bold transition"
|
||||
>
|
||||
📦 Barang Sudah Diterima (Selesai)
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* KASUS 3: STATUS REJECTED */}
|
||||
{matchingClaim.status === "rejected" && (
|
||||
<p className="text-red-400 text-xs font-bold text-center">
|
||||
❌ KLAIM DITOLAK
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-slate-400 py-8">
|
||||
Belum ada laporan kehilangan.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MyFoundItemsTab = ({ state, handlers }) => {
|
||||
const { myFoundItems, loading, user } = state;
|
||||
const { handleViewDetail } = 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-4">
|
||||
<h2 className="text-xl font-semibold text-white">
|
||||
Barang yang Saya Temukan
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => state.setShowReportFoundModal(true)}
|
||||
className="px-4 py-2 bg-gradient-to-r from-green-600 to-green-700 text-white rounded-lg hover:from-green-700 hover:to-green-800 transition shadow-lg"
|
||||
>
|
||||
+ Lapor Penemuan
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-slate-400">
|
||||
<div className="text-6xl mb-4 animate-pulse">⏳</div>
|
||||
<p>Memuat data...</p>
|
||||
</div>
|
||||
) : myFoundItems.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{myFoundItems.map((item) => (
|
||||
<div key={item.id} className="relative">
|
||||
<ItemCard
|
||||
item={item}
|
||||
currentUserId={user?.id}
|
||||
onViewDetail={handleViewDetail}
|
||||
showActions={true}
|
||||
/>
|
||||
|
||||
{/* --- TAMBAHAN VISUAL UNTUK STATUS --- */}
|
||||
|
||||
{/* Jika status Pending Claim (Menunggu Pemilik/Admin) */}
|
||||
{item.status === "pending_claim" && (
|
||||
<div className="mt-2 bg-yellow-500/10 border border-yellow-500/30 p-2 rounded-lg text-center">
|
||||
<p className="text-xs text-yellow-400 font-semibold">
|
||||
⏳ Menunggu Konfirmasi
|
||||
</p>
|
||||
<p className="text-[10px] text-slate-400">
|
||||
Menunggu pemilik asli atau admin memverifikasi.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Jika status Verified (Sudah Diapprove) */}
|
||||
{item.status === "verified" && (
|
||||
<div className="mt-2 bg-green-500/10 border border-green-500/30 p-2 rounded-lg text-center">
|
||||
<p className="text-xs text-green-400 font-semibold">
|
||||
✅ Terverifikasi
|
||||
</p>
|
||||
<p className="text-[10px] text-slate-400">
|
||||
Pemilik telah mengonfirmasi. Silakan serahkan barang.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Jika status Case Closed (Selesai) */}
|
||||
{item.status === "case_closed" && (
|
||||
<div className="mt-2 bg-blue-500/10 border border-blue-500/30 p-2 rounded-lg text-center">
|
||||
<p className="text-xs text-blue-400 font-semibold">
|
||||
🏁 Kasus Selesai
|
||||
</p>
|
||||
<p className="text-[10px] text-slate-400">
|
||||
Barang telah dikembalikan. Terima kasih!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-slate-400">
|
||||
<div className="text-6xl mb-4">🎉</div>
|
||||
<p>Anda belum melaporkan barang temuan</p>
|
||||
<button
|
||||
onClick={() => state.setShowReportFoundModal(true)}
|
||||
className="mt-4 px-6 py-2 bg-gradient-to-r from-green-600 to-green-700 text-white rounded-lg hover:from-green-700 hover:to-green-800 transition shadow-lg"
|
||||
>
|
||||
Lapor Sekarang
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MyClaimsTab = ({ state, handlers }) => {
|
||||
const { myClaims, loading } = state;
|
||||
const { handleUserRespondClaim } = handlers;
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-slate-800 to-slate-900 rounded-xl p-6 shadow-xl border border-slate-700">
|
||||
<h2 className="text-xl font-semibold mb-6 text-white">
|
||||
Klaim Saya & Konfirmasi
|
||||
</h2>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-slate-400">
|
||||
<div className="text-6xl mb-4">⏳</div>
|
||||
<p>Memuat data...</p>
|
||||
</div>
|
||||
) : myClaims.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{myClaims.map((claim) => (
|
||||
<ClaimCard
|
||||
key={claim.id}
|
||||
claim={claim}
|
||||
onUserRespond={handleUserRespondClaim}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-slate-400">
|
||||
<div className="text-6xl mb-4">🤝</div>
|
||||
<p>Anda belum memiliki riwayat klaim</p>
|
||||
<button
|
||||
onClick={() => state.setActiveTab("browse")}
|
||||
className="mt-4 px-6 py-2 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-lg hover:from-blue-700 hover:to-blue-800 transition shadow-lg"
|
||||
>
|
||||
Lihat Barang Ditemukan
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
window.MyLostItemsTab = MyLostItemsTab;
|
||||
window.MyFoundItemsTab = MyFoundItemsTab;
|
||||
window.MyClaimsTab = MyClaimsTab;
|
||||
@@ -0,0 +1,266 @@
|
||||
// ==========================================
|
||||
// UPDATE MyClaimsTab.js untuk pass currentUserId
|
||||
// ==========================================
|
||||
|
||||
const MyClaimsTab = ({ state, handlers }) => {
|
||||
const { myLostItems, myClaims, loading, user } = state; // ⭐ Ambil user dari state
|
||||
|
||||
const lostItemsWithClaims = myLostItems.filter(
|
||||
(item) =>
|
||||
item.direct_claim_id &&
|
||||
item.direct_claim_status &&
|
||||
item.status !== "completed" &&
|
||||
item.direct_claim_status !== "completed"
|
||||
);
|
||||
|
||||
const regularClaims = myClaims || [];
|
||||
|
||||
const getStatusBadge = (status) => {
|
||||
const badges = {
|
||||
active: { color: "bg-blue-500", text: "🔍 Masih Hilang", emoji: "😢" },
|
||||
claimed: {
|
||||
color: "bg-yellow-500",
|
||||
text: "⏳ Menunggu Persetujuan Anda",
|
||||
emoji: "🤔",
|
||||
},
|
||||
found: { color: "bg-green-500", text: "✅ Ditemukan", emoji: "🎉" },
|
||||
completed: {
|
||||
color: "bg-purple-500",
|
||||
text: "✅ Selesai - Barang Diterima",
|
||||
emoji: "🎉",
|
||||
},
|
||||
pending: {
|
||||
color: "bg-yellow-500",
|
||||
text: "⏳ Menunggu Verifikasi",
|
||||
emoji: "⏳",
|
||||
},
|
||||
approved: { color: "bg-green-500", text: "✅ Disetujui", emoji: "✅" },
|
||||
rejected: { color: "bg-red-500", text: "❌ Ditolak", emoji: "❌" },
|
||||
waiting_owner: {
|
||||
color: "bg-orange-500",
|
||||
text: "⏳ Menunggu Anda",
|
||||
emoji: "👤",
|
||||
},
|
||||
verified: {
|
||||
color: "bg-green-600",
|
||||
text: "✅ Terverifikasi",
|
||||
emoji: "✅",
|
||||
},
|
||||
};
|
||||
return (
|
||||
badges[status] || {
|
||||
color: "bg-gray-500",
|
||||
text: status,
|
||||
emoji: "❓",
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-center py-12 text-slate-400">
|
||||
<div className="animate-spin text-4xl mb-4">⏳</div>
|
||||
<p>Memuat data klaim...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{lostItemsWithClaims.length > 0 && (
|
||||
<div className="bg-slate-800/50 backdrop-blur-sm rounded-2xl border border-slate-700 p-6">
|
||||
<h3 className="text-2xl font-bold text-white mb-4 flex items-center gap-3">
|
||||
<span className="text-3xl">😢</span>
|
||||
Barang Hilang Saya
|
||||
<span className="bg-orange-500/20 text-orange-400 text-sm px-3 py-1 rounded-full border border-orange-500/30">
|
||||
{lostItemsWithClaims.length} item dengan klaim pending
|
||||
</span>
|
||||
</h3>
|
||||
<p className="text-slate-400 mb-6">
|
||||
Ada yang menemukan barang Anda! Silakan approve atau reject klaim
|
||||
mereka.
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
{lostItemsWithClaims.map((lostItem) => {
|
||||
const statusBadge = getStatusBadge(
|
||||
lostItem.direct_claim_status || lostItem.status
|
||||
);
|
||||
const isWaitingOwner =
|
||||
lostItem.direct_claim_status === "waiting_owner";
|
||||
const isVerified =
|
||||
lostItem.direct_claim_status === "verified" ||
|
||||
lostItem.status === "found";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={lostItem.id}
|
||||
className={`bg-slate-900/80 rounded-xl border ${
|
||||
isWaitingOwner
|
||||
? "border-orange-500/50 shadow-lg shadow-orange-500/20"
|
||||
: "border-slate-700"
|
||||
} p-6 hover:shadow-xl transition-all`}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h4 className="text-xl font-bold text-white">
|
||||
{lostItem.name}
|
||||
</h4>
|
||||
<span
|
||||
className={`${statusBadge.color} text-white text-xs px-3 py-1 rounded-full font-medium`}
|
||||
>
|
||||
{statusBadge.emoji} {statusBadge.text}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1 text-sm text-slate-400">
|
||||
<p>
|
||||
<span className="font-medium">📦 Kategori:</span>{" "}
|
||||
{lostItem.category}
|
||||
</p>
|
||||
{lostItem.color && (
|
||||
<p>
|
||||
<span className="font-medium">🎨 Warna:</span>{" "}
|
||||
{lostItem.color}
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<span className="font-medium">📍 Lokasi Hilang:</span>{" "}
|
||||
{lostItem.location}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">
|
||||
📅 Tanggal Hilang:
|
||||
</span>{" "}
|
||||
{new Date(lostItem.date_lost).toLocaleDateString(
|
||||
"id-ID"
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
<span className="font-medium">📝 Deskripsi:</span>{" "}
|
||||
{lostItem.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ⭐ Display claim card with proper context */}
|
||||
{lostItem.direct_claim && (
|
||||
<div className="mt-4">
|
||||
<ClaimCard
|
||||
claim={{
|
||||
...lostItem.direct_claim,
|
||||
lost_item_user_id: lostItem.user_id, // ⭐ Pass owner ID
|
||||
}}
|
||||
currentUserId={user?.id} // ⭐ Pass current user ID
|
||||
onUserRespond={handlers.handleUserRespondClaim}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Fallback untuk direct claim tanpa detail */}
|
||||
{!lostItem.direct_claim && isWaitingOwner && (
|
||||
<div className="bg-orange-900/30 border border-orange-500/30 rounded-lg p-4 mt-4">
|
||||
<p className="text-orange-300 font-semibold mb-3 flex items-center gap-2">
|
||||
<span className="text-2xl">🔔</span>
|
||||
Ada yang mengaku menemukan barang ini!
|
||||
</p>
|
||||
<p className="text-sm text-slate-300 mb-4">
|
||||
Seseorang telah mengajukan klaim untuk barang ini.
|
||||
Silakan putuskan apakah ini benar barang Anda.
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() =>
|
||||
handlers.handleUserRespondClaim(
|
||||
lostItem.direct_claim_id,
|
||||
"approve"
|
||||
)
|
||||
}
|
||||
className="flex-1 bg-gradient-to-r from-green-600 to-green-700 text-white px-4 py-3 rounded-lg font-semibold hover:from-green-700 hover:to-green-800 transition shadow-lg shadow-green-500/30"
|
||||
>
|
||||
✅ Approve (Ini barang saya!)
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
handlers.handleUserRespondClaim(
|
||||
lostItem.direct_claim_id,
|
||||
"reject"
|
||||
)
|
||||
}
|
||||
className="flex-1 bg-gradient-to-r from-red-600 to-red-700 text-white px-4 py-3 rounded-lg font-semibold hover:from-red-700 hover:to-red-800 transition shadow-lg shadow-red-500/30"
|
||||
>
|
||||
❌ Reject (Bukan barang saya)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isVerified && (
|
||||
<div className="bg-green-900/30 border border-green-500/30 rounded-lg p-4 mt-4">
|
||||
<p className="text-green-300 font-semibold mb-3 flex items-center gap-2">
|
||||
<span className="text-2xl">✅</span>
|
||||
Klaim Disetujui!
|
||||
</p>
|
||||
<p className="text-sm text-slate-300 mb-4">
|
||||
Anda telah menyetujui bahwa penemu menemukan barang
|
||||
Anda. Koordinasikan pengambilan barang, lalu konfirmasi
|
||||
setelah menerima barang.
|
||||
</p>
|
||||
<button
|
||||
onClick={() =>
|
||||
handlers.handleUserCompleteCase(
|
||||
lostItem.direct_claim_id
|
||||
)
|
||||
}
|
||||
className="w-full bg-gradient-to-r from-purple-600 to-purple-700 text-white px-4 py-3 rounded-lg font-semibold hover:from-purple-700 hover:to-purple-800 transition shadow-lg shadow-purple-500/30"
|
||||
>
|
||||
🎉 Konfirmasi: Saya Sudah Terima Barang
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{regularClaims.length > 0 && (
|
||||
<div className="bg-slate-800/50 backdrop-blur-sm rounded-2xl border border-slate-700 p-6">
|
||||
<h3 className="text-2xl font-bold text-white mb-4 flex items-center gap-3">
|
||||
<span className="text-3xl">🤝</span>
|
||||
Klaim Barang yang Saya Ajukan
|
||||
<span className="bg-blue-500/20 text-blue-400 text-sm px-3 py-1 rounded-full border border-blue-500/30">
|
||||
{regularClaims.length} klaim
|
||||
</span>
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{regularClaims.map((claim) => (
|
||||
<ClaimCard
|
||||
key={claim.id}
|
||||
claim={{
|
||||
...claim,
|
||||
user_id: claim.user_id, // Penemu
|
||||
lost_item_user_id: claim.lost_item?.user_id, // Pemilik
|
||||
}}
|
||||
currentUserId={user?.id} // ⭐ Pass current user ID
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{lostItemsWithClaims.length === 0 && regularClaims.length === 0 && (
|
||||
<div className="text-center py-16 bg-slate-800/30 rounded-2xl border border-slate-700">
|
||||
<div className="text-6xl mb-4">🔭</div>
|
||||
<h3 className="text-xl font-bold text-white mb-2">Belum Ada Klaim</h3>
|
||||
<p className="text-slate-400">
|
||||
Anda belum memiliki klaim atau barang hilang dengan klaim pending.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
window.MyClaimsTab = MyClaimsTab;
|
||||
@@ -0,0 +1,308 @@
|
||||
const MyFoundItemsTab = ({ state, handlers }) => {
|
||||
const { myFoundItems, loading, user } = state;
|
||||
const { showToast, loadData } = handlers;
|
||||
|
||||
const handleApproveItem = async (item) => {
|
||||
if (!item.direct_claim_id) {
|
||||
showToast("⚠️ Tidak ada klaim untuk item ini", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmMsg = `✅ Konfirmasi Penerimaan Barang
|
||||
|
||||
Apakah Anda yakin barang "${item.name}" ini adalah milik Anda?
|
||||
|
||||
Jika Ya:
|
||||
- Status akan berubah menjadi "verified"
|
||||
- Anda bisa koordinasi dengan penemu untuk pengambilan
|
||||
- Kasus akan dilanjutkan
|
||||
|
||||
⚠️ Pastikan ini benar-benar barang Anda!`;
|
||||
|
||||
if (!confirm(confirmMsg)) return;
|
||||
|
||||
try {
|
||||
state.setLoading(true);
|
||||
|
||||
// ✅ ENDPOINT YANG BENAR - menggunakan claim ID
|
||||
await ApiUtils.post(`/api/user/claims/${item.direct_claim_id}/respond`, {
|
||||
action: "approve",
|
||||
});
|
||||
|
||||
showToast(
|
||||
"✅ Berhasil! Silakan koordinasi pengambilan dengan penemu.",
|
||||
"success"
|
||||
);
|
||||
loadData();
|
||||
} catch (error) {
|
||||
console.error("Error approving item:", error);
|
||||
showToast("Gagal menerima: " + error.message, "error");
|
||||
} finally {
|
||||
state.setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRejectItem = async (item) => {
|
||||
if (!item.direct_claim_id) {
|
||||
showToast("⚠️ Tidak ada klaim untuk item ini", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const reason = prompt(
|
||||
"❌ Alasan Penolakan (opsional):\n\nContoh: Bukan barang saya, warna berbeda, dll."
|
||||
);
|
||||
|
||||
if (reason === null) return; // User cancelled
|
||||
|
||||
const confirmMsg = `❌ Konfirmasi Penolakan
|
||||
|
||||
Yakin menolak barang "${item.name}"?
|
||||
${reason ? `Alasan: ${reason}` : "(Tanpa alasan)"}
|
||||
|
||||
Barang akan kembali dalam status pencarian.`;
|
||||
|
||||
if (!confirm(confirmMsg)) return;
|
||||
|
||||
try {
|
||||
state.setLoading(true);
|
||||
|
||||
// ✅ ENDPOINT YANG BENAR - menggunakan claim ID
|
||||
await ApiUtils.post(`/api/user/claims/${item.direct_claim_id}/respond`, {
|
||||
action: "reject",
|
||||
});
|
||||
|
||||
showToast(
|
||||
"❌ Barang ditolak. Status kembali dalam pencarian.",
|
||||
"success"
|
||||
);
|
||||
loadData();
|
||||
} catch (error) {
|
||||
console.error("Error rejecting item:", error);
|
||||
showToast("Gagal menolak: " + error.message, "error");
|
||||
} finally {
|
||||
state.setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
📦 Barang yang Dilaporkan Orang Lain Menemukan
|
||||
</h2>
|
||||
<p className="text-slate-400 text-sm mt-1">
|
||||
Barang hilang Anda yang dilaporkan orang lain menemukan - butuh
|
||||
konfirmasi Anda
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => loadData()}
|
||||
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 Cards */}
|
||||
<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">
|
||||
{
|
||||
myFoundItems.filter((i) => i.status === "pending_verification")
|
||||
.length
|
||||
}
|
||||
</div>
|
||||
<div className="text-slate-400 text-sm">Menunggu Konfirmasi</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">
|
||||
{myFoundItems.filter((i) => i.status === "verified").length}
|
||||
</div>
|
||||
<div className="text-slate-400 text-sm">Diverifikasi</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">
|
||||
{myFoundItems.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>
|
||||
) : myFoundItems.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{myFoundItems.map((item) => {
|
||||
const isPendingVerification =
|
||||
item.status === "pending_verification";
|
||||
const isVerified = item.status === "verified";
|
||||
|
||||
return (
|
||||
<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 ${
|
||||
isPendingVerification
|
||||
? "border-yellow-500/50 hover:border-yellow-500 shadow-yellow-500/20"
|
||||
: isVerified
|
||||
? "border-green-500/50 hover:border-green-500"
|
||||
: "border-slate-600 hover:border-blue-500"
|
||||
}`}
|
||||
>
|
||||
{/* 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>
|
||||
{item.category_name && (
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
🏷️ {item.category_name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className={`px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider ${
|
||||
isPendingVerification
|
||||
? "bg-yellow-500/20 text-yellow-400 border border-yellow-500/50 animate-pulse"
|
||||
: isVerified
|
||||
? "bg-green-500/20 text-green-400 border border-green-500/50"
|
||||
: Helpers.getStatusBadgeClass(item.status)
|
||||
}`}
|
||||
>
|
||||
{isPendingVerification
|
||||
? "⏳ Menunggu"
|
||||
: isVerified
|
||||
? "✅ Verified"
|
||||
: item.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Item Details */}
|
||||
<div className="space-y-2 mb-4">
|
||||
{item.color && (
|
||||
<div className="flex items-center gap-2 text-sm text-slate-300">
|
||||
<span className="text-blue-400">🎨</span>
|
||||
<span>Warna: {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>
|
||||
)}
|
||||
{item.date_found && (
|
||||
<div className="flex items-center gap-2 text-sm text-slate-300">
|
||||
<span className="text-blue-400">📅</span>
|
||||
<span>{Helpers.formatDate(item.date_found)}</span>
|
||||
</div>
|
||||
)}
|
||||
{item.reporter_name && (
|
||||
<div className="flex items-center gap-2 text-sm text-slate-300">
|
||||
<span className="text-blue-400">👤</span>
|
||||
<span>Ditemukan oleh: {item.reporter_name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
{item.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>
|
||||
)}
|
||||
|
||||
{/* Pending Verification Alert */}
|
||||
{isPendingVerification && (
|
||||
<div className="bg-yellow-500/10 border-2 border-yellow-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-yellow-400">
|
||||
Konfirmasi Diperlukan
|
||||
</strong>
|
||||
</div>
|
||||
<p className="text-xs text-slate-300">
|
||||
Seseorang menemukan barang yang sesuai dengan laporan
|
||||
Anda. Silakan konfirmasi apakah ini benar barang Anda.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Verified Alert */}
|
||||
{isVerified && (
|
||||
<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 Diverifikasi
|
||||
</strong>
|
||||
</div>
|
||||
<p className="text-xs text-slate-300">
|
||||
Anda telah mengonfirmasi ini barang Anda. Koordinasikan
|
||||
pengambilan.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2">
|
||||
{isPendingVerification && (
|
||||
<>
|
||||
{/* Approve Button */}
|
||||
<button
|
||||
onClick={() => handleApproveItem(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 font-semibold flex items-center justify-center gap-1"
|
||||
>
|
||||
<span>✅</span> Terima
|
||||
</button>
|
||||
|
||||
{/* Reject Button */}
|
||||
<button
|
||||
onClick={() => handleRejectItem(item)}
|
||||
className="flex-1 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 font-semibold flex items-center justify-center gap-1"
|
||||
>
|
||||
<span>❌</span> Tolak
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Detail Button */}
|
||||
<button
|
||||
onClick={() =>
|
||||
handlers.handleViewDetail &&
|
||||
handlers.handleViewDetail(item)
|
||||
}
|
||||
className={`${
|
||||
isPendingVerification ? "px-3" : "flex-1"
|
||||
} 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> {isPendingVerification ? "" : "Detail"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-slate-400">
|
||||
<div className="text-6xl mb-4">📭</div>
|
||||
<p>Belum ada barang yang dilaporkan ditemukan</p>
|
||||
<p className="text-sm mt-2">
|
||||
Ketika seseorang menemukan barang Anda, akan muncul di sini
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
window.MyFoundItemsTab = MyFoundItemsTab;
|
||||
@@ -0,0 +1,321 @@
|
||||
const MyLostItemsTab = ({ state, handlers }) => {
|
||||
const { myLostItems, myClaims, loading } = state;
|
||||
|
||||
const lostItemsWithClaims = myLostItems.filter((item) => {
|
||||
if (!item.direct_claim_id || !item.direct_claim_status) {
|
||||
return false;
|
||||
}
|
||||
const excludedStatuses = ["completed", "resolved", "case_closed"];
|
||||
if (excludedStatuses.includes(item.status)) {
|
||||
return false;
|
||||
}
|
||||
if (excludedStatuses.includes(item.direct_claim_status)) {
|
||||
return false;
|
||||
}
|
||||
const pendingStatuses = ["waiting_owner", "verified", "claimed", "pending"];
|
||||
return pendingStatuses.includes(item.direct_claim_status);
|
||||
});
|
||||
|
||||
const regularClaims = myClaims || [];
|
||||
|
||||
// UPDATED: Menambahkan parameter isMyLostItem (default true)
|
||||
const getStatusBadge = (status, isMyLostItem = true) => {
|
||||
const badges = {
|
||||
active: { color: "bg-blue-500", text: "📍 Masih Hilang", emoji: "😢" },
|
||||
claimed: {
|
||||
color: "bg-yellow-500",
|
||||
text: "⏳ Menunggu Persetujuan Anda",
|
||||
emoji: "🤔",
|
||||
},
|
||||
found: { color: "bg-green-500", text: "✅ Ditemukan", emoji: "🎉" },
|
||||
completed: {
|
||||
color: "bg-purple-600",
|
||||
text: "✅ Selesai - Barang Diterima",
|
||||
emoji: "🎉",
|
||||
},
|
||||
resolved: {
|
||||
color: "bg-purple-600",
|
||||
text: "✅ Selesai - Dikembalikan",
|
||||
emoji: "🎉",
|
||||
},
|
||||
pending: {
|
||||
color: "bg-yellow-500",
|
||||
text: "⏳ Menunggu Verifikasi",
|
||||
emoji: "⏳",
|
||||
},
|
||||
approved: { color: "bg-green-500", text: "✅ Disetujui", emoji: "✅" },
|
||||
rejected: { color: "bg-red-500", text: "❌ Ditolak", emoji: "❌" },
|
||||
waiting_owner: {
|
||||
color: "bg-orange-500",
|
||||
// LOGIC FIX: Membedakan teks berdasarkan kepemilikan
|
||||
text: isMyLostItem ? "⏳ Menunggu Anda" : "⏳ Menunggu Owner",
|
||||
emoji: "👤",
|
||||
},
|
||||
verified: {
|
||||
color: "bg-green-600",
|
||||
text: "✅ Terverifikasi",
|
||||
emoji: "✅",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
badges[status] || {
|
||||
color: "bg-gray-500",
|
||||
text: status,
|
||||
emoji: "❓",
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-center py-12 text-slate-400">
|
||||
<div className="animate-spin text-4xl mb-4">⏳</div>
|
||||
<p>Memuat data klaim...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Header Section */}
|
||||
<div className="flex justify-between items-center bg-gradient-to-r from-blue-600 to-blue-700 rounded-xl p-4 shadow-lg">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-white">
|
||||
😢 Barang Hilang Saya
|
||||
</h3>
|
||||
<p className="text-blue-100 text-sm">
|
||||
Kelola laporan kehilangan barang Anda
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => state.setShowReportLostModal(true)}
|
||||
className="px-6 py-3 bg-white text-blue-600 rounded-lg font-bold hover:bg-blue-50 transition shadow-xl hover:scale-105 transform flex items-center gap-2"
|
||||
>
|
||||
<span className="text-xl">➕</span>
|
||||
Lapor Kehilangan
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* SECTION 1: Barang Hilang Saya (Anda sebagai Owner) */}
|
||||
{lostItemsWithClaims.length > 0 && (
|
||||
<div className="bg-slate-800/50 backdrop-blur-sm rounded-2xl border border-slate-700 p-6">
|
||||
<h3 className="text-2xl font-bold text-white mb-4 flex items-center gap-3">
|
||||
<span className="text-3xl">😢</span>
|
||||
Barang Hilang Saya
|
||||
<span className="bg-orange-500/20 text-orange-400 text-sm px-3 py-1 rounded-full border border-orange-500/30">
|
||||
{lostItemsWithClaims.length} item dengan klaim pending
|
||||
</span>
|
||||
</h3>
|
||||
<p className="text-slate-400 mb-6">
|
||||
Ada yang menemukan barang Anda! Silakan approve atau reject klaim
|
||||
mereka.
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
{lostItemsWithClaims.map((lostItem) => {
|
||||
// Context: True (Ini barang saya)
|
||||
const statusBadge = getStatusBadge(
|
||||
lostItem.direct_claim_status || lostItem.status,
|
||||
true
|
||||
);
|
||||
const isWaitingOwner =
|
||||
lostItem.direct_claim_status === "waiting_owner";
|
||||
const isVerified =
|
||||
lostItem.direct_claim_status === "verified" ||
|
||||
lostItem.status === "found";
|
||||
return (
|
||||
<div
|
||||
key={lostItem.id}
|
||||
className={`bg-slate-900/80 rounded-xl border ${
|
||||
isWaitingOwner
|
||||
? "border-orange-500/50 shadow-lg shadow-orange-500/20"
|
||||
: "border-slate-700"
|
||||
} p-6 hover:shadow-xl transition-all`}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h4 className="text-xl font-bold text-white">
|
||||
{lostItem.name}
|
||||
</h4>
|
||||
<span
|
||||
className={`${statusBadge.color} text-white text-xs px-3 py-1 rounded-full font-medium`}
|
||||
>
|
||||
{statusBadge.emoji} {statusBadge.text}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1 text-sm text-slate-400">
|
||||
<p>
|
||||
<span className="font-medium">📦 Kategori:</span>{" "}
|
||||
{lostItem.category}
|
||||
</p>
|
||||
{lostItem.color && (
|
||||
<p>
|
||||
<span className="font-medium">🎨 Warna:</span>{" "}
|
||||
{lostItem.color}
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<span className="font-medium">📍 Lokasi Hilang:</span>{" "}
|
||||
{lostItem.location}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">
|
||||
📅 Tanggal Hilang:
|
||||
</span>{" "}
|
||||
{new Date(lostItem.date_lost).toLocaleDateString(
|
||||
"id-ID"
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
<span className="font-medium">📝 Deskripsi:</span>{" "}
|
||||
{lostItem.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isWaitingOwner && (
|
||||
<div className="bg-orange-900/30 border border-orange-500/30 rounded-lg p-4 mt-4">
|
||||
<p className="text-orange-300 font-semibold mb-3 flex items-center gap-2">
|
||||
<span className="text-2xl">🔔</span>
|
||||
Ada yang mengaku menemukan barang ini!
|
||||
</p>
|
||||
<p className="text-sm text-slate-300 mb-4">
|
||||
Seseorang telah mengajukan klaim untuk barang ini.
|
||||
Silakan putuskan apakah ini benar barang Anda.
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() =>
|
||||
handlers.handleUserRespondClaim(
|
||||
lostItem.direct_claim_id,
|
||||
"approve"
|
||||
)
|
||||
}
|
||||
className="flex-1 bg-gradient-to-r from-green-600 to-green-700 text-white px-4 py-3 rounded-lg font-semibold hover:from-green-700 hover:to-green-800 transition shadow-lg shadow-green-500/30"
|
||||
>
|
||||
✅ Approve (Ini barang saya!)
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
handlers.handleUserRespondClaim(
|
||||
lostItem.direct_claim_id,
|
||||
"reject"
|
||||
)
|
||||
}
|
||||
className="flex-1 bg-gradient-to-r from-red-600 to-red-700 text-white px-4 py-3 rounded-lg font-semibold hover:from-red-700 hover:to-red-800 transition shadow-lg shadow-red-500/30"
|
||||
>
|
||||
❌ Reject (Bukan barang saya)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isVerified && (
|
||||
<div className="bg-green-900/30 border border-green-500/30 rounded-lg p-4 mt-4">
|
||||
<p className="text-green-300 font-semibold mb-3 flex items-center gap-2">
|
||||
<span className="text-2xl">✅</span>
|
||||
Klaim Disetujui!
|
||||
</p>
|
||||
<p className="text-sm text-slate-300 mb-4">
|
||||
Anda telah menyetujui bahwa penemu menemukan barang
|
||||
Anda. Koordinasikan pengambilan barang, lalu konfirmasi
|
||||
setelah menerima barang.
|
||||
</p>
|
||||
<button
|
||||
onClick={() =>
|
||||
handlers.handleUserCompleteCase(
|
||||
lostItem.direct_claim_id
|
||||
)
|
||||
}
|
||||
className="w-full bg-gradient-to-r from-purple-600 to-purple-700 text-white px-4 py-3 rounded-lg font-semibold hover:from-purple-700 hover:to-purple-800 transition shadow-lg shadow-purple-500/30"
|
||||
>
|
||||
🎉 Konfirmasi: Saya Sudah Terima Barang
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SECTION 2: Klaim Barang yang Saya Ajukan (Anda sebagai Penemu) */}
|
||||
{regularClaims.length > 0 && (
|
||||
<div className="bg-slate-800/50 backdrop-blur-sm rounded-2xl border border-slate-700 p-6">
|
||||
<h3 className="text-2xl font-bold text-white mb-4 flex items-center gap-3">
|
||||
<span className="text-3xl">🤝</span>
|
||||
Klaim Barang yang Saya Ajukan
|
||||
<span className="bg-blue-500/20 text-blue-400 text-sm px-3 py-1 rounded-full border border-blue-500/30">
|
||||
{regularClaims.length} klaim
|
||||
</span>
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{regularClaims.map((claim) => {
|
||||
// Context: False (Ini klaim saya ke orang lain/owner)
|
||||
// UPDATED: Pass false agar text menjadi "Menunggu Owner"
|
||||
const statusBadge = getStatusBadge(claim.status, false);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={claim.id}
|
||||
className="bg-slate-900/80 rounded-xl border border-slate-700 p-5 hover:shadow-xl transition-all"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<h4 className="text-lg font-bold text-white flex-1">
|
||||
{claim.item_name}
|
||||
</h4>
|
||||
<span
|
||||
className={`${statusBadge.color} text-white text-xs px-3 py-1 rounded-full font-medium whitespace-nowrap ml-2`}
|
||||
>
|
||||
{statusBadge.emoji} {statusBadge.text}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm text-slate-400">
|
||||
<p>
|
||||
<span className="font-medium">📝 Deskripsi:</span>{" "}
|
||||
{claim.description}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">📞 Kontak:</span>{" "}
|
||||
{claim.contact}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">📅 Tanggal Klaim:</span>{" "}
|
||||
{new Date(claim.created_at).toLocaleDateString("id-ID")}
|
||||
</p>
|
||||
{claim.notes && (
|
||||
<p className="text-yellow-400 mt-2">
|
||||
<span className="font-medium">💬 Catatan:</span>{" "}
|
||||
{claim.notes}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{lostItemsWithClaims.length === 0 && regularClaims.length === 0 && (
|
||||
<div className="text-center py-16 bg-slate-800/30 rounded-2xl border border-slate-700">
|
||||
<div className="text-6xl mb-4">🔭</div>
|
||||
<h3 className="text-xl font-bold text-white mb-2">Belum Ada Klaim</h3>
|
||||
<p className="text-slate-400 mb-6">
|
||||
Anda belum memiliki klaim atau barang hilang dengan klaim pending.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => state.setShowReportLostModal(true)}
|
||||
className="px-6 py-3 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"
|
||||
>
|
||||
➕ Lapor Kehilangan Sekarang
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
window.MyLostItemsTab = MyLostItemsTab;
|
||||
@@ -0,0 +1,131 @@
|
||||
const PublicLostItemsTab = ({ state, handlers }) => {
|
||||
const { publicLostItems, user } = state;
|
||||
|
||||
const getStatusDisplay = (lostItem) => {
|
||||
const statusMap = {
|
||||
active: { label: "Aktif", color: "bg-green-600 text-white" },
|
||||
claimed: { label: "Sedang Diklaim", color: "bg-yellow-600 text-white" },
|
||||
resolved: { label: "Selesai", color: "bg-blue-600 text-white" },
|
||||
};
|
||||
return (
|
||||
statusMap[lostItem.status] || {
|
||||
label: lostItem.status,
|
||||
color: "bg-gray-600 text-white",
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const shouldShowFoundButton = (lostItem) => {
|
||||
return lostItem.user_id !== user.id && lostItem.status === "active";
|
||||
};
|
||||
|
||||
const shouldShowClaimedInfo = (lostItem) => {
|
||||
return lostItem.status === "claimed" || lostItem.status === "resolved";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700">
|
||||
<h3 className="text-2xl font-bold text-white mb-4">
|
||||
😢 Barang Hilang (Dari Pengguna Lain)
|
||||
</h3>
|
||||
<p className="text-slate-400 mb-6">
|
||||
Jika Anda menemukan barang yang cocok, klik "Saya Menemukan!" untuk
|
||||
memberitahu pemiliknya
|
||||
</p>
|
||||
|
||||
{publicLostItems.length === 0 ? (
|
||||
<div className="text-center py-12 text-slate-400">
|
||||
<p className="text-6xl mb-4">🔭</p>
|
||||
<p className="text-xl">Tidak ada barang hilang saat ini</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{publicLostItems.map((lostItem) => {
|
||||
const statusDisplay = getStatusDisplay(lostItem);
|
||||
const showFoundButton = shouldShowFoundButton(lostItem);
|
||||
const showClaimedInfo = shouldShowClaimedInfo(lostItem);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={lostItem.id}
|
||||
className={`bg-slate-900/50 rounded-lg p-5 border transition ${
|
||||
lostItem.status === "resolved"
|
||||
? "border-blue-500/50 opacity-75"
|
||||
: "border-slate-700 hover:border-blue-500"
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<h4 className="font-bold text-white text-lg">
|
||||
{lostItem.name}
|
||||
</h4>
|
||||
<p className="text-sm text-slate-400">
|
||||
{lostItem.category}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className={`text-xs px-2 py-1 rounded-full ${statusDisplay.color}`}
|
||||
>
|
||||
{statusDisplay.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 mb-4 text-sm text-slate-300">
|
||||
<p>
|
||||
<strong>📍 Lokasi:</strong>{" "}
|
||||
{lostItem.location || "Tidak disebutkan"}
|
||||
</p>
|
||||
<p>
|
||||
<strong>🎨 Warna:</strong>{" "}
|
||||
{lostItem.color || "Tidak disebutkan"}
|
||||
</p>
|
||||
<p>
|
||||
<strong>📅 Tanggal Hilang:</strong>{" "}
|
||||
{new Date(lostItem.date_lost).toLocaleDateString("id-ID")}
|
||||
</p>
|
||||
<p>
|
||||
<strong>📝 Deskripsi:</strong> {lostItem.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* PERBAIKAN: Ubah dari handleOpenDirectClaimModal ke handleOpenFoundOption */}
|
||||
{showFoundButton && (
|
||||
<button
|
||||
onClick={() => handlers.handleOpenFoundOption(lostItem)}
|
||||
className="w-full bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white px-4 py-3 rounded-lg font-semibold transition shadow-lg"
|
||||
>
|
||||
🎉 Saya Menemukan Barang Ini!
|
||||
</button>
|
||||
)}
|
||||
|
||||
{lostItem.status === "claimed" && (
|
||||
<div className="bg-yellow-900/30 border border-yellow-700/50 rounded-lg p-3 text-center">
|
||||
<p className="text-yellow-400 text-sm">
|
||||
⏳ Sedang menunggu konfirmasi pemilik
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{lostItem.status === "resolved" && (
|
||||
<div className="bg-blue-900/30 border border-blue-700/50 rounded-lg p-3 text-center">
|
||||
<p className="text-blue-400 text-sm font-semibold">
|
||||
✅ Barang sudah dikembalikan ke pemilik
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{lostItem.user_id === user.id && (
|
||||
<div className="bg-purple-900/30 border border-purple-700/50 rounded-lg p-3 text-center">
|
||||
<p className="text-purple-400 text-sm">
|
||||
📋 Ini laporan kehilangan Anda
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,429 @@
|
||||
const useUserHandlers = (state) => {
|
||||
const {
|
||||
setToast,
|
||||
setLoading,
|
||||
setStats,
|
||||
setItems,
|
||||
setMyFoundItems,
|
||||
setMyLostItems,
|
||||
setPublicLostItems,
|
||||
setMyClaims,
|
||||
setSelectedItem,
|
||||
setShowDetailModal,
|
||||
setShowClaimModal,
|
||||
setShowReportFoundModal,
|
||||
setPhotoPreview,
|
||||
setSuggestedLostItems,
|
||||
setShowSelectLostItemModal,
|
||||
setClaimInitialData,
|
||||
myLostItems,
|
||||
} = state;
|
||||
|
||||
const showToast = (message, type = "info") => {
|
||||
setToast({ message, type });
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
state.setLoading(true);
|
||||
const [
|
||||
itemsResponse,
|
||||
myFoundResponse,
|
||||
myLostResponse,
|
||||
publicLostResponse,
|
||||
myClaimsResponse,
|
||||
categoriesResponse,
|
||||
] = await Promise.all([
|
||||
ApiUtils.get(`${CONFIG.API_ENDPOINTS.ITEMS}?page=1&limit=100`),
|
||||
ApiUtils.get(CONFIG.API_ENDPOINTS.USER.ITEMS).catch(() => ({
|
||||
data: [],
|
||||
})),
|
||||
ApiUtils.get(CONFIG.API_ENDPOINTS.USER.LOST_ITEMS).catch(() => ({
|
||||
data: [],
|
||||
})),
|
||||
ApiUtils.get(CONFIG.API_ENDPOINTS.LOST_ITEMS).catch(() => ({
|
||||
data: [],
|
||||
})),
|
||||
ApiUtils.get(CONFIG.API_ENDPOINTS.USER.CLAIMS).catch(() => ({
|
||||
data: [],
|
||||
})),
|
||||
ApiUtils.get(
|
||||
CONFIG.API_ENDPOINTS.CATEGORIES || "/api/categories"
|
||||
).catch(() => ({ data: [] })),
|
||||
]);
|
||||
|
||||
state.setStats({
|
||||
lost_items: myLostResponse.data?.length || 0,
|
||||
found_items: myFoundResponse.data?.length || 0,
|
||||
claims: myClaimsResponse.data?.length || 0,
|
||||
});
|
||||
|
||||
state.setItems(itemsResponse.data || []);
|
||||
state.setMyFoundItems(myFoundResponse.data || []);
|
||||
state.setMyLostItems(myLostResponse.data || []);
|
||||
state.setPublicLostItems(publicLostResponse.data || []);
|
||||
state.setMyClaims(myClaimsResponse.data || []);
|
||||
|
||||
if (state.setCategories) {
|
||||
state.setCategories(categoriesResponse.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading data:", error);
|
||||
showToast("Gagal memuat data. Cek koneksi database.", "error");
|
||||
} finally {
|
||||
state.setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
if (Helpers.showConfirm("Apakah Anda yakin ingin logout?")) {
|
||||
AuthUtils.clearAuth();
|
||||
window.location.href = "/login";
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewDetail = async (item) => {
|
||||
try {
|
||||
const response = await ApiUtils.get(
|
||||
`${CONFIG.API_ENDPOINTS.ITEMS}/${item.id}`
|
||||
);
|
||||
setSelectedItem(response.data || response);
|
||||
setShowDetailModal(true);
|
||||
} catch (error) {
|
||||
showToast("Gagal memuat detail", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUserRespondClaim = async (claimId, action) => {
|
||||
const confirmMsg =
|
||||
action === "approve"
|
||||
? '✅ Yakin ini barang Anda?\n\nJika approve:\n- Status akan menjadi "Verified"\n- Anda bisa koordinasi pengambilan dengan penemu\n- Setelah terima barang, konfirmasi untuk case closed'
|
||||
: "❌ Yakin menolak?\n\nJika reject:\n- Klaim akan ditolak\n- Barang kembali dalam status pencarian\n- Orang lain bisa mengajukan klaim";
|
||||
|
||||
if (!confirm(confirmMsg)) return;
|
||||
|
||||
try {
|
||||
state.setLoading(true);
|
||||
// FIXED: URL endpoint sesuai controller
|
||||
await ApiUtils.post(`/api/claims/${claimId}/respond`, {
|
||||
action: action,
|
||||
});
|
||||
|
||||
showToast(
|
||||
action === "approve"
|
||||
? "✅ Berhasil! Silakan koordinasi pengambilan dengan penemu."
|
||||
: "❌ Klaim ditolak. Barang kembali dalam pencarian.",
|
||||
"success"
|
||||
);
|
||||
|
||||
// Reload data agar tampilan tombol berubah
|
||||
await loadData();
|
||||
} catch (error) {
|
||||
console.error("Error responding:", error);
|
||||
showToast(
|
||||
"Gagal memproses: " + (error.response?.data?.message || error.message),
|
||||
"error"
|
||||
);
|
||||
} finally {
|
||||
state.setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenDirectClaimModal = (lostItem) => {
|
||||
console.log("Membuka direct claim modal untuk:", lostItem.name);
|
||||
state.setSelectedLostItemForFound(lostItem);
|
||||
state.setShowDirectClaimModal(true);
|
||||
};
|
||||
|
||||
const handleClaim = (item) => {
|
||||
setSelectedItem(item);
|
||||
const matches = myLostItems.filter(
|
||||
(lostItem) =>
|
||||
lostItem.status === "active" &&
|
||||
lostItem.category_id === item.category_id
|
||||
);
|
||||
|
||||
if (matches.length > 0) {
|
||||
setSuggestedLostItems(matches);
|
||||
setShowSelectLostItemModal(true);
|
||||
} else {
|
||||
setClaimInitialData(null);
|
||||
setShowClaimModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectLostItemForClaim = (lostItem) => {
|
||||
const initialData = {
|
||||
description: `[Berdasarkan Laporan Hilang #${lostItem.id} - ${lostItem.name}]\n${lostItem.description}`,
|
||||
contact: state.user?.phone || "",
|
||||
};
|
||||
setClaimInitialData(initialData);
|
||||
setShowSelectLostItemModal(false);
|
||||
setShowClaimModal(true);
|
||||
};
|
||||
|
||||
const handleManualClaimEntry = () => {
|
||||
setClaimInitialData(null);
|
||||
setShowSelectLostItemModal(false);
|
||||
setShowClaimModal(true);
|
||||
};
|
||||
|
||||
const submitClaim = async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
|
||||
if (!formData.get("description") || !formData.get("contact")) {
|
||||
showToast("Deskripsi dan Kontak harus diisi", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
showToast("Memproses klaim...", "info");
|
||||
|
||||
let proofUrl = "";
|
||||
const proofFile = formData.get("proof");
|
||||
if (proofFile && proofFile.size > 0) {
|
||||
showToast("Mengupload bukti...", "info");
|
||||
const uploadFormData = new FormData();
|
||||
uploadFormData.append("proof", proofFile);
|
||||
const uploadResponse = await ApiUtils.uploadFile(
|
||||
"/api/upload/claim-proof",
|
||||
uploadFormData
|
||||
);
|
||||
proofUrl = uploadResponse.data.url;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
item_id: parseInt(state.selectedItem.id),
|
||||
description: formData.get("description"),
|
||||
contact: formData.get("contact"),
|
||||
proof_url: proofUrl,
|
||||
};
|
||||
|
||||
await ApiUtils.post(CONFIG.API_ENDPOINTS.CLAIMS, payload);
|
||||
showToast("Klaim berhasil disubmit!", "success");
|
||||
state.setShowClaimModal(false);
|
||||
loadData();
|
||||
} catch (error) {
|
||||
showToast(error.message || "Gagal submit klaim", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const submitReportLost = async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const dateLost = formData.get("date_lost");
|
||||
const dateLostISO = new Date(dateLost + "T00:00:00Z").toISOString();
|
||||
|
||||
const data = {
|
||||
name: formData.get("name"),
|
||||
category_id: parseInt(formData.get("category_id")),
|
||||
color: formData.get("color") || "",
|
||||
location: formData.get("location") || "",
|
||||
description: formData.get("description"),
|
||||
date_lost: dateLostISO,
|
||||
};
|
||||
|
||||
await ApiUtils.post(CONFIG.API_ENDPOINTS.LOST_ITEMS, data);
|
||||
showToast("Laporan kehilangan berhasil!", "success");
|
||||
state.setShowReportLostModal(false);
|
||||
loadData();
|
||||
} catch (error) {
|
||||
showToast("Gagal submit laporan: " + error.message, "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const submitReportFound = async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
let photoUrl = "";
|
||||
const photoFile = formData.get("photo");
|
||||
if (photoFile && photoFile.size > 0) {
|
||||
showToast("Mengupload foto...", "info");
|
||||
const uploadFormData = new FormData();
|
||||
uploadFormData.append("image", photoFile);
|
||||
const uploadData = await ApiUtils.uploadFile(
|
||||
CONFIG.API_ENDPOINTS.UPLOAD,
|
||||
uploadFormData
|
||||
);
|
||||
photoUrl = uploadData.data.url;
|
||||
}
|
||||
|
||||
const dateFound = formData.get("date_found");
|
||||
const dateFoundISO = new Date(dateFound + "T00:00:00Z").toISOString();
|
||||
|
||||
const payload = {
|
||||
name: formData.get("name"),
|
||||
category_id: parseInt(formData.get("category_id")),
|
||||
photo_url: photoUrl,
|
||||
location: formData.get("location"),
|
||||
description: formData.get("description"),
|
||||
secret_details: formData.get("secret_details"),
|
||||
date_found: dateFoundISO,
|
||||
reporter_name: formData.get("reporter_name"),
|
||||
reporter_contact: formData.get("reporter_contact"),
|
||||
};
|
||||
|
||||
if (
|
||||
state.selectedLostItemForFound &&
|
||||
state.foundReportMethod === "direct"
|
||||
) {
|
||||
payload.lost_item_id = state.selectedLostItemForFound.id;
|
||||
payload.verification_flow = "direct";
|
||||
payload.is_direct_to_owner = true;
|
||||
}
|
||||
|
||||
await ApiUtils.post(CONFIG.API_ENDPOINTS.ITEMS, payload);
|
||||
|
||||
if (
|
||||
state.foundReportMethod === "direct" &&
|
||||
state.selectedLostItemForFound
|
||||
) {
|
||||
showToast("Laporan terkirim ke PEMILIK untuk disetujui!", "success");
|
||||
} else {
|
||||
showToast("Laporan terkirim ke MANAGER untuk verifikasi.", "success");
|
||||
}
|
||||
|
||||
setShowReportFoundModal(false);
|
||||
setPhotoPreview(null);
|
||||
state.setSelectedLostItemForFound(null);
|
||||
loadData();
|
||||
} catch (error) {
|
||||
showToast("Gagal mengirim laporan: " + error.message, "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenFoundOption = (lostItem) => {
|
||||
console.log("Membuka opsi penemuan untuk:", lostItem.name);
|
||||
state.setSelectedLostItemForFound(lostItem);
|
||||
state.setShowFoundOptionModal(true);
|
||||
};
|
||||
|
||||
const handleSelectFoundOption = (method) => {
|
||||
console.log("Metode dipilih:", method);
|
||||
state.setFoundReportMethod(method);
|
||||
state.setShowFoundOptionModal(false);
|
||||
|
||||
if (method === "direct") {
|
||||
handleDirectClaimFlow();
|
||||
} else {
|
||||
state.setShowReportFoundModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDirectClaimFlow = () => {
|
||||
if (!state.selectedLostItemForFound) {
|
||||
showToast("Tidak ada lost item yang dipilih", "error");
|
||||
return;
|
||||
}
|
||||
state.setShowDirectClaimModal(true);
|
||||
};
|
||||
|
||||
const handlePhotoChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setPhotoPreview(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUserCompleteCase = async (claimId) => {
|
||||
const confirmMsg =
|
||||
'🎉 Konfirmasi Penerimaan Barang\n\nApakah Anda sudah menerima barang fisik ini?\n\n✅ Jika Ya:\n- Status akan menjadi "Completed"\n- Kasus ditutup secara permanen\n- Tidak bisa dibatalkan\n\n⚠️ Pastikan barang sudah di tangan Anda!';
|
||||
|
||||
if (!confirm(confirmMsg)) return;
|
||||
|
||||
try {
|
||||
state.setLoading(true);
|
||||
|
||||
// FIXED: Gunakan /api/claims/:id/complete (bukan /api/user/claims/:id/complete)
|
||||
await ApiUtils.post(`/api/claims/${claimId}/complete`, {});
|
||||
|
||||
showToast(
|
||||
"🎉 Selamat! Barang Anda telah kembali. Kasus selesai.",
|
||||
"success"
|
||||
);
|
||||
|
||||
loadData();
|
||||
} catch (error) {
|
||||
console.error("Error completing case:", error);
|
||||
showToast("Gagal menyelesaikan: " + error.message, "error");
|
||||
} finally {
|
||||
state.setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDirectClaimSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
|
||||
if (!state.selectedLostItemForFound) {
|
||||
showToast("Tidak ada lost item yang dipilih", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
state.setLoading(true);
|
||||
|
||||
const payload = {
|
||||
description: formData.get("description"),
|
||||
contact: formData.get("contact"),
|
||||
proof_url: formData.get("proof_url") || "",
|
||||
};
|
||||
|
||||
await ApiUtils.post(
|
||||
`/api/lost-items/${state.selectedLostItemForFound.id}/direct-claim`,
|
||||
payload
|
||||
);
|
||||
|
||||
showToast("✅ Klaim terkirim ke pemilik untuk persetujuan!", "success");
|
||||
state.setShowDirectClaimModal(false);
|
||||
state.setSelectedLostItemForFound(null);
|
||||
loadData();
|
||||
} catch (error) {
|
||||
console.error("Direct claim error:", error);
|
||||
showToast("Gagal mengirim klaim: " + error.message, "error");
|
||||
} finally {
|
||||
state.setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
showToast,
|
||||
loadData,
|
||||
handleLogout,
|
||||
handleViewDetail,
|
||||
handleClaim,
|
||||
submitClaim,
|
||||
submitReportLost,
|
||||
submitReportFound,
|
||||
handlePhotoChange,
|
||||
handleSelectLostItemForClaim,
|
||||
handleManualClaimEntry,
|
||||
handleOpenFoundOption,
|
||||
handleSelectFoundOption,
|
||||
handleUserRespondClaim,
|
||||
handleUserCompleteCase,
|
||||
handleDirectClaimSubmit,
|
||||
handleDirectClaimFlow,
|
||||
handleOpenDirectClaimModal,
|
||||
};
|
||||
};
|
||||
|
||||
window.useUserHandlers = useUserHandlers;
|
||||
@@ -0,0 +1,97 @@
|
||||
// assets/js/pages/user/useUserState.js
|
||||
const { useState } = React;
|
||||
|
||||
const useUserState = () => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [activeTab, setActiveTab] = useState("browse");
|
||||
const [stats, setStats] = useState({
|
||||
lost_items: 0,
|
||||
found_items: 0,
|
||||
claims: 0,
|
||||
});
|
||||
const [items, setItems] = useState([]);
|
||||
const [filteredItems, setFilteredItems] = useState([]);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [categoryFilter, setCategoryFilter] = useState("");
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||
const [showClaimModal, setShowClaimModal] = useState(false);
|
||||
const [showReportLostModal, setShowReportLostModal] = useState(false);
|
||||
const [showReportFoundModal, setShowReportFoundModal] = useState(false);
|
||||
const [toast, setToast] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [myFoundItems, setMyFoundItems] = useState([]);
|
||||
const [myLostItems, setMyLostItems] = useState([]);
|
||||
const [publicLostItems, setPublicLostItems] = useState([]);
|
||||
const [photoPreview, setPhotoPreview] = useState(null);
|
||||
const [myClaims, setMyClaims] = useState([]);
|
||||
const [showSelectLostItemModal, setShowSelectLostItemModal] = useState(false);
|
||||
const [suggestedLostItems, setSuggestedLostItems] = useState([]);
|
||||
const [claimInitialData, setClaimInitialData] = useState(null);
|
||||
const [categories, setCategories] = useState([]);
|
||||
const [showFoundOptionModal, setShowFoundOptionModal] = useState(false);
|
||||
const [selectedLostItemForFound, setSelectedLostItemForFound] =
|
||||
useState(null);
|
||||
const [foundReportMethod, setFoundReportMethod] = useState(null);
|
||||
const [showDirectClaimModal, setShowDirectClaimModal] = useState(false);
|
||||
|
||||
return {
|
||||
user,
|
||||
setUser,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
stats,
|
||||
setStats,
|
||||
items,
|
||||
setItems,
|
||||
filteredItems,
|
||||
setFilteredItems,
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
categoryFilter,
|
||||
setCategoryFilter,
|
||||
selectedItem,
|
||||
setSelectedItem,
|
||||
showDetailModal,
|
||||
setShowDetailModal,
|
||||
showClaimModal,
|
||||
setShowClaimModal,
|
||||
showReportLostModal,
|
||||
setShowReportLostModal,
|
||||
showReportFoundModal,
|
||||
setShowReportFoundModal,
|
||||
toast,
|
||||
setToast,
|
||||
loading,
|
||||
setLoading,
|
||||
myFoundItems,
|
||||
setMyFoundItems,
|
||||
myLostItems,
|
||||
setMyLostItems,
|
||||
publicLostItems,
|
||||
setPublicLostItems,
|
||||
photoPreview,
|
||||
setPhotoPreview,
|
||||
myClaims,
|
||||
setMyClaims,
|
||||
showSelectLostItemModal,
|
||||
setShowSelectLostItemModal,
|
||||
suggestedLostItems,
|
||||
setSuggestedLostItems,
|
||||
claimInitialData,
|
||||
setClaimInitialData,
|
||||
categories,
|
||||
setCategories,
|
||||
showFoundOptionModal,
|
||||
setShowFoundOptionModal,
|
||||
selectedLostItemForFound,
|
||||
setSelectedLostItemForFound,
|
||||
foundReportMethod,
|
||||
setFoundReportMethod,
|
||||
showDirectClaimModal,
|
||||
setShowDirectClaimModal,
|
||||
};
|
||||
};
|
||||
|
||||
// Export to window
|
||||
window.useUserState = useUserState;
|
||||
Reference in New Issue
Block a user