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

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;