2025-11-23 22:49:46 +07:00

769 lines
24 KiB
JavaScript

// web/js/manager.js
// Dashboard Manager JavaScript - FIXED ENDPOINTS
let allItems = [];
let allClaims = [];
let allLostItems = [];
let allArchive = [];
// Initialize dashboard
window.addEventListener("DOMContentLoaded", async () => {
const user = checkAuth();
if (!user || user.role !== "manager") {
window.location.href = "/login";
return;
}
await loadStats();
await loadItems();
setupSearchAndFilters();
});
// Load statistics - FIXED
async function loadStats() {
try {
const stats = await apiCall("/api/manager/dashboard");
document.getElementById("statTotalItems").textContent =
stats.total_items || 0;
document.getElementById("statPendingClaims").textContent =
stats.pending_claims || 0;
document.getElementById("statVerified").textContent = stats.verified || 0;
document.getElementById("statExpired").textContent = stats.expired || 0;
} catch (error) {
console.error("Error loading stats:", error);
}
}
// Load items - FIXED
async function loadItems() {
setLoading("itemsGrid", true);
try {
const response = await apiCall("/api/items");
allItems = response.data || [];
renderItems(allItems);
} catch (error) {
console.error("Error loading items:", error);
showEmptyState("itemsGrid", "📦", "Gagal memuat data barang");
}
}
// Render items
function renderItems(items) {
const grid = document.getElementById("itemsGrid");
if (!items || items.length === 0) {
showEmptyState("itemsGrid", "📦", "Belum ada barang");
return;
}
grid.innerHTML = items
.map(
(item) => `
<div class="item-card">
<img src="${
item.photo_url || "https://via.placeholder.com/280x200?text=No+Image"
}"
alt="${item.name}"
class="item-image"
onerror="this.src='https://via.placeholder.com/280x200?text=No+Image'">
<div class="item-body">
<h3 class="item-title">${item.name}</h3>
<div class="item-meta">
<span>📍 ${item.location}</span>
<span>📅 ${formatDate(item.date_found)}</span>
<span>${getStatusBadge(item.status)}</span>
</div>
<div class="item-actions">
<button class="btn btn-primary btn-sm" onclick="viewItemDetail(${
item.id
})">Detail</button>
${
item.status !== "case_closed" && item.status !== "expired"
? `<button class="btn btn-warning btn-sm" onclick="editItem(${item.id})">Edit</button>`
: ""
}
${
item.status === "verified"
? `<button class="btn btn-success btn-sm" onclick="closeCase(${item.id})">Close Case</button>`
: ""
}
</div>
</div>
</div>
`
)
.join("");
}
// View item detail - FIXED
async function viewItemDetail(itemId) {
try {
const item = await apiCall(`/api/items/${itemId}`);
const modalContent = document.getElementById("itemDetailContent");
modalContent.innerHTML = `
<img src="${
item.photo_url || "https://via.placeholder.com/600x400?text=No+Image"
}"
alt="${item.name}"
style="width: 100%; max-height: 400px; object-fit: cover; border-radius: 10px; margin-bottom: 20px;"
onerror="this.src='https://via.placeholder.com/600x400?text=No+Image'">
<h3>${item.name}</h3>
<div style="display: grid; gap: 15px; margin-top: 20px;">
<div><strong>Kategori:</strong> ${item.category}</div>
<div><strong>Lokasi Ditemukan:</strong> ${item.location}</div>
<div><strong>Tanggal Ditemukan:</strong> ${formatDate(
item.date_found
)}</div>
<div><strong>Status:</strong> ${getStatusBadge(item.status)}</div>
<div><strong>Pelapor:</strong> ${item.reporter_name}</div>
<div><strong>Kontak:</strong> ${item.reporter_contact}</div>
<div style="background: #f8fafc; padding: 15px; border-radius: 10px;">
<strong>Deskripsi Keunikan (Rahasia):</strong><br>
${item.description}
</div>
</div>
`;
openModal("itemDetailModal");
} catch (error) {
console.error("Error loading item detail:", error);
showAlert("Gagal memuat detail barang", "danger");
}
}
// Edit item - FIXED
async function editItem(itemId) {
try {
const item = await apiCall(`/api/items/${itemId}`);
const form = document.getElementById("editItemForm");
form.elements.item_id.value = item.id;
form.elements.name.value = item.name;
form.elements.category.value = item.category;
form.elements.location.value = item.location;
form.elements.description.value = item.description;
form.elements.reporter_name.value = item.reporter_name;
form.elements.reporter_contact.value = item.reporter_contact;
form.elements.date_found.value = item.date_found.split("T")[0];
openModal("editItemModal");
} catch (error) {
console.error("Error loading item:", error);
showAlert("Gagal memuat data barang", "danger");
}
}
// Submit edit item - FIXED
document
.getElementById("editItemForm")
?.addEventListener("submit", async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const itemId = formData.get("item_id");
formData.delete("item_id");
try {
const submitBtn = e.target.querySelector('button[type="submit"]');
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="loading"></span> Menyimpan...';
await apiUpload(`/api/items/${itemId}`, formData, "PUT");
showAlert("Barang berhasil diupdate!", "success");
closeModal("editItemModal");
await loadItems();
await loadStats();
} catch (error) {
console.error("Error updating item:", error);
showAlert(error.message || "Gagal update barang", "danger");
} finally {
const submitBtn = e.target.querySelector('button[type="submit"]');
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = "Update";
}
}
});
// Close case - FIXED
async function closeCase(itemId) {
if (!confirmAction("Tutup kasus ini? Barang akan dipindahkan ke arsip."))
return;
try {
await apiCall(`/api/items/${itemId}/status`, {
method: "PATCH",
body: JSON.stringify({ status: "case_closed" }),
});
showAlert("Kasus berhasil ditutup!", "success");
await loadItems();
await loadStats();
} catch (error) {
console.error("Error closing case:", error);
showAlert(error.message || "Gagal menutup kasus", "danger");
}
}
// Load claims - FIXED
async function loadClaims() {
setLoading("claimsList", true);
try {
const response = await apiCall("/api/claims");
allClaims = response.data || [];
renderClaims(allClaims);
} catch (error) {
console.error("Error loading claims:", error);
document.getElementById("claimsList").innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">🤝</div>
<p>Gagal memuat data klaim</p>
</div>
`;
}
}
// Render claims
function renderClaims(claims) {
const list = document.getElementById("claimsList");
if (!claims || claims.length === 0) {
list.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">🤝</div>
<p>Belum ada klaim yang masuk</p>
</div>
`;
return;
}
list.innerHTML = claims
.map(
(claim) => `
<div class="claim-card">
<div class="claim-header">
<h3>${claim.item_name}</h3>
${getStatusBadge(claim.status)}
</div>
<div class="claim-info">
<div><strong>Pengklaim:</strong> ${claim.user_name}</div>
<div><strong>Kontak:</strong> ${claim.contact}</div>
<div><strong>Tanggal Klaim:</strong> ${formatDate(
claim.created_at
)}</div>
${
claim.match_percentage
? `
<div><strong>Match:</strong>
<span style="color: ${
claim.match_percentage >= 70 ? "#10b981" : "#f59e0b"
}; font-weight: 600;">
${claim.match_percentage}%
</span>
</div>
`
: ""
}
</div>
<div style="background: #f8fafc; padding: 15px; border-radius: 10px; margin-bottom: 15px;">
<strong>Deskripsi dari Pengklaim:</strong><br>
${claim.description}
</div>
${
claim.status === "pending"
? `
<div class="claim-actions">
<button class="btn btn-primary btn-sm" onclick="verifyClaim(${claim.id})">Verifikasi</button>
<button class="btn btn-success btn-sm" onclick="approveClaim(${claim.id})">Approve</button>
<button class="btn btn-danger btn-sm" onclick="rejectClaim(${claim.id})">Reject</button>
</div>
`
: ""
}
${
claim.status === "rejected" && claim.notes
? `
<div style="color: #ef4444; margin-top: 10px;">
<strong>Alasan:</strong> ${claim.notes}
</div>
`
: ""
}
</div>
`
)
.join("");
}
// Verify claim - FIXED
async function verifyClaim(claimId) {
try {
const claim = await apiCall(`/api/claims/${claimId}`);
const modalContent = document.getElementById("verifyClaimContent");
modalContent.innerHTML = `
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 30px;">
<div>
<h4>Deskripsi Asli Barang</h4>
<div style="background: #f8fafc; padding: 15px; border-radius: 10px; margin-top: 10px;">
${claim.item_description}
</div>
</div>
<div>
<h4>Deskripsi dari Pengklaim</h4>
<div style="background: #fef3c7; padding: 15px; border-radius: 10px; margin-top: 10px;">
${claim.description}
</div>
</div>
</div>
${
claim.proof_url
? `
<div style="margin-top: 20px;">
<h4>Bukti Pendukung</h4>
<img src="${claim.proof_url}" style="width: 100%; max-height: 300px; object-fit: contain; border-radius: 10px; margin-top: 10px;">
</div>
`
: ""
}
${
claim.match_percentage
? `
<div style="margin-top: 20px; padding: 15px; background: ${
claim.match_percentage >= 70 ? "#d1fae5" : "#fef3c7"
}; border-radius: 10px; text-align: center;">
<strong>Similarity Match:</strong>
<span style="font-size: 2rem; font-weight: 700; color: ${
claim.match_percentage >= 70 ? "#10b981" : "#f59e0b"
};">
${claim.match_percentage}%
</span>
</div>
`
: ""
}
<div style="margin-top: 20px;">
<strong>Info Pengklaim:</strong>
<div style="margin-top: 10px;">
<div>Nama: ${claim.user_name}</div>
<div>Kontak: ${claim.contact}</div>
</div>
</div>
<div style="display: flex; gap: 10px; margin-top: 30px;">
<button class="btn btn-success" onclick="approveClaim(${claimId})" style="flex: 1;">✓ Approve Klaim</button>
<button class="btn btn-danger" onclick="rejectClaim(${claimId})" style="flex: 1;">✗ Reject Klaim</button>
</div>
`;
openModal("verifyClaimModal");
} catch (error) {
console.error("Error loading claim:", error);
showAlert("Gagal memuat data klaim", "danger");
}
}
// Approve claim - FIXED
async function approveClaim(claimId) {
const notes = prompt("Catatan (opsional):");
try {
await apiCall(`/api/claims/${claimId}/verify`, {
method: "POST",
body: JSON.stringify({
approved: true,
notes: notes || "",
}),
});
showAlert("Klaim berhasil diapprove!", "success");
closeModal("verifyClaimModal");
await loadClaims();
await loadItems();
await loadStats();
} catch (error) {
console.error("Error approving claim:", error);
showAlert(error.message || "Gagal approve klaim", "danger");
}
}
// Reject claim - FIXED
async function rejectClaim(claimId) {
const notes = prompt("Alasan penolakan (wajib):");
if (!notes) {
showAlert("Alasan penolakan harus diisi!", "warning");
return;
}
try {
await apiCall(`/api/claims/${claimId}/verify`, {
method: "POST",
body: JSON.stringify({
approved: false,
notes,
}),
});
showAlert("Klaim berhasil ditolak!", "success");
closeModal("verifyClaimModal");
await loadClaims();
await loadStats();
} catch (error) {
console.error("Error rejecting claim:", error);
showAlert(error.message || "Gagal reject klaim", "danger");
}
}
// Load lost items - FIXED
async function loadLost() {
setLoading("lostItemsGrid", true);
try {
const response = await apiCall("/api/lost-items");
allLostItems = response.data || [];
renderLostItems(allLostItems);
} catch (error) {
console.error("Error loading lost items:", error);
showEmptyState("lostItemsGrid", "😢", "Gagal memuat data barang hilang");
}
}
// Render lost items
function renderLostItems(items) {
const grid = document.getElementById("lostItemsGrid");
if (!items || items.length === 0) {
showEmptyState("lostItemsGrid", "😢", "Belum ada laporan barang hilang");
return;
}
grid.innerHTML = items
.map(
(item) => `
<div class="item-card">
<div class="item-body">
<h3 class="item-title">${item.name}</h3>
<div class="item-meta">
<span>🏷️ ${item.category}</span>
<span>🎨 ${item.color}</span>
<span>📅 ${formatDate(item.date_lost)}</span>
${item.location ? `<span>📍 ${item.location}</span>` : ""}
</div>
<p style="color: #64748b; font-size: 0.9rem; margin-top: 10px;">${
item.description
}</p>
<div style="margin-top: 10px;">
<small><strong>Pelapor:</strong> ${item.user_name}</small>
</div>
<button class="btn btn-primary btn-sm" onclick="findSimilarItems(${
item.id
})" style="margin-top: 10px; width: 100%;">
🔍 Cari Barang yang Mirip
</button>
</div>
</div>
`
)
.join("");
}
// Find similar items - FIXED
async function findSimilarItems(lostItemId) {
try {
setLoading("matchItemsContent", true);
openModal("matchItemsModal");
const response = await apiCall(`/api/lost-items/${lostItemId}/matches`);
const matches = response.data || [];
const modalContent = document.getElementById("matchItemsContent");
if (matches.length === 0) {
modalContent.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">🔍</div>
<h3>Tidak ada barang yang cocok</h3>
<p>Belum ada barang ditemukan yang mirip dengan laporan ini</p>
</div>
`;
return;
}
modalContent.innerHTML = `
<p style="margin-bottom: 20px; color: #64748b;">Ditemukan ${
matches.length
} barang yang mungkin cocok:</p>
<div class="items-grid">
${matches
.map(
(match) => `
<div class="item-card" style="border: 2px solid ${
match.similarity >= 70 ? "#10b981" : "#f59e0b"
};">
<img src="${
match.photo_url ||
"https://via.placeholder.com/280x200?text=No+Image"
}"
alt="${match.name}"
class="item-image"
onerror="this.src='https://via.placeholder.com/280x200?text=No+Image'">
<div class="item-body">
<div style="text-align: center; margin-bottom: 10px;">
<span style="font-size: 1.5rem; font-weight: 700; color: ${
match.similarity >= 70 ? "#10b981" : "#f59e0b"
};">
${match.similarity}% Match
</span>
</div>
<h3 class="item-title">${match.name}</h3>
<div class="item-meta">
<span>📍 ${match.location}</span>
<span>📅 ${formatDate(match.date_found)}</span>
<span>${getStatusBadge(match.status)}</span>
</div>
<button class="btn btn-primary btn-sm" onclick="viewItemDetail(${
match.id
})" style="width: 100%; margin-top: 10px;">
Lihat Detail
</button>
</div>
</div>
`
)
.join("")}
</div>
`;
} catch (error) {
console.error("Error finding similar items:", error);
document.getElementById("matchItemsContent").innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">❌</div>
<p>Gagal mencari barang yang mirip</p>
</div>
`;
}
}
// Load archive - FIXED
async function loadArchive() {
setLoading("archiveGrid", true);
try {
const response = await apiCall("/api/archives");
allArchive = response.data || [];
renderArchive(allArchive);
} catch (error) {
console.error("Error loading archive:", error);
showEmptyState("archiveGrid", "📂", "Gagal memuat data arsip");
}
}
// Render archive
function renderArchive(items) {
const grid = document.getElementById("archiveGrid");
if (!items || items.length === 0) {
showEmptyState("archiveGrid", "📂", "Belum ada barang di arsip");
return;
}
grid.innerHTML = items
.map(
(item) => `
<div class="item-card">
<img src="${
item.photo_url || "https://via.placeholder.com/280x200?text=No+Image"
}"
alt="${item.name}"
class="item-image"
onerror="this.src='https://via.placeholder.com/280x200?text=No+Image'">
<div class="item-body">
<h3 class="item-title">${item.name}</h3>
<div class="item-meta">
<span>📍 ${item.location}</span>
<span>📅 ${formatDate(item.date_found)}</span>
<span>${getStatusBadge(item.status)}</span>
</div>
</div>
</div>
`
)
.join("");
}
// Report found item - FIXED
function openReportFoundModal() {
openModal("reportFoundModal");
}
// Submit found item report - FIXED
document
.getElementById("reportFoundForm")
?.addEventListener("submit", async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
const submitBtn = e.target.querySelector('button[type="submit"]');
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="loading"></span> Mengirim...';
await apiUpload("/api/items", formData);
showAlert("Barang berhasil ditambahkan!", "success");
closeModal("reportFoundModal");
e.target.reset();
await loadItems();
await loadStats();
} catch (error) {
console.error("Error submitting item:", error);
showAlert(error.message || "Gagal menambahkan barang", "danger");
} finally {
const submitBtn = e.target.querySelector('button[type="submit"]');
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = "Submit";
}
}
});
// Setup search and filters
function setupSearchAndFilters() {
// Items tab
const searchItems = document.getElementById("searchItems");
const categoryFilterItems = document.getElementById("categoryFilterItems");
const statusFilterItems = document.getElementById("statusFilterItems");
const sortItems = document.getElementById("sortItems");
const performItemsSearch = debounce(() => {
const searchTerm = searchItems?.value.toLowerCase() || "";
const category = categoryFilterItems?.value || "";
const status = statusFilterItems?.value || "";
const sort = sortItems?.value || "date_desc";
let filtered = allItems.filter((item) => {
const matchesSearch =
item.name.toLowerCase().includes(searchTerm) ||
item.location.toLowerCase().includes(searchTerm);
const matchesCategory = !category || item.category === category;
const matchesStatus = !status || item.status === status;
return matchesSearch && matchesCategory && matchesStatus;
});
// Sort
filtered.sort((a, b) => {
switch (sort) {
case "date_desc":
return new Date(b.date_found) - new Date(a.date_found);
case "date_asc":
return new Date(a.date_found) - new Date(b.date_found);
case "name_asc":
return a.name.localeCompare(b.name);
case "name_desc":
return b.name.localeCompare(a.name);
default:
return 0;
}
});
renderItems(filtered);
}, 300);
searchItems?.addEventListener("input", performItemsSearch);
categoryFilterItems?.addEventListener("change", performItemsSearch);
statusFilterItems?.addEventListener("change", performItemsSearch);
sortItems?.addEventListener("change", performItemsSearch);
// Claims tab
const searchClaims = document.getElementById("searchClaims");
const statusFilterClaims = document.getElementById("statusFilterClaims");
const performClaimsSearch = debounce(() => {
const searchTerm = searchClaims?.value.toLowerCase() || "";
const status = statusFilterClaims?.value || "";
let filtered = allClaims.filter((claim) => {
const matchesSearch =
claim.item_name.toLowerCase().includes(searchTerm) ||
claim.user_name.toLowerCase().includes(searchTerm);
const matchesStatus = !status || claim.status === status;
return matchesSearch && matchesStatus;
});
renderClaims(filtered);
}, 300);
searchClaims?.addEventListener("input", performClaimsSearch);
statusFilterClaims?.addEventListener("change", performClaimsSearch);
}
// Create edit item modal if not exists
if (!document.getElementById("editItemModal")) {
const editItemModal = document.createElement("div");
editItemModal.id = "editItemModal";
editItemModal.className = "modal";
editItemModal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Edit Barang</h3>
<button class="close-btn" onclick="closeModal('editItemModal')">&times;</button>
</div>
<div class="modal-body">
<form id="editItemForm">
<input type="hidden" name="item_id">
<div class="form-group">
<label>Foto Barang (Opsional - Kosongkan jika tidak ingin mengubah)</label>
<input type="file" name="photo" accept="image/*">
</div>
<div class="form-group">
<label>Nama Barang *</label>
<input type="text" name="name" required>
</div>
<div class="form-group">
<label>Kategori *</label>
<select name="category" required>
<option value="">Pilih Kategori</option>
<option value="pakaian">Pakaian</option>
<option value="alat_makan">Alat Makan</option>
<option value="aksesoris">Aksesoris</option>
<option value="elektronik">Elektronik</option>
<option value="alat_tulis">Alat Tulis</option>
<option value="lainnya">Lainnya</option>
</select>
</div>
<div class="form-group">
<label>Lokasi Ditemukan *</label>
<input type="text" name="location" required>
</div>
<div class="form-group">
<label>Deskripsi Keunikan *</label>
<textarea name="description" required></textarea>
</div>
<div class="form-group">
<label>Nama Pelapor *</label>
<input type="text" name="reporter_name" required>
</div>
<div class="form-group">
<label>Kontak Pelapor *</label>
<input type="text" name="reporter_contact" required>
</div>
<div class="form-group">
<label>Tanggal Ditemukan *</label>
<input type="date" name="date_found" required>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%;">Update</button>
</form>
</div>
</div>
`;
document.body.appendChild(editItemModal);
}