Lompat ke konten Lompat ke sidebar Lompat ke footer

Source Code Laporan Keuangan Masjid

Di era digital saat ini, manajemen masjid dituntut untuk semakin profesional, terutama dalam hal pengelolaan dana umat. Berikut adalah Source Code Aplikasi Laporan Keuangan Masjid berbasis web (Google Apps Script) yang modern, responsif, dan siap pakai.

Apa Itu Laporan Keuangan Masjid?

Laporan Keuangan Masjid adalah catatan sistematis mengenai arus kas masuk (pemasukan) dan arus kas keluar (pengeluaran) dalam periode tertentu. Laporan ini mencakup pencatatan infaq Jumat, donasi, biaya operasional, pembangunan, hingga kegiatan sosial.

Pentingnya Transparansi Keuangan

Transparansi bukan sekadar pelaporan angka, melainkan bentuk pertanggungjawaban (akuntabilitas) kepada jamaah dan Allah SWT. Berikut mengapa transparansi sangat vital:

  • Membangun Kepercayaan (Trust): Jamaah akan lebih semangat berinfaq jika mengetahui dananya dikelola dengan amanah dan jelas peruntukannya.
  • Menghindari Fitnah: Pencatatan yang rapi meminimalisir kecurigaan atau kesalahpahaman antar pengurus maupun jamaah.
  • Perencanaan Lebih Baik: Dengan data visual yang jelas, DKM bisa merencanakan program pembangunan atau sosial dengan lebih terukur.

Source Code Aplikasi

Aplikasi ini dibangun menggunakan Google Apps Script (GAS) dan HTML5 + Bootstrap 5. Kelebihannya adalah gratis (hosting di Google), database menggunakan Google Sheets, dan tampilan sangat modern di HP.

File 1: Code.gs (Backend)
/**
 * Melayani halaman HTML utama
 */
function doGet() {
  return HtmlService.createTemplateFromFile('Index')
      .evaluate()
      .setTitle('Sistem Keuangan Masjid An Nur')
      .addMetaTag('viewport', 'width=device-width, initial-scale=1')
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

/**
 * Konfigurasi nama Sheet (Tab) database
 */
var SHEET_NAME = "Database_Keuangan";

/**
 * Membuka atau membuat Sheet database
 */
function getDbSheet() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName(SHEET_NAME);
  
  if (!sheet) {
    sheet = ss.insertSheet(SHEET_NAME);
    // Header Kolom
    sheet.appendRow(["ID", "Tanggal", "Jenis", "Kategori", "Jumlah", "Keterangan", "Timestamp"]);
    // Format Header
    sheet.getRange(1, 1, 1, 7).setFontWeight("bold").setBackground("#198754").setFontColor("white");
    sheet.setFrozenRows(1);
  }
  return sheet;
}

/**
 * Mengambil semua data transaksi dari Sheet
 */
function getTransactions() {
  try {
    var sheet = getDbSheet();
    var data = sheet.getDataRange().getValues();
    var transactions = [];
    
    // Mulai dari baris ke-2 (index 1) untuk melewati header
    if (data.length > 1) {
      for (var i = 1; i < data.length; i++) {
        var row = data[i];
        transactions.push({
          id: row[0],
          date: Utilities.formatDate(new Date(row[1]), Session.getScriptTimeZone(), "yyyy-MM-dd"),
          type: row[2],
          category: row[3],
          amount: row[4],
          description: row[5]
        });
      }
    }
    
    // Sortir dari tanggal terbaru (descending)
    return transactions.sort(function(a, b) {
      return new Date(b.date) - new Date(a.date);
    });
    
  } catch (e) {
    Logger.log("Error getting data: " + e.toString());
    throw new Error("Gagal mengambil data.");
  }
}

/**
 * Menyimpan transaksi baru ke Sheet
 */
function saveTransaction(formObject) {
  try {
    var sheet = getDbSheet();
    var id = new Date().getTime().toString(); // ID unik sederhana berdasarkan waktu
    var timestamp = new Date();
    
    sheet.appendRow([
      id,
      formObject.date,
      formObject.type,
      formObject.category,
      formObject.amount,
      formObject.description,
      timestamp
    ]);
    
    return { success: true, message: "Data berhasil disimpan!" };
    
  } catch (e) {
    return { success: false, message: "Error: " + e.toString() };
  }
}

/**
 * Menghapus transaksi berdasarkan ID
 */
