677 lines
20 KiB
JavaScript
677 lines
20 KiB
JavaScript
// 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; |