// assets/js/pages/manager/useManagerHandlers.js const useManagerHandlers = (state) => { const { setToast, setLoading, setStats, setItems, setClaims, setExpiredItems, setSelectedItem, setShowDetailModal, setSelectedClaim, setShowVerifyModal, setShowCloseCaseModal, setCloseCaseData, selectedClaim, closeCaseData, setShowReportFoundModal, setPhotoPreview, setShowEditItemModal, setSelectedItemToEdit, selectedItemToEdit, } = state; const showToast = (message, type = "info") => { setToast({ message, type }); }; const loadData = async () => { try { const [itemsResponse, claimsResponse, expiredResponse] = await Promise.all([ ApiUtils.get(`${CONFIG.API_ENDPOINTS.ITEMS}?page=1&limit=1000`), ApiUtils.get(CONFIG.API_ENDPOINTS.CLAIMS), ApiUtils.get( `${CONFIG.API_ENDPOINTS.ITEMS}?status=expired&page=1&limit=1000` ), ]); const allItems = itemsResponse.data || []; const allClaims = claimsResponse.data || []; const allExpired = expiredResponse.data || []; const calculatedStats = { total_items: allItems.length, pending_claims: allClaims.filter((c) => c.status === "pending").length, verified: allItems.filter((i) => i.status === "verified").length, expired: allExpired.length, }; setStats(calculatedStats); setItems(allItems); setClaims(allClaims); setExpiredItems(allExpired); } catch (error) { showToast("Gagal memuat data: " + error.message, "error"); } }; const handlePhotoChange = (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onloadend = () => { setPhotoPreview(reader.result); }; reader.readAsDataURL(file); } }; // ✅ ADD: Handle Edit Click const handleEditItemClick = async (item) => { try { // Reset preview foto agar tidak nyangkut foto sebelumnya setPhotoPreview(null); // ✅ TAMBAHKAN BARIS INI const response = await ApiUtils.get( `${CONFIG.API_ENDPOINTS.ITEMS}/${item.id}` ); setSelectedItemToEdit(response.data || response); setShowEditItemModal(true); } catch (error) { showToast("Gagal memuat data barang", "error"); } }; // ✅ ADD: Handle Update Item const handleUpdateItem = async (e) => { e.preventDefault(); const formData = new FormData(e.target); try { setLoading(true); showToast("Memproses update barang...", "info"); // ✅ LOGIKA BARU: Upload Foto jika ada let photoUrl = state.selectedItemToEdit.photo_url; // Default ke foto lama const photoFile = formData.get("photo"); if (photoFile && photoFile.size > 0) { showToast("Mengupload foto baru...", "info"); const uploadFormData = new FormData(); uploadFormData.append("image", photoFile); const uploadData = await ApiUtils.uploadFile( CONFIG.API_ENDPOINTS.UPLOAD, uploadFormData ); photoUrl = uploadData.data.url; } // ✅ SELESAI LOGIKA UPLOAD const dateFound = formData.get("date_found"); const dateFoundISO = new Date(dateFound + "T00:00:00Z").toISOString(); const itemData = { name: formData.get("name"), category_id: parseInt(formData.get("category_id")), photo_url: photoUrl, // ✅ Masukkan URL foto ke payload 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"), status: formData.get("status"), // ✅ KIRIM STATUS BARU reason: formData.get("reason") || "Admin/Manager update", }; await ApiUtils.put( // ✅ GUNAKAN PUT `${CONFIG.API_ENDPOINTS.ITEMS}/${state.selectedItemToEdit.id}`, itemData // ✅ Kirim data langsung ); showToast("Barang berhasil diupdate!", "success"); state.setShowEditItemModal(false); state.setSelectedItemToEdit(null); setPhotoPreview(null); loadData(); } catch (error) { showToast("Gagal update barang: " + error.message, "error"); } finally { state.setLoading(false); } }; const loadLostItems = async () => { try { setLoading(true); const response = await ApiUtils.get( `${CONFIG.API_ENDPOINTS.LOST_ITEMS}?page=1&limit=1000` ); // Enhance with AI suggestions (optional - can be done by backend) const lostItemsWithSuggestions = await Promise.all( (response.data || []).map(async (lostItem) => { if (lostItem.status === "active") { // Get potential matches based on category and date const potentialMatches = state.items .filter( (item) => item.status === "unclaimed" && item.category_id === lostItem.category_id && Math.abs( new Date(item.date_found) - new Date(lostItem.date_lost) ) < 7 * 24 * 60 * 60 * 1000 // Within 7 days ) .map((item) => ({ item_id: item.id, item_name: item.name, location: item.location, date_found: item.date_found, match_score: calculateMatchScore(lostItem, item), })) .sort((a, b) => b.match_score - a.match_score) .slice(0, 5); return { ...lostItem, ai_suggestions: potentialMatches, }; } return lostItem; }) ); state.setLostItems(lostItemsWithSuggestions); } catch (error) { showToast("Gagal memuat laporan barang hilang", "error"); } finally { setLoading(false); } }; // Simple match score calculator const calculateMatchScore = (lostItem, foundItem) => { // Helper function untuk hitung skor teks sederhana const getKeywordScore = (sourceText, targetText, minWordLength = 3) => { if (!sourceText || !targetText) return 0; const source = sourceText.toLowerCase(); const target = targetText.toLowerCase(); // 1. Cocok Sempurna if (source === target) return 100; // 2. Target mengandung source utuh if (target.includes(source)) return 95; // 3. Keyword Matching const keywords = source .split(/[\s,.]+/) .filter((w) => w.length >= minWordLength); if (keywords.length === 0) return 0; let matchCount = 0; keywords.forEach((word) => { if (target.includes(word)) { matchCount++; } }); return (matchCount / keywords.length) * 100; }; // --- HITUNG SKOR --- // 1. Skor Nama Barang (Bobot 50%) // Membandingkan Nama di laporan hilang vs Nama di barang temuan const nameScore = getKeywordScore(lostItem.name, foundItem.name, 2); // 2. Skor Secret Details (Bobot 50%) // Membandingkan Deskripsi User vs Secret Details Admin const secretScore = getKeywordScore( lostItem.description, foundItem.secret_details, 3 ); // --- TOTAL SKOR --- // Menggabungkan kedua skor dengan rata-rata const totalScore = nameScore * 0.5 + secretScore * 0.5; return Math.round(totalScore); }; const handleMatchLostItem = (lostItem) => { state.setSelectedLostItem(lostItem); state.setShowMatchLostItemModal(true); }; const submitMatchLostItem = async (lostItemId, foundItemId) => { try { setLoading(true); showToast("Menghubungkan barang...", "info"); // Call backend API to match await ApiUtils.post( `${CONFIG.API_ENDPOINTS.LOST_ITEMS}/${lostItemId}/match`, { item_id: foundItemId, } ); showToast("Barang berhasil dihubungkan!", "success"); state.setShowMatchLostItemModal(false); state.setSelectedLostItem(null); loadLostItems(); loadData(); } catch (error) { showToast("Gagal menghubungkan barang: " + error.message, "error"); } finally { setLoading(false); } }; const handleDeleteLostItem = async (lostItemId) => { try { setLoading(true); await ApiUtils.delete(`${CONFIG.API_ENDPOINTS.LOST_ITEMS}/${lostItemId}`); showToast("Laporan barang hilang berhasil dihapus!", "success"); loadLostItems(); } catch (error) { showToast("Gagal menghapus laporan: " + error.message, "error"); } finally { setLoading(false); } }; // ✅ ADD: Handle Delete Item const handleDeleteItem = async (itemId) => { try { state.setLoading(true); await ApiUtils.delete(`${CONFIG.API_ENDPOINTS.ITEMS}/${itemId}`); showToast("Barang berhasil dihapus!", "success"); loadData(); } catch (error) { showToast("Gagal menghapus barang: " + error.message, "error"); } finally { state.setLoading(false); } }; const submitReportFound = async (e) => { e.preventDefault(); const formData = new FormData(e.target); try { setLoading(true); showToast("Memproses laporan...", "info"); // Upload photo if exists 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; } // Prepare data const dateFound = formData.get("date_found"); const dateFoundISO = new Date(dateFound + "T00:00:00Z").toISOString(); const reporterUserIdRaw = formData.get("reporter_user_id"); const reporterUserId = reporterUserIdRaw ? parseInt(reporterUserIdRaw) : 0; const itemData = { 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"), reporter_user_id: reporterUserId, manager_notes: formData.get("manager_notes") || "", }; await ApiUtils.post(CONFIG.API_ENDPOINTS.ITEMS, itemData); showToast("Laporan berhasil disimpan!", "success"); setShowReportFoundModal(false); // ✅ Now this works setPhotoPreview(null); // ✅ Now this works loadData(); } catch (error) { showToast("Gagal menyimpan laporan: " + error.message, "error"); } finally { setLoading(false); } }; const handleViewLostItemDetail = (item) => { state.setSelectedLostDetail(item); state.setShowLostDetailModal(true); }; const handleManualClaimClick = (item) => { state.setSelectedItemForClaim(item); state.setShowManualClaimModal(true); }; const submitManualClaim = async (formData) => { try { state.setLoading(true); showToast("Memproses klaim manual...", "info"); const payload = { item_id: parseInt(formData.item_id), user_id: formData.user_id ? parseInt(formData.user_id) : null, description: formData.description, contact: formData.contact, claimer_name: formData.claimer_name, manager_notes: formData.manager_notes || "", }; await ApiUtils.post(CONFIG.API_ENDPOINTS.CLAIMS, payload); showToast("Klaim manual berhasil dicatat!", "success"); state.setShowManualClaimModal(false); state.setSelectedItemForClaim(null); loadData(); loadClaims(); } catch (error) { showToast("Gagal mencatat klaim: " + error.message, "error"); } finally { state.setLoading(false); } }; const loadClaims = async () => { try { const response = await ApiUtils.get(CONFIG.API_ENDPOINTS.CLAIMS); setClaims(response.data || []); } catch (error) { showToast("Gagal memuat klaim", "error"); } }; 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}` ); let itemData = response.data || response; const finalData = { ...itemData, reporter_name: itemData.reporter_name || "Data tidak tersedia", reporter_contact: itemData.reporter_contact || "Data tidak tersedia", description: itemData.description || "Tidak ada deskripsi", secret_details: itemData.secret_details || "Tidak ada deskripsi rahasia", }; setSelectedItem(finalData); setShowDetailModal(true); } catch (error) { showToast("Gagal memuat detail: " + error.message, "error"); } }; const handleVerifyClaim = async (claim) => { try { const detailData = await ApiUtils.get( `${CONFIG.API_ENDPOINTS.CLAIMS}/${claim.id}` ); if (!detailData || !detailData.data) { throw new Error("Invalid claim data"); } setSelectedClaim(detailData.data); setShowVerifyModal(true); } catch (error) { showToast("Gagal memuat klaim: " + error.message, "error"); } }; const handleApproveClaim = () => { // Tutup modal verifikasi detail agar tidak tumpuk (opsional, tergantung preferensi UX) state.setShowVerifyModal(false); // Buka modal konfirmasi approve state.setShowApproveModal(true); }; // 2. Fungsi eksekusi: Melakukan request API (Dipanggil saat tombol "Ya, Approve" di modal diklik) const submitApproveClaim = async (notes) => { // Validasi sederhana if (!state.selectedClaim || !state.selectedClaim.id) { showToast("Data klaim tidak valid!", "error"); return; } try { setLoading(true); // Aktifkan loading state // Request ke API await ApiUtils.post( `${CONFIG.API_ENDPOINTS.CLAIMS}/${state.selectedClaim.id}/verify`, { status: "approved", notes: notes || "", // Menggunakan notes dari input Modal } ); showToast("Klaim berhasil diapprove!", "success"); // Tutup modal dan refresh data state.setShowApproveModal(false); loadClaims(); loadData(); } catch (error) { showToast("Gagal approve klaim: " + error.message, "error"); } finally { setLoading(false); // Matikan loading state } }; const handleRejectClaim = async () => { if (!selectedClaim || !selectedClaim.id) { showToast("Data klaim tidak valid!", "error"); return; } const notes = prompt("Alasan penolakan (wajib):"); if (!notes) { showToast("Alasan penolakan harus diisi!", "error"); return; } try { await ApiUtils.post( `${CONFIG.API_ENDPOINTS.CLAIMS}/${selectedClaim.id}/verify`, { status: "rejected", notes, } ); showToast("Klaim berhasil ditolak!", "success"); setShowVerifyModal(false); loadClaims(); loadData(); } catch (error) { showToast("Gagal reject klaim: " + error.message, "error"); } }; const handleCloseCase = async (claim) => { setSelectedClaim(claim); setShowCloseCaseModal(true); }; const submitCloseCase = async (e) => { e.preventDefault(); if (!closeCaseData.berita_acara_no) { showToast("No. Berita Acara wajib diisi!", "error"); return; } try { setLoading(true); let buktiUrl = closeCaseData.bukti_serah_terima; const buktiFile = document.querySelector('input[name="bukti_file"]') ?.files[0]; if (buktiFile) { showToast("Mengupload bukti...", "info"); const uploadFormData = new FormData(); uploadFormData.append("proof", buktiFile); const uploadResponse = await ApiUtils.uploadFile( "/api/upload/claim-proof", uploadFormData ); buktiUrl = uploadResponse.data.url; } await ApiUtils.post( `${CONFIG.API_ENDPOINTS.CLAIMS}/${selectedClaim.id}/close`, { berita_acara_no: closeCaseData.berita_acara_no, bukti_serah_terima: buktiUrl, notes: closeCaseData.notes, } ); showToast("Case berhasil ditutup!", "success"); setShowCloseCaseModal(false); setCloseCaseData({ berita_acara_no: "", bukti_serah_terima: "", notes: "", }); loadClaims(); loadData(); } catch (error) { showToast("Gagal close case: " + error.message, "error"); } finally { setLoading(false); } }; const handleCancelApproval = async (claimId) => { try { setLoading(true); // Kita panggil endpoint baru (perlu dibuat di backend) // Atau gunakan endpoint verify dengan status khusus jika backend mendukung await ApiUtils.post( `${CONFIG.API_ENDPOINTS.CLAIMS}/${claimId}/cancel-approval`, {} ); showToast( "Status Approved berhasil dibatalkan. Klaim kembali ke Pending.", "success" ); loadClaims(); loadData(); // Refresh stats } catch (error) { showToast("Gagal membatalkan approval: " + error.message, "error"); } finally { setLoading(false); } }; const handleReopenCase = async (claim) => { const reason = prompt("Alasan Reopen Case (WAJIB):"); if (!reason || reason.trim() === "") { showToast("Alasan reopen wajib diisi!", "error"); return; } if ( !confirm( `Yakin reopen case untuk "${claim.item_name}"?\n\nAlasan: ${reason}` ) ) { return; } try { setLoading(true); await ApiUtils.post(`${CONFIG.API_ENDPOINTS.CLAIMS}/${claim.id}/reopen`, { reason, }); showToast("Case berhasil dibuka kembali!", "success"); loadClaims(); loadData(); } catch (error) { showToast("Gagal reopen case: " + error.message, "error"); } finally { setLoading(false); } }; return { showToast, loadData, loadClaims, handleLogout, handleViewDetail, handleVerifyClaim, handleApproveClaim, handleRejectClaim, handleCloseCase, submitCloseCase, handleReopenCase, handlePhotoChange, // ✅ Pastikan ini ada submitReportFound, // ✅ REMOVED: loadAuditLogs (not needed in manager) handleManualClaimClick, submitManualClaim, handleCancelApproval, handleEditItemClick, handleUpdateItem, handleDeleteItem, loadLostItems, handleMatchLostItem, submitMatchLostItem, handleDeleteLostItem, handleViewLostItemDetail, handleApproveClaim, // Fungsi pemicu (dihubungkan ke tombol di VerifyModal) submitApproveClaim, // Fungsi submit (dihubungkan ke form di ApproveClaimModal) }; }; window.useManagerHandlers = useManagerHandlers;