function deleteTransaction(id) {
  try {
    var sheet = getDbSheet();
    var data = sheet.getDataRange().getValues();
    
    for (var i = 1; i < data.length; i++) {
      // Kolom ID ada di index 0
      if (String(data[i][0]) === String(id)) {
        sheet.deleteRow(i + 1); // +1 karena index sheet mulai dari 1
        return { success: true };
      }
    }
    return { success: false, message: "ID tidak ditemukan" };
    
  } catch (e) {
    return { success: false, message: "Gagal menghapus: " + e.toString() };
  }
}
File 2: Index.html (Frontend)
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Keuangan Masjid An Nur</title>
    
    <!-- Bootstrap 5.3 CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
    <!-- Font Awesome untuk Ikon -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <!-- Google Fonts -->
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
    <!-- Chart.js -->
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <!-- SweetAlert2 -->
    <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>

    <style>
        :root {
            --primary-color: #198754; /* Emerald Green */
            --secondary-color: #f8f9fa;
        }
        body {
            font-family: 'Inter', sans-serif;
            background-color: #f4f6f9;
        }
        .navbar {
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        .card {
            border: none;
            border-radius: 12px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.05);
            transition: transform 0.2s;
        }
        .card:hover {
            transform: translateY(-2px);
        }
        .nav-pills .nav-link {
            color: #495057;
            border-radius: 20px;
            padding: 8px 20px;
            font-weight: 500;
        }
        .nav-pills .nav-link.active {
            background-color: var(--primary-color);
            color: white;
        }
        .stat-icon {
            width: 48px;
            height: 48px;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 12px;
            font-size: 20px;
        }
        
        /* CSS Khusus Cetak/Print */
        @media print {
            body * {
                visibility: hidden;
            }
            #report-section, #report-section * {
                visibility: visible;
            }
            #report-section {
                position: absolute;
                left: 0;
                top: 0;
                width: 100%;
            }
            .no-print {
                display: none !important;
            }
            .card {
                box-shadow: none !important;
                border: 1px solid #ddd !important;
            }
            /* Hilangkan background warna saat print agar hemat tinta, kecuali diminta browser */
            .bg-light {
                background-color: #fff !important;
                border: 1px solid #eee;
            }
        }

        .loading-overlay {
            position: fixed;
            top: 0; left: 0; right: 0; bottom: 0;
            background: rgba(255,255,255,0.8);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 9999;
        }
    </style>
