Memuat Data...

Mohon tunggu sejenak

Sodaqoh Hub

"Sebaik-baiknya kalian adalah yang bermanfaat untuk orang lain."

Memuat Data...

Mohon tunggu sejenak

Sodaqoh Hub

"Sebaik-baiknya kalian adalah yang bermanfaat untuk orang lain."

"; let PREVIEW_MODE = false; // Global State let currentUser = null; let dbJamaah = []; let dbTransaksi = []; let dbUsers = []; // Chart References let chartUangInstance = null; let chartBerasInstance = null; // Register Chart.js DataLabels plugin globally if (typeof ChartDataLabels !== 'undefined') { Chart.register(ChartDataLabels); } const NAMA_BULAN = [ 'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember' ]; // Inisialisasi awal saat halaman dimuat window.onload = function() { setLocalDateAsDefault('sodaqoh-tanggal'); if (typeof google !== 'undefined' && google.script && google.script.run) { PREVIEW_MODE = false; } }; function setLocalDateAsDefault(elementId) { const el = document.getElementById(elementId); if(el) { const today = new Date(); const offset = today.getTimezoneOffset() * 60000; const localISOTime = (new Date(today - offset)).toISOString().split('T')[0]; el.value = localISOTime; } } /** * PENTING: Fetch with Retry Mechanism untuk koneksi yang lebih stabil * Menggunakan Exponential Backoff agar tidak kena limit rate API Google */ async function fetchWithRetry(url, options, retries = 3, backoff = 500) { for (let i = 0; i < retries; i++) { try { const response = await fetch(url, options); if (!response.ok) throw new Error('HTTP error ' + response.status); return await response.json(); } catch (error) { if (i === retries - 1) throw error; // Jeda sebelum retry berikutnya: 500ms, 1000ms, dst await new Promise(res => setTimeout(res, backoff * Math.pow(2, i))); } } } /** * Helper Enkripsi Client-side SHA-256 */ async function hashPassword(str) { if (!str) return ''; const encoder = new TextEncoder(); const data = encoder.encode(str); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } function showLoading(title = "Memuat Data...", sub = "Mohon tunggu sejenak") { document.getElementById('loading-title').innerText = title; document.getElementById('loading-sub').innerText = sub; const overlay = document.getElementById('loading-overlay'); overlay.classList.remove('pointer-events-none', 'opacity-0'); overlay.classList.add('opacity-100'); } function hideLoading() { const overlay = document.getElementById('loading-overlay'); overlay.classList.remove('opacity-100'); overlay.classList.add('opacity-0', 'pointer-events-none'); } /** * Proses Login */ async function handleLogin(e) { e.preventDefault(); const u = document.getElementById('login-username').value.trim(); const p = document.getElementById('login-password').value.trim(); if (!u || !p) { Swal.fire({ icon: 'warning', title: 'Data Belum Lengkap', text: 'Username dan password wajib diisi!' }); return; } showLoading("Mengautentikasi...", "Memeriksa kredensial ke server"); const hashedPassword = await hashPassword(p); fetchWithRetry(APP_URL, { method: 'POST', body: JSON.stringify({ action: 'login', username: u, passwordHash: hashedPassword, rawPassword: p }) }) .then(data => { hideLoading(); if (data.status === 'success') { currentUser = data.user; completeLogin(); fetchInitialData(); } else { Swal.fire({ icon: 'error', title: 'Login Gagal', text: data.message }); } }) .catch(err => { hideLoading(); Swal.fire({ icon: 'error', title: 'Gagal Terhubung', text: 'Koneksi ke Apps Script terputus. Pastikan link URL sudah benar atau coba lagi.' }); }); } function completeLogin() { document.getElementById('view-login').classList.add('hidden'); document.getElementById('app-wrapper').classList.remove('hidden'); if (currentUser) { document.getElementById('user-display-name').innerText = currentUser.nama || currentUser.username; document.getElementById('user-display-role').innerText = currentUser.role || 'Petugas'; document.getElementById('user-avatar').innerText = (currentUser.nama || currentUser.username).substring(0, 2).toUpperCase(); // Sembunyikan/Tampilkan tab Kelola User sesuai role const navUsersBtn = document.getElementById('nav-users'); if (navUsersBtn) { const isAdmin = (currentUser.role || '').toLowerCase() === 'admin'; if (isAdmin) { navUsersBtn.classList.remove('hidden'); } else { navUsersBtn.classList.add('hidden'); } } } switchTab('dashboard'); } function handleLogout() { Swal.fire({ title: 'Konfirmasi Keluar', text: 'Apakah Anda yakin ingin keluar dari aplikasi?', icon: 'question', showCancelButton: true, confirmButtonText: 'Ya, Keluar', cancelButtonText: 'Batal' }).then((res) => { if (res.isConfirmed) { currentUser = null; document.getElementById('app-wrapper').classList.add('hidden'); document.getElementById('view-login').classList.remove('hidden'); document.getElementById('form-login').reset(); } }); } function toggleSidebar() { const sidebar = document.getElementById('sidebar'); const backdrop = document.getElementById('sidebar-backdrop'); sidebar.classList.toggle('-translate-x-full'); backdrop.classList.toggle('hidden'); } function switchTab(tabId) { // Validasi hak akses: hanya Admin yang dapat membuka tab Kelola User if (tabId === 'users' && (!currentUser || (currentUser.role || '').toLowerCase() !== 'admin')) { Swal.fire({ icon: 'warning', title: 'Akses Ditolak', text: 'Hanya pengguna dengan role Admin yang memiliki akses ke menu Kelola User!' }); return; } document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden')); document.querySelectorAll('.nav-btn').forEach(el => { el.classList.remove('bg-emerald-600', 'text-white'); el.classList.add('text-slate-300'); }); const targetTab = document.getElementById('tab-' + tabId); const targetNav = document.getElementById('nav-' + tabId); if (targetTab) targetTab.classList.remove('hidden'); if (targetNav) { targetNav.classList.add('bg-emerald-600', 'text-white'); targetNav.classList.remove('text-slate-300'); } // Mobile sidebar auto hide const sidebar = document.getElementById('sidebar'); if (!sidebar.classList.contains('-translate-x-full') && window.innerWidth < 768) { toggleSidebar(); } // Render spesifik per tab if (tabId === 'dashboard') { renderDashboardStats(); renderRecentTable(); } else if (tabId === 'input-sodaqoh') { populateJamaahDropdown(); } else if (tabId === 'jamaah') { renderJamaahTable(); } else if (tabId === 'riwayat') { renderRiwayatTable(); } else if (tabId === 'rekap-tren') { renderCharts(); } else if (tabId === 'laporan') { renderLaporanTable(); } else if (tabId === 'users') { renderUsersTable(); } } /** * Mengambil Data Terbaru dari Server Google Apps Script */ function fetchInitialData() { showLoading("Memuat Database...", "Mengambil data Jamaah, Transaksi & Users"); fetchWithRetry(APP_URL, { method: 'POST', body: JSON.stringify({ action: 'getInitialData' }) }) .then(data => { hideLoading(); if (data.status === 'success') { dbJamaah = data.jamaah || []; dbTransaksi = data.transaksi || []; dbUsers = data.users || []; renderDashboardStats(); renderRecentTable(); populateJamaahDropdown(); } else { Swal.fire({ icon: 'error', title: 'Gagal Memuat Data', text: data.message }); } }) .catch(err => { hideLoading(); Swal.fire({ icon: 'error', title: 'Error Server', text: 'Gagal mengunduh data dari Apps Script. Coba lagi.' }); }); } function formatRupiah(number) { return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(number); } function renderDashboardStats() { const now = new Date(); const currentMonth = now.getMonth(); const currentYear = now.getFullYear(); // Filter transaksi hanya untuk bulan dan tahun berjalan const currentMonthTrans = dbTransaksi.filter(t => { if (!t.tanggal) return false; const d = new Date(t.tanggal); return d.getMonth() === currentMonth && d.getFullYear() === currentYear; }); const totalBeras = currentMonthTrans.reduce((acc, curr) => acc + (parseFloat(curr.beras) || 0), 0); const totalUang = currentMonthTrans.reduce((acc, curr) => acc + (parseFloat(curr.uang) || 0), 0); // Hitung partisipasi unik jamaah bulan ini const uniqueJamaahSodaqoh = new Set(currentMonthTrans.map(t => t.jamaahId)).size; const totalJamaah = dbJamaah.length; const partisipasiPct = totalJamaah > 0 ? Math.round((uniqueJamaahSodaqoh / totalJamaah) * 100) : 0; const labelBulanTahun = `${NAMA_BULAN[currentMonth]} ${currentYear}`; document.getElementById('stat-total-beras').innerText = totalBeras.toFixed(1).replace(/\.0$/, '') + " Kg"; document.getElementById('stat-total-uang').innerText = formatRupiah(totalUang); document.getElementById('stat-partisipasi').innerText = partisipasiPct + "%"; document.getElementById('stat-partisipasi-detail').innerText = `${uniqueJamaahSodaqoh} dari ${totalJamaah} Jamaah`; document.getElementById('stat-beras-subtitle').innerText = `Terkumpul ${labelBulanTahun}`; document.getElementById('stat-uang-subtitle').innerText = `Terkumpul ${labelBulanTahun}`; } function renderRecentTable() { const tbody = document.getElementById('table-recent-tbody'); tbody.innerHTML = ''; const sorted = [...dbTransaksi].reverse().slice(0, 5); if (sorted.length === 0) { tbody.innerHTML = `Belum ada transaksi recorded.`; return; } sorted.forEach(t => { tbody.innerHTML += ` ${t.tanggal} ${t.namaJamaah} ${t.beras > 0 ? t.beras + ' Kg' : '-'} ${t.uang > 0 ? formatRupiah(t.uang) : '-'} `; }); } function populateJamaahDropdown() { const select = document.getElementById('sodaqoh-jamaah-select'); select.innerHTML = ''; dbJamaah.forEach(j => { select.innerHTML += ``; }); } function addBeras(val) { const input = document.getElementById('sodaqoh-beras'); const current = parseFloat(input.value) || 0; input.value = (current + val).toFixed(1).replace(/\.0$/, ''); } function addUang(val) { const input = document.getElementById('sodaqoh-uang'); const current = parseInt(input.value) || 0; input.value = current + val; } function resetSodaqohForm() { document.getElementById('form-sodaqoh').reset(); setLocalDateAsDefault('sodaqoh-tanggal'); } function handleSaveSodaqoh(e) { e.preventDefault(); const jamaahId = document.getElementById('sodaqoh-jamaah-select').value; const beras = parseFloat(document.getElementById('sodaqoh-beras').value) || 0; const uang = parseFloat(document.getElementById('sodaqoh-uang').value) || 0; const tanggal = document.getElementById('sodaqoh-tanggal').value; const catatan = document.getElementById('sodaqoh-catatan').value.trim(); if (!jamaahId) { Swal.fire({ icon: 'warning', title: 'Jamaah Belum Dipilih', text: 'Silakan pilih jamaah terlebih dahulu.' }); return; } if (beras <= 0 && uang <= 0) { Swal.fire({ icon: 'warning', title: 'Jumlah Kosong', text: 'Masukkan jumlah sodaqoh beras atau uang PPG!' }); return; } const jamaahObj = dbJamaah.find(j => j.id === jamaahId); const namaJamaah = jamaahObj ? jamaahObj.nama : 'Jamaah'; const petugas = currentUser ? (currentUser.nama || currentUser.username) : 'System'; const payload = { jamaahId, namaJamaah, beras, uang, tanggal, catatan, petugas }; showLoading("Menyimpan Transaksi...", "Memproses sodaqoh jamaah"); fetchWithRetry(APP_URL, { method: 'POST', body: JSON.stringify({ action: 'addSodaqoh', data: payload }) }) .then(data => { hideLoading(); if (data.status === 'success') { dbTransaksi.push({ id: data.id, ...payload }); resetSodaqohForm(); Swal.fire({ icon: 'success', title: 'Berhasil Disimpan', text: `Sodaqoh ${namaJamaah} telah dicatat!` }); renderDashboardStats(); } else { Swal.fire({ icon: 'error', title: 'Gagal Menyimpan', text: data.message }); } }) .catch(err => { hideLoading(); Swal.fire({ icon: 'error', title: 'Error Jaringan', text: 'Gagal mengirim data transaksi. Coba lagi.' }); }); } function renderJamaahTable() { const tbody = document.getElementById('table-jamaah-tbody'); const search = document.getElementById('search-jamaah').value.toLowerCase(); tbody.innerHTML = ''; const filtered = dbJamaah.filter(j => j.nama.toLowerCase().includes(search) || j.kelompok.toLowerCase().includes(search)); if (filtered.length === 0) { tbody.innerHTML = `Data jamaah tidak ditemukan.`; return; } filtered.forEach(j => { tbody.innerHTML += ` ${j.nama} ${j.kelompok || '-'} ${j.hp || '-'} ${j.alamat || '-'} `; }); } function openModalJamaah() { document.getElementById('form-jamaah-modal').reset(); document.getElementById('jamaah-id').value = ''; document.getElementById('modal-jamaah-title').innerText = 'Tambah Jamaah Baru'; document.getElementById('modal-jamaah').classList.remove('hidden'); } function closeModalJamaah() { document.getElementById('modal-jamaah').classList.add('hidden'); } function editJamaah(id) { const j = dbJamaah.find(item => item.id === id); if (!j) return; document.getElementById('jamaah-id').value = j.id; document.getElementById('jamaah-nama').value = j.nama; document.getElementById('jamaah-kelompok').value = j.kelompok; document.getElementById('jamaah-hp').value = j.hp; document.getElementById('jamaah-alamat').value = j.alamat; document.getElementById('modal-jamaah-title').innerText = 'Edit Data Jamaah'; document.getElementById('modal-jamaah').classList.remove('hidden'); } function handleSaveJamaah(e) { e.preventDefault(); const id = document.getElementById('jamaah-id').value; const nama = document.getElementById('jamaah-nama').value.trim(); const kelompok = document.getElementById('jamaah-kelompok').value.trim(); const hp = document.getElementById('jamaah-hp').value.trim(); const alamat = document.getElementById('jamaah-alamat').value.trim(); const payload = { id, nama, kelompok, hp, alamat }; const isUpdate = !!id; showLoading("Menyimpan Jamaah...", "Mengirim data ke database"); fetchWithRetry(APP_URL, { method: 'POST', body: JSON.stringify({ action: isUpdate ? 'updateJamaah' : 'addJamaah', data: payload }) }) .then(data => { hideLoading(); if (data.status === 'success') { if (isUpdate) { const idx = dbJamaah.findIndex(j => j.id === id); if (idx !== -1) dbJamaah[idx] = payload; } else { dbJamaah.push({ id: data.id, ...payload }); } closeModalJamaah(); renderJamaahTable(); populateJamaahDropdown(); Swal.fire({ icon: 'success', title: 'Berhasil', text: data.message }); } else { Swal.fire({ icon: 'error', title: 'Gagal', text: data.message }); } }) .catch(err => { hideLoading(); Swal.fire({ icon: 'error', title: 'Error Server', text: 'Gagal menyimpan jamaah.' }); }); } function deleteJamaah(id, nama) { Swal.fire({ title: 'Hapus Jamaah?', text: `Apakah Anda yakin ingin menghapus data "${nama}"?`, icon: 'warning', showCancelButton: true, confirmButtonColor: '#ef4444', confirmButtonText: 'Ya, Hapus' }).then((result) => { if (result.isConfirmed) { showLoading("Menghapus...", "Menghapus data jamaah"); fetchWithRetry(APP_URL, { method: 'POST', body: JSON.stringify({ action: 'deleteJamaah', id }) }) .then(data => { hideLoading(); if (data.status === 'success') { dbJamaah = dbJamaah.filter(j => j.id !== id); renderJamaahTable(); populateJamaahDropdown(); Swal.fire({ icon: 'success', title: 'Terhapus', text: data.message }); } else { Swal.fire({ icon: 'error', title: 'Gagal', text: data.message }); } }) .catch(err => { hideLoading(); Swal.fire({ icon: 'error', title: 'Gagal', text: 'Kesalahan jaringan saat menghapus.' }); }); } }); } function renderRiwayatTable() { const tbody = document.getElementById('table-riwayat-tbody'); const search = document.getElementById('search-riwayat').value.toLowerCase(); tbody.innerHTML = ''; const filtered = dbTransaksi.filter(t => (t.namaJamaah || '').toLowerCase().includes(search) || (t.catatan || '').toLowerCase().includes(search) || (t.petugas || '').toLowerCase().includes(search) ); if (filtered.length === 0) { tbody.innerHTML = `Tidak ada transaksi ditemukan.`; return; } [...filtered].reverse().forEach(t => { tbody.innerHTML += ` ${t.tanggal} ${t.namaJamaah} ${t.beras > 0 ? t.beras + ' Kg' : '-'} ${t.uang > 0 ? formatRupiah(t.uang) : '-'} ${t.catatan || '-'} ${t.petugas || '-'} `; }); } function openModalEditSodaqoh(id) { const t = dbTransaksi.find(item => item.id === id); if (!t) return; const select = document.getElementById('edit-sodaqoh-jamaah'); select.innerHTML = ''; dbJamaah.forEach(j => { select.innerHTML += ``; }); document.getElementById('edit-sodaqoh-id').value = t.id; document.getElementById('edit-sodaqoh-jamaah').value = t.jamaahId; document.getElementById('edit-sodaqoh-beras').value = t.beras || 0; document.getElementById('edit-sodaqoh-uang').value = t.uang || 0; document.getElementById('edit-sodaqoh-tanggal').value = t.tanggal; document.getElementById('edit-sodaqoh-catatan').value = t.catatan || ''; document.getElementById('modal-edit-sodaqoh').classList.remove('hidden'); } function closeModalEditSodaqoh() { document.getElementById('modal-edit-sodaqoh').classList.add('hidden'); } function handleUpdateSodaqoh(e) { e.preventDefault(); const id = document.getElementById('edit-sodaqoh-id').value; const jamaahId = document.getElementById('edit-sodaqoh-jamaah').value; const beras = parseFloat(document.getElementById('edit-sodaqoh-beras').value) || 0; const uang = parseFloat(document.getElementById('edit-sodaqoh-uang').value) || 0; const tanggal = document.getElementById('edit-sodaqoh-tanggal').value; const catatan = document.getElementById('edit-sodaqoh-catatan').value.trim(); if (!jamaahId) { Swal.fire({ icon: 'warning', title: 'Jamaah Belum Dipilih', text: 'Silakan pilih jamaah.' }); return; } if (beras <= 0 && uang <= 0) { Swal.fire({ icon: 'warning', title: 'Jumlah Kosong', text: 'Masukkan jumlah sodaqoh beras atau uang PPG!' }); return; } const jamaahObj = dbJamaah.find(j => j.id === jamaahId); const namaJamaah = jamaahObj ? jamaahObj.nama : 'Jamaah'; const petugas = currentUser ? (currentUser.nama || currentUser.username) : 'System'; const payload = { id, jamaahId, namaJamaah, beras, uang, tanggal, catatan, petugas }; showLoading("Memperbarui Transaksi...", "Menyimpan perubahan ke database"); fetchWithRetry(APP_URL, { method: 'POST', body: JSON.stringify({ action: 'updateSodaqoh', data: payload }) }) .then(data => { hideLoading(); if (data.status === 'success') { const idx = dbTransaksi.findIndex(t => t.id === id); if (idx !== -1) dbTransaksi[idx] = payload; closeModalEditSodaqoh(); renderRiwayatTable(); renderDashboardStats(); Swal.fire({ icon: 'success', title: 'Berhasil Diperbarui', text: data.message }); } else { Swal.fire({ icon: 'error', title: 'Gagal', text: data.message }); } }) .catch(err => { hideLoading(); Swal.fire({ icon: 'error', title: 'Error Server', text: 'Gagal memperbarui transaksi.' }); }); } function deleteTransaksi(id) { Swal.fire({ title: 'Hapus Transaksi?', text: 'Transaksi ini akan dihapus permanen dari database.', icon: 'warning', showCancelButton: true, confirmButtonColor: '#ef4444', confirmButtonText: 'Ya, Hapus' }).then((res) => { if (res.isConfirmed) { showLoading("Menghapus...", "Menghapus catatan transaksi"); fetchWithRetry(APP_URL, { method: 'POST', body: JSON.stringify({ action: 'deleteSodaqoh', id }) }) .then(data => { hideLoading(); if (data.status === 'success') { dbTransaksi = dbTransaksi.filter(t => t.id !== id); renderRiwayatTable(); renderDashboardStats(); Swal.fire({ icon: 'success', title: 'Dihapus', text: data.message }); } }); } }); } function renderCharts() { const months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agt', 'Sep', 'Okt', 'Nov', 'Des']; const monthlyUang = new Array(12).fill(0); const monthlyBeras = new Array(12).fill(0); dbTransaksi.forEach(t => { if (t.tanggal) { const monthIdx = new Date(t.tanggal).getMonth(); if (!isNaN(monthIdx)) { monthlyUang[monthIdx] += (parseFloat(t.uang) || 0); monthlyBeras[monthIdx] += (parseFloat(t.beras) || 0); } } }); // Destroy existing charts if re-rendered if (chartUangInstance) chartUangInstance.destroy(); if (chartBerasInstance) chartBerasInstance.destroy(); const ctxUang = document.getElementById('chart-uang').getContext('2d'); chartUangInstance = new Chart(ctxUang, { type: 'line', data: { labels: months, datasets: [{ label: 'Total Uang PPG (Rp)', data: monthlyUang, borderColor: '#f59e0b', backgroundColor: 'rgba(245, 158, 11, 0.1)', fill: true, tension: 0.3, pointRadius: 5, pointHoverRadius: 7 }] }, options: { responsive: true, maintainAspectRatio: false, layout: { padding: { top: 25 } }, plugins: { datalabels: { display: function(context) { return context.dataset.data[context.dataIndex] > 0; }, align: 'top', anchor: 'end', formatter: function(val) { return val >= 1000 ? (val / 1000).toLocaleString('id-ID') + 'rb' : val; }, font: { weight: 'bold', size: 10 }, color: '#b45309' } } } }); const ctxBeras = document.getElementById('chart-beras').getContext('2d'); chartBerasInstance = new Chart(ctxBeras, { type: 'bar', data: { labels: months, datasets: [{ label: 'Total Beras (Kg)', data: monthlyBeras, backgroundColor: '#10b981', borderRadius: 8 }] }, options: { responsive: true, maintainAspectRatio: false, layout: { padding: { top: 25 } }, plugins: { datalabels: { display: function(context) { return context.dataset.data[context.dataIndex] > 0; }, align: 'top', anchor: 'end', formatter: function(val) { return val + ' Kg'; }, font: { weight: 'bold', size: 10 }, color: '#047857' } } } }); } function renderLaporanTable() { const statusFilter = document.getElementById('filter-laporan-status').value; const bulanFilter = document.getElementById('filter-laporan-bulan').value; const tahunFilter = parseInt(document.getElementById('filter-laporan-tahun').value) || 2026; const tbody = document.getElementById('table-laporan-tbody'); tbody.innerHTML = ''; // Update keterangan subtitle periode const textBulan = bulanFilter === 'semua' ? 'Semua Bulan' : NAMA_BULAN[parseInt(bulanFilter) - 1]; const textStatus = statusFilter === 'semua' ? 'Semua Jamaah' : (statusFilter === 'sudah' ? 'Jamaah SUDAH Sodaqoh' : 'Jamaah BELUM Sodaqoh'); document.getElementById('laporan-subtitle-info').innerText = `Periode: ${textBulan} ${tahunFilter} | Filter: ${textStatus}`; let filteredTrans = dbTransaksi.filter(t => { if (!t.tanggal) return false; const d = new Date(t.tanggal); const matchYear = d.getFullYear() === tahunFilter; const matchMonth = bulanFilter === 'semua' ? true : (d.getMonth() + 1) === parseInt(bulanFilter); return matchYear && matchMonth; }); let reportData = dbJamaah.map((j, idx) => { const jamaahTrans = filteredTrans.filter(t => t.jamaahId === j.id); const totalBeras = jamaahTrans.reduce((a, b) => a + (parseFloat(b.beras) || 0), 0); const totalUang = jamaahTrans.reduce((a, b) => a + (parseFloat(b.uang) || 0), 0); const isSudah = jamaahTrans.length > 0; return { no: idx + 1, nama: j.nama, kelompok: j.kelompok, status: isSudah ? 'SUDAH' : 'BELUM', beras: totalBeras, uang: totalUang }; }); if (statusFilter === 'sudah') reportData = reportData.filter(r => r.status === 'SUDAH'); if (statusFilter === 'belum') reportData = reportData.filter(r => r.status === 'BELUM'); // Hitung Total Akumulasi Sesuai Filter const grandTotalBeras = reportData.reduce((acc, r) => acc + r.beras, 0); const grandTotalUang = reportData.reduce((acc, r) => acc + r.uang, 0); const totalJamaahFilter = reportData.length; // Render ke ringkasan (Summary Cards) document.getElementById('laporan-summary-jamaah').innerText = `${totalJamaahFilter} Jamaah`; document.getElementById('laporan-summary-beras').innerText = `${grandTotalBeras.toFixed(1).replace(/\.0$/, '')} Kg`; document.getElementById('laporan-summary-uang').innerText = formatRupiah(grandTotalUang); if (reportData.length === 0) { tbody.innerHTML = `Tidak ada data untuk laporan ini.`; return; } reportData.forEach((r, index) => { const badge = r.status === 'SUDAH' ? `SUDAH` : `BELUM`; tbody.innerHTML += ` ${index + 1} ${r.nama} ${r.kelompok || '-'} ${badge} ${r.beras > 0 ? r.beras + ' Kg' : '-'} ${r.uang > 0 ? formatRupiah(r.uang) : '-'} `; }); } function exportExcelLaporan() { const table = document.getElementById('printable-area'); const wb = XLSX.utils.table_to_book(table, { sheet: "Laporan Partisipasi" }); XLSX.writeFile(wb, "Laporan_Sodaqoh_Jamaah.xlsx"); } function renderUsersTable() { const tbody = document.getElementById('table-users-tbody'); tbody.innerHTML = ''; dbUsers.forEach(u => { tbody.innerHTML += ` ${u.username} ${u.nama} ${u.role} `; }); } function openModalUser() { document.getElementById('form-user-modal').reset(); document.getElementById('user-id').value = ''; document.getElementById('modal-user-title').innerText = 'Tambah User Baru'; document.getElementById('hint-user-password').classList.add('hidden'); document.getElementById('user-password').required = true; document.getElementById('modal-user').classList.remove('hidden'); } function closeModalUser() { document.getElementById('modal-user').classList.add('hidden'); } function editUser(id) { const u = dbUsers.find(item => item.id === id); if (!u) return; document.getElementById('user-id').value = u.id; document.getElementById('user-username').value = u.username; document.getElementById('user-nama').value = u.nama; document.getElementById('user-role').value = u.role; document.getElementById('user-password').value = ''; document.getElementById('user-password').required = false; document.getElementById('hint-user-password').classList.remove('hidden'); document.getElementById('modal-user-title').innerText = 'Edit User'; document.getElementById('modal-user').classList.remove('hidden'); } async function handleSaveUser(e) { e.preventDefault(); const id = document.getElementById('user-id').value; const username = document.getElementById('user-username').value.trim(); const nama = document.getElementById('user-nama').value.trim(); const password = document.getElementById('user-password').value.trim(); const role = document.getElementById('user-role').value; let passwordHash = ''; if (password) { passwordHash = await hashPassword(password); } const payload = { id, username, nama, role, passwordHash }; const isUpdate = !!id; showLoading("Menyimpan User...", "Mengirim data ke server"); fetchWithRetry(APP_URL, { method: 'POST', body: JSON.stringify({ action: isUpdate ? 'updateUser' : 'addUser', data: payload }) }) .then(data => { hideLoading(); if (data.status === 'success') { if (isUpdate) { const idx = dbUsers.findIndex(u => u.id === id); if (idx !== -1) dbUsers[idx] = { id, username, nama, role }; } else { dbUsers.push({ id: data.id, username, nama, role }); } closeModalUser(); renderUsersTable(); Swal.fire({ icon: 'success', title: 'Berhasil', text: data.message }); } else { Swal.fire({ icon: 'error', title: 'Gagal', text: data.message }); } }) .catch(err => { hideLoading(); Swal.fire({ icon: 'error', title: 'Error Server', text: 'Gagal menyimpan user.' }); }); } function deleteUser(id, username) { Swal.fire({ title: 'Hapus User?', text: `Hapus akun user "${username}"?`, icon: 'warning', showCancelButton: true, confirmButtonColor: '#ef4444', confirmButtonText: 'Ya, Hapus' }).then((res) => { if (res.isConfirmed) { showLoading("Menghapus...", "Menghapus user"); fetchWithRetry(APP_URL, { method: 'POST', body: JSON.stringify({ action: 'deleteUser', id }) }) .then(data => { hideLoading(); if (data.status === 'success') { dbUsers = dbUsers.filter(u => u.id !== id); renderUsersTable(); Swal.fire({ icon: 'success', title: 'Terhapus', text: data.message }); } }); } }); }