</head>
<body>

    <!-- Loading Spinner -->
    <div id="loading" class="loading-overlay">
        <div class="spinner-border text-success" role="status">
            <span class="visually-hidden">Loading...</span>
        </div>
    </div>

    <!-- Navbar -->
    <nav class="navbar navbar-expand-lg navbar-dark bg-success sticky-top">
        <div class="container">
            <a class="navbar-brand fw-bold" href="#">
                <i class="fas fa-mosque me-2"></i> Masjid An Nur
            </a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse justify-content-end" id="navbarNav">
                <ul class="navbar-nav nav-pills gap-2 bg-white p-2 rounded-pill mt-2 mt-lg-0">
                    <li class="nav-item">
                        <a class="nav-link active" href="#" onclick="switchView('dashboard')"><i class="fas fa-chart-pie me-1"></i> Dashboard</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#" onclick="switchView('input')"><i class="fas fa-plus-circle me-1"></i> Input</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#" onclick="switchView('report')"><i class="fas fa-file-alt me-1"></i> Laporan</a>
                    </li>
                </ul>
            </div>
        </div>
    </nav>

    <div class="container py-4">
        
        <!-- View: Dashboard -->
        <div id="dashboard-view">
            <!-- Summary Cards -->
            <div class="row g-3 mb-4">
                <div class="col-md-4">
                    <div class="card h-100 border-start border-4 border-success">
                        <div class="card-body">
                            <div class="d-flex justify-content-between align-items-center mb-2">
                                <h6 class="text-muted mb-0">Total Pemasukan</h6>
                                <div class="stat-icon bg-success bg-opacity-10 text-success">
                                    <i class="fas fa-arrow-up"></i>
                                </div>
                            </div>
                            <h3 class="fw-bold text-dark" id="total-income">Rp 0</h3>
                        </div>
                    </div>
                </div>
                <div class="col-md-4">
                    <div class="card h-100 border-start border-4 border-danger">
                        <div class="card-body">
                            <div class="d-flex justify-content-between align-items-center mb-2">
                                <h6 class="text-muted mb-0">Total Pengeluaran</h6>
                                <div class="stat-icon bg-danger bg-opacity-10 text-danger">
                                    <i class="fas fa-arrow-down"></i>
                                </div>
                            </div>
                            <h3 class="fw-bold text-dark" id="total-expense">Rp 0</h3>
                        </div>
                    </div>
                </div>
                <div class="col-md-4">
                    <div class="card h-100 border-start border-4 border-primary">
                        <div class="card-body">
                            <div class="d-flex justify-content-between align-items-center mb-2">
                                <h6 class="text-muted mb-0">Saldo Akhir</h6>
                                <div class="stat-icon bg-primary bg-opacity-10 text-primary">
                                    <i class="fas fa-wallet"></i>
                                </div>
                            </div>
                            <h3 class="fw-bold text-dark" id="current-balance">Rp 0</h3>
                        </div>
                    </div>
                </div>
            </div>

            <!-- Charts -->
            <div class="row g-4">
                <div class="col-md-8">
                    <div class="card h-100">
                        <div class="card-header bg-white fw-bold">Grafik Arus Kas (6 Bulan Terakhir)</div>
                        <div class="card-body">
                            <canvas id="barChart"></canvas>
                        </div>
                    </div>
                </div>
                <div class="col-md-4">
                    <div class="card h-100">
                        <div class="card-header bg-white fw-bold">Analisa Pengeluaran</div>
                        <div class="card-body">
                            <canvas id="pieChart"></canvas>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <!-- View: Input Form -->
        <div id="input-view" class="d-none">
            <div class="row justify-content-center">
                <div class="col-lg-8">
                    <div class="card">
                        <div class="card-header bg-white py-3">
                            <h5 class="mb-0 fw-bold"><i class="fas fa-pen-to-square me-2"></i>Form Transaksi Baru</h5>
                        </div>
                        <div class="card-body p-4">
                            <form id="transactionForm" onsubmit="handleFormSubmit(event)">
                                
                                <div class="mb-4 text-center">
                                    <div class="btn-group w-100" role="group">
                                        <input type="radio" class="btn-check" name="type" id="type-income" value="Pemasukan" checked onchange="updateCategories()">
                                        <label class="btn btn-outline-success py-3" for="type-income">
                                            <i class="fas fa-plus me-2"></i>Pemasukan
                                        </label>
                                        
                                        <input type="radio" class="btn-check" name="type" id="type-expense" value="Pengeluaran" onchange="updateCategories()">
                                        <label class="btn btn-outline-danger py-3" for="type-expense">
                                            <i class="fas fa-minus me-2"></i>Pengeluaran
                                        </label>
                                    </div>
                                </div>

                                <div class="row g-3">
                                    <div class="col-md-6">
                                        <label class="form-label">Tanggal</label>
                                        <input type="date" class="form-control" name="date" id="input-date" required>
                                    </div>
                                    <div class="col-md-6">
                                        <label class="form-label">Kategori</label>
                                        <select class="form-select" name="category" id="input-category" required>
                                            <!-- Options populated by JS -->
                                        </select>
                                    </div>
                                    <div class="col-12">
                                        <label class="form-label">Jumlah (Rp)</label>
                                        <div class="input-group">
                                            <span class="input-group-text">Rp</span>
                                            <input type="number" class="form-control" name="amount" min="1" required placeholder="0">
                                        </div>
                                    </div>
                                    <div class="col-12">
                                        <label class="form-label">Keterangan</label>
                                        <textarea class="form-control" name="description" rows="3" required placeholder="Detail transaksi..."></textarea>
                                    </div>
                                </div>

                                <div class="d-grid mt-4">
                                    <button type="submit" class="btn btn-success btn-lg">
                                        <i class="fas fa-save me-2"></i>Simpan Transaksi
                                    </button>
                                </div>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <!-- View: Report & History -->
        <div id="report-section" class="d-none"> <!-- ID used for print context -->
            <div class="card">
                <div class="card-header bg-white py-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
                    <h5 class="mb-0 fw-bold"><i class="fas fa-table me-2"></i>Laporan Keuangan</h5>
                    <div class="d-flex gap-2 no-print">
                        <select id="filter-period" class="form-select form-select-sm" onchange="filterData()" style="width: auto;">
                            <option value="all">Semua Data</option>
                            <option value="week">Minggu Ini</option>
                            <option value="month">Bulan Ini</option>
                            <option value="year">Tahun Ini</option>
                        </select>
                        <button onclick="window.print()" class="btn btn-primary btn-sm">
                            <i class="fas fa-print me-1"></i> Cetak Laporan
                        </button>
                    </div>
                </div>
                
                <!-- Print Header (Visible only when printing) -->
                <div class="d-none d-print-block p-4 text-center mb-3">
                    <h2>MASJID AN NUR</h2>
                    <p class="mb-0">Laporan Keuangan Masjid</p>
                    <small id="print-period-label" class="text-muted"></small>
                    <hr>
                </div>

                <div class="card-body">
                    <!-- Summary Report Box -->
                    <div class="row text-center mb-4 p-3 bg-light rounded mx-1 border">
                        <div class="col-4 border-end">
                            <small class="text-muted">Pemasukan Periode Ini</small>
                            <h5 class="text-success fw-bold mb-0" id="report-income">Rp 0</h5>
                        </div>
                        <div class="col-4 border-end">
                            <small class="text-muted">Pengeluaran Periode Ini</small>
                            <h5 class="text-danger fw-bold mb-0" id="report-expense">Rp 0</h5>
                        </div>
                        <div class="col-4">
                            <small class="text-muted">Saldo Periode Ini</small>
                            <h5 class="text-primary fw-bold mb-0" id="report-balance">Rp 0</h5>
                        </div>
                    </div>

                    <div class="table-responsive">
                        <table class="table table-hover table-striped align-middle" id="transaction-table">
                            <thead class="table-success">
                                <tr>
                                    <th>Tanggal</th>
                                    <th>Kategori</th>
                                    <th>Keterangan</th>
                                    <th class="text-end">Masuk</th>
                                    <th class="text-end">Keluar</th>
                                    <th class="text-center no-print">Aksi</th>
                                </tr>
                            </thead>
                            <tbody id="table-body">
                                <!-- Data will be loaded here -->
                            </tbody>
                        </table>
                    </div>
                </div>
                
                <!-- Print Footer -->
                <div class="d-none d-print-block mt-5 pt-5 px-4">
                    <div class="row text-center">
                        <div class="col-4">
                            <p>Mengetahui,<br>Ketua DKM</p>
                            <br><br><br>
                            <p>( ........................... )</p>
                        </div>
                        <div class="col-4 offset-4">
                            <p>Bendahara Masjid</p>
                            <br><br><br>
                            <p>( ........................... )</p>
                        </div>
                    </div>
                </div>

            </div>
        </div>

    </div>

    <!-- JavaScript Application Logic -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
    
    <script>
        // --- Data & State Management ---
        let allTransactions = [];
        let barChartInstance = null;
        let pieChartInstance = null;

        // --- Kategori Constants ---
        const categories = {
            'Pemasukan': [
                'Kotak Amal Jumat',
                'Kotak Amal Harian/Rutin',
                'Donasi/Sumbangan',
                'Infaq Pembangunan',
                'ZIS (Zakat, Infaq, Sedekah)',
                'Hasil Wakaf/Usaha Masjid',
                'Lain-lain'
            ],
            'Pengeluaran': [
                'Biaya Operasional',
                'Listrik',
                'Air',
                'Kebersihan',
                'Pembangunan',
                'Acara',
                'Honorarium',
                'Pemeliharaan dan Perbaikan',
                'Pembelian Perlengkapan',
                'Kegiatan Keagamaan',
                'Lain-lain'
            ]
        };

        // --- Inisialisasi ---
        document.addEventListener('DOMContentLoaded', () => {
            // Set default date
            document.getElementById('input-date').valueAsDate = new Date();
            // Init Categories
            updateCategories();
            // Load Data
            fetchData();
        });

        // --- Navigasi ---
        function switchView(viewName) {
            document.getElementById('dashboard-view').classList.add('d-none');
            document.getElementById('input-view').classList.add('d-none');
            document.getElementById('report-section').classList.add('d-none');
            
            // Remove active class from navs
            document.querySelectorAll('.nav-link').forEach(el => el.classList.remove('active'));
            
            if (viewName === 'dashboard') {
                document.getElementById('dashboard-view').classList.remove('d-none');
                document.querySelectorAll('.nav-link')[0].classList.add('active');
                renderCharts(); // Re-render charts
            } else if (viewName === 'input') {
                document.getElementById('input-view').classList.remove('d-none');
                document.querySelectorAll('.nav-link')[1].classList.add('active');
            } else if (viewName === 'report') {
                document.getElementById('report-section').classList.remove('d-none');
                document.querySelectorAll('.nav-link')[2].classList.add('active');
                filterData(); // Refresh report view
            }
        }

        // --- Form Helper ---
        function updateCategories() {
            const type = document.querySelector('input[name="type"]:checked').value;
            const select = document.getElementById('input-category');
            select.innerHTML = '';
            
            categories[type].forEach(cat => {
                const option = document.createElement('option');
                option.value = cat;
                option.text = cat;
                select.appendChild(option);
            });
        }

        const formatRupiah = (number) => {
            return new Intl.NumberFormat('id-ID', {
                style: 'currency',
                currency: 'IDR',
                minimumFractionDigits: 0
            }).format(number);
        };

        // --- Google Apps Script Integration ---

        function fetchData() {
            document.getElementById('loading').style.display = 'flex';
            if (typeof google === 'undefined' || typeof google.script === 'undefined') {
                 // Fallback for local testing (jika dibuka tanpa GAS)
                 console.warn('Google Script Run not found. Using empty data for test.');
                 onDataLoaded([]); 
                 return;
            }
            google.script.run
                .withSuccessHandler(onDataLoaded)
                .withFailureHandler(onError)
                .getTransactions();
        }

        function onDataLoaded(data) {
            allTransactions = data;
            document.getElementById('loading').style.display = 'none';
            calculateSummary();
            filterData(); // This populates the table
            renderCharts();
        }

        function handleFormSubmit(e) {
            e.preventDefault();
            document.getElementById('loading').style.display = 'flex';

            const formData = {
                date: e.target.date.value,
                type: document.querySelector('input[name="type"]:checked').value,
                category: e.target.category.value,
                amount: e.target.amount.value,
                description: e.target.description.value
            };

            google.script.run
                .withSuccessHandler((res) => {
                    document.getElementById('loading').style.display = 'none';
                    if(res.success) {
                        Swal.fire('Berhasil', 'Data transaksi tersimpan', 'success');
                        e.target.reset();
                        document.getElementById('input-date').valueAsDate = new Date();
                        updateCategories();
                        fetchData(); // Reload data
                    } else {
                        Swal.fire('Gagal', res.message, 'error');
                    }
                })
                .withFailureHandler(onError)
                .saveTransaction(formData);
        }

        function deleteItem(id) {
            Swal.fire({
                title: 'Hapus data?',
                text: "Data yang dihapus tidak bisa dikembalikan!",
                icon: 'warning',
                showCancelButton: true,
                confirmButtonColor: '#d33',
                cancelButtonColor: '#3085d6',
                confirmButtonText: 'Ya, Hapus!'
            }).then((result) => {
                if (result.isConfirmed) {
                    document.getElementById('loading').style.display = 'flex';
                    google.script.run
                        .withSuccessHandler(() => {
                            fetchData();
                            Swal.fire('Terhapus!', 'Data telah dihapus.', 'success');
                        })
                        .withFailureHandler(onError)
                        .deleteTransaction(id);
                }
            });
        }

        function onError(error) {
            document.getElementById('loading').style.display = 'none';
            Swal.fire('Error', error.message, 'error');
        }

        // --- Data Processing & Logic ---

        function calculateSummary() {
            let income = 0;
            let expense = 0;

            allTransactions.forEach(t => {
                if (t.type === 'Pemasukan') income += Number(t.amount);
                else expense += Number(t.amount);
            });

            document.getElementById('total-income').innerText = formatRupiah(income);
            document.getElementById('total-expense').innerText = formatRupiah(expense);
            document.getElementById('current-balance').innerText = formatRupiah(income - expense);
        }

        function filterData() {
            const period = document.getElementById('filter-period').value;
            const printLabel = document.getElementById('print-period-label');
            const now = new Date();
            // Reset hours for reliable comparison
            now.setHours(23, 59, 59, 999); 

            let filtered = [];
            let labelText = "";

            if (period === 'all') {
                filtered = allTransactions;
                labelText = "Periode: Semua Data";
            } else {
                if (period === 'year') {
                     labelText = "Periode: Tahun " + now.getFullYear();
                } else if (period === 'month') {
                     const monthName = now.toLocaleString('id-ID', { month: 'long' });
                     labelText = "Periode: Bulan " + monthName + " " + now.getFullYear();
                } else if (period === 'week') {
                     labelText = "Periode: 7 Hari Terakhir";
                }

                filtered = allTransactions.filter(t => {
                    // Fix: Parse YYYY-MM-DD explicitly to avoid timezone shifts
                    // simple new Date(t.date) is usually ok, but explicitly zeroing helps
                    const parts = t.date.split('-'); 
                    // Note: Month is 0-indexed in JS Date
                    const tDate = new Date(parts[0], parts[1] - 1, parts[2]); 
                    
                    if (period === 'year') return tDate.getFullYear() === now.getFullYear();
                    if (period === 'month') return tDate.getMonth() === now.getMonth() && tDate.getFullYear() === now.getFullYear();
                    if (period === 'week') {
                        const oneWeekAgo = new Date(now);
                        oneWeekAgo.setDate(now.getDate() - 7);
                        oneWeekAgo.setHours(0, 0, 0, 0); // Start of that day
                        
                        return tDate >= oneWeekAgo && tDate <= now;
                    }
                    return true;
                });
            }
            
            if(printLabel) printLabel.innerText = labelText;
            renderTable(filtered);
        }

        function renderTable(data) {
            const tbody = document.getElementById('table-body');
            tbody.innerHTML = '';
            
            let pIncome = 0;
            let pExpense = 0;

            if (data.length === 0) {
                tbody.innerHTML = `
                    <tr>
                        <td colspan="6" class="text-center py-4 text-muted">
                            <i class="fas fa-info-circle me-2"></i>Belum ada data transaksi pada periode ini.
                        </td>
                    </tr>`;
            } else {
                data.forEach(t => {
                    const isIncome = t.type === 'Pemasukan';
                    if(isIncome) pIncome += Number(t.amount);
                    else pExpense += Number(t.amount);

                    const tr = document.createElement('tr');
                    tr.innerHTML = `
                        <td>${t.date}</td>
                        <td><span class="badge ${isIncome ? 'bg-success bg-opacity-10 text-success' : 'bg-danger bg-opacity-10 text-danger'} border ${isIncome ? 'border-success' : 'border-danger'}">${t.category}</span></td>
                        <td>${t.description}</td>
                        <td class="text-end text-success fw-bold">${isIncome ? formatRupiah(t.amount) : '-'}</td>
                        <td class="text-end text-danger fw-bold">${!isIncome ? formatRupiah(t.amount) : '-'}</td>
                        <td class="text-center no-print">
                            <button class="btn btn-sm btn-outline-danger border-0" onclick="deleteItem('${t.id}')">
                                <i class="fas fa-trash"></i>
                            </button>
                        </td>
                    `;
                    tbody.appendChild(tr);
                });
            }

            // Update Report Headers
            document.getElementById('report-income').innerText = formatRupiah(pIncome);
            document.getElementById('report-expense').innerText = formatRupiah(pExpense);
            document.getElementById('report-balance').innerText = formatRupiah(pIncome - pExpense);
        }

        // --- Chart Logic ---

        function renderCharts() {
            if(barChartInstance) barChartInstance.destroy();
            if(pieChartInstance) pieChartInstance.destroy();

            // --- Logic Bar Chart (6 Bulan Terakhir) ---
            const months = [];
            const incomeData = [];
            const expenseData = [];
            
            // Set start date to the 1st of current month to avoid "31st day" skip bug
            const today = new Date();
            today.setDate(1); 

            // Generate last 6 months labels
            for (let i = 5; i >= 0; i--) {
                const d = new Date(today);
                d.setMonth(d.getMonth() - i);
                const monthKey = d.toLocaleString('id-ID', { month: 'short' }); // Jan, Feb...
                months.push(monthKey);
                
                // Filter data for this specific month/year
                const m = d.getMonth();
                const y = d.getFullYear();
                
                // Menggunakan logic parsing yang konsisten dengan filterData
                // agar timezone aman
                const inc = allTransactions
                    .filter(t => {
                        const parts = t.date.split('-');
                        const tDate = new Date(parts[0], parts[1] - 1, parts[2]);
                        return t.type === 'Pemasukan' && tDate.getMonth() === m && tDate.getFullYear() === y;
                    })
                    .reduce((acc, curr) => acc + Number(curr.amount), 0);
                    
                const exp = allTransactions
                    .filter(t => {
                        const parts = t.date.split('-');
                        const tDate = new Date(parts[0], parts[1] - 1, parts[2]);
                        return t.type === 'Pengeluaran' && tDate.getMonth() === m && tDate.getFullYear() === y;
                    })
                    .reduce((acc, curr) => acc + Number(curr.amount), 0);
                    
                incomeData.push(inc);
                expenseData.push(exp);
            }

            const ctxBar = document.getElementById('barChart').getContext('2d');
            barChartInstance = new Chart(ctxBar, {
                type: 'bar',
                data: {
                    labels: months,
                    datasets: [
                        {
                            label: 'Pemasukan',
                            data: incomeData,
                            backgroundColor: '#198754',
                            borderRadius: 4
                        },
                        {
                            label: 'Pengeluaran',
                            data: expenseData,
                            backgroundColor: '#dc3545',
                            borderRadius: 4
                        }
                    ]
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    scales: {
                        y: { beginAtZero: true, ticks: { callback: function(value) { return 'Rp ' + value/1000 + 'k'; } } }
                    },
                    plugins: {
                        tooltip: {
                            callbacks: {
                                label: function(context) {
                                    return context.dataset.label + ': ' + formatRupiah(context.raw);
                                }
                            }
                        }
                    }
                }
            });

            // --- Logic Pie Chart (Komposisi Pengeluaran) ---
            // Fokus ke pengeluaran agar bisa analisa biaya
            const catMap = {};
            allTransactions.filter(t => t.type === 'Pengeluaran').forEach(t => {
                catMap[t.category] = (catMap[t.category] || 0) + Number(t.amount);
            });
            
            // Handle jika tidak ada data pengeluaran
            const pieLabels = Object.keys(catMap).length ? Object.keys(catMap) : ['Belum ada data'];
            const pieValues = Object.keys(catMap).length ? Object.values(catMap) : [1];
            const pieColors = Object.keys(catMap).length 
                ? ['#dc3545', '#fd7e14', '#ffc107', '#20c997', '#0d6efd', '#6610f2', '#6c757d', '#343a40'] 
                : ['#e9ecef'];

            const ctxPie = document.getElementById('pieChart').getContext('2d');
            pieChartInstance = new Chart(ctxPie, {
                type: 'doughnut',
                data: {
                    labels: pieLabels,
                    datasets: [{
                        data: pieValues,
                        backgroundColor: pieColors
                    }]
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    plugins: {
                        legend: { position: 'bottom', labels: { boxWidth: 10, font: { size: 10 } } },
                        tooltip: {
                            callbacks: {
                                label: function(context) {
                                    const val = context.raw;
                                    // Don't show tooltip for placeholder
                                    if(context.label === 'Belum ada data') return 'Belum ada data';
                                    return context.label + ': ' + formatRupiah(val);
                                }
                            }
                        }
                    }
                }
            });
        }
    </script>
</body>
</html>

Langkah-Langkah Instalasi

Silakan ikuti panduan berikut untuk memasang aplikasi ini di akun Google Anda secara GRATIS:

  • Buat Google Spreadsheet Buka Google Sheets, buat spreadsheet baru, dan beri nama (misal: "Keuangan Masjid"). Biarkan kosong, sistem akan membuatnya otomatis nanti.
  • Buka Apps Script Di menu Google Sheet, klik Extensions (Ekstensi) > Apps Script.
  • Pasang Kode Backend (Code.gs) Hapus semua kode yang ada di file Code.gs, lalu salin dan tempel kode dari File 1 (Backend) di atas.
  • Buat File HTML (Index.html) Klik tanda tambah (+) di samping "Files", pilih HTML, beri nama filenya Index. Hapus isinya, lalu salin dan tempel kode dari File 2 (Frontend) di atas.
  • Deploy Aplikasi Klik tombol biru Deploy (di kanan atas) > New deployment.
    - Select type: Web app
    - Description: "Versi 1"
    - Execute as: Me (Email Anda)
    - Who has access: Anyone (Siapa saja)
    Klik Deploy.
  • Selesai! Anda akan mendapatkan URL Web App. Klik URL tersebut untuk membuka aplikasi keuangan masjid Anda.

Tips: Simpan URL Web App tersebut (Bookmark) di browser HP pengurus masjid agar mudah diakses sewaktu-waktu.