Lompat ke konten Lompat ke sidebar Lompat ke footer

Prompt dan Source Code Aplikasi Buku Tamu Sekolah Menggunakan Appscript

Di era digitalisasi pendidikan, pencatatan tamu sekolah secara manual menggunakan buku tulis sudah mulai ditinggalkan. Selain kurang efisien, data juga rawan hilang dan sulit untuk direkapitulasi. Solusi terbaik, termudah, dan termurah adalah dengan membuat Aplikasi Buku Tamu Online menggunakan Google Apps Script (AppScript).

Apa itu Google Apps Script (AppScript)?

Google Apps Script adalah platform pembuatan skrip (coding) berbasis *cloud* dari Google yang memungkinkan kita untuk mengotomatisasi proses dan membuat aplikasi web ringan tanpa perlu membeli server atau hosting. Script ini terintegrasi langsung dengan ekosistem Google Workspace, termasuk Google Sheets, Docs, dan Drive.

Manfaat untuk Aplikasi Catat Tamu

  • 100% Gratis & Tanpa Server: Anda tidak perlu menyewa hosting atau domain bulanan. Semuanya dihosting oleh infrastruktur Google secara gratis.
  • Database Otomatis ke Google Sheets: Setiap tamu yang mengisi formulir, datanya akan langsung masuk dan tersusun rapi di Spreadsheet layaknya file Excel.
  • Keamanan Data Terjamin: Data tersimpan di akun Google Drive Anda sendiri sehingga aman dari hilang, rusak, atau server down.
  • Mudah Diakses: Berupa link (URL) yang bisa dibuka melalui *smartphone*, tablet, atau laptop oleh satpam maupun tamu sekolah.

Langkah-Langkah Pembuatan Aplikasi

  • Langkah 1: Buka Google Sheets baru di akun Google Anda (drive.google.com), lalu beri nama file bebas (misal: "Data Buku Tamu Sekolah").
  • Langkah 2: Pada menu atas Google Sheets, klik Ekstensi > Apps Script. Sebuah tab baru berisi editor kode akan terbuka.
  • Langkah 3: Di editor tersebut, buat dua file. Yang pertama biarkan bernama Code.gs, lalu buat file HTML baru (File > Baru > HTML) dan beri nama Index.html.
  • Langkah 4: Salin (Copy) kode backend (Code.gs) dan frontend (Index.html) yang ada di bawah artikel ini, lalu tempel (Paste) ke file yang sesuai di editor Apps Script Anda.
  • Langkah 5: Simpan project (ikon disket). Lalu di pojok kanan atas klik tombol Terapkan (Deploy) > Deployment Baru.
  • Langkah 6: Pilih jenis Aplikasi Web. Pada bagian "Yang memiliki akses", pastikan pilih Siapa saja (Anyone) agar tamu tidak perlu login. Klik Terapkan dan setujui izin (Authorize access). Selesai! URL aplikasi Anda siap digunakan.

1. Prompt AI (Jika Ingin Memodifikasi via AI)

Jika Anda ingin menggunakan ChatGPT, Gemini, atau Claude untuk membuat ulang atau mengembangkan aplikasi ini, silakan salin prompt spesifik dan anti-error di bawah ini:

Prompt AI
Bertindaklah sebagai Senior Full-Stack Web Developer dan Pakar Keamanan Google Apps Script. 

Tugas Anda adalah membuatkan Web App "Buku Tamu Online" untuk "SMP Negeri 3 Kerinci" yang terintegrasi dengan Google Sheets sebagai database. Hasil akhir harus terdiri dari 2 file terpisah yang siap deploy: `Code.gs` (Backend) dan `Index.html` (Frontend).

Terapkan spesifikasi ketat berikut ini:

1. STACK TEKNOLOGI (FRONTEND):
- HTML5 murni dalam 1 file.
- CSS Framework: Bootstrap 5 (via CDN).
- Ikon: Bootstrap Icons.
- Notifikasi: SweetAlert2.
- Grafik: Chart.js (Grafik Batang statistik pengunjung per hari).
- Export: jsPDF dan jsPDF-AutoTable.

2. UI/UX & TATA LETAK (DASHBOARD MODERN):
- Buat layout Dashboard: Sidebar di kiri (gradient biru) dan Main Content di kanan.
- Responsif: Di HP, Sidebar disembunyikan dan diakses via tombol Navbar atas (Offcanvas slide).
- Di Main Content: Sediakan "Welcome Card" elegan, tombol aksi, area Grafik Kunjungan (Chart.js), dan area Tabel Data (lengkap dengan state "Loading" dan "Empty State").

3. FITUR FORM (POP-UP MODAL):
- Form pengisian tidak memakan tempat, melainkan menggunakan Modal Pop-up Bootstrap.
- Field required: Nama Lengkap, Instansi/Asal, No Telp/WA, Tujuan.
- Terapkan fungsi `autofocus` pada input Nama saat modal terbuka.
- Tombol submit harus memiliki efek "loading" (spinner) saat diklik agar user tidak double-click. Tutup modal otomatis jika sukses.

4. FITUR TABEL & PDF:
- Tabel menampilkan urutan dari yang terbaru ke terlama.
- Kolom Kontak harus berupa tombol/link yang langsung mengarah ke `https://wa.me/` dengan nomor telepon yang sudah diformat ke 62.
- Tombol Export PDF menghasilkan file Landscape yang rapi dengan AutoTable (kop biru, ada tanggal cetak).

5. BACKEND APPS SCRIPT (Code.gs) & OTOMATISASI:
- Fungsi `doGet` memanggil `Index.html`.
- Otomatisasi Sheet: Buat fungsi `getSheet()`. Jika tab "Data Tamu" belum ada, otomatis buatkan tabnya, isi header (Waktu, Nama Lengkap, Instansi, No Telp, Tujuan), tebalkan font (bold), dan bekukan baris 1 (freeze row).

6. KEAMANAN & STABILITAS (WAJIB ADA TIGA FITUR INI):
- Anti-Tabrakan (Backend): Gunakan `LockService.getScriptLock()` dengan timeout 10 detik di fungsi simpan, agar jika ada banyak tamu mensubmit bersamaan, data masuk antrean dan tidak tertimpa/hilang.
- Anti-Injeksi Rumus (Backend): Sanitasi input data. Jika input berawalan "=", "+", "-", atau "@", tambahkan tanda kutip satu (') di depannya agar tidak dieksekusi sebagai rumus oleh Google Sheets.
- Anti-XSS (Frontend): Buat fungsi `escapeHTML` di JavaScript. Sanitasi semua data teks (Nama, Instansi, Tujuan, dll) sebelum di-render/dicetak ke `innerHTML` tabel.
- Local Fallback (Frontend): Buat logika deteksi `if (typeof google !== 'undefined')`. Jika tidak berjalan di Apps Script (hanya preview HTML biasa), gunakan array lokal sementara agar aplikasi tetap bisa dites UI-nya tanpa error.

Berikan kodenya secara lengkap, terstruktur, tanpa bagian yang terpotong. Berikan instruksi singkat cara deploy-nya di akhir.

2. Source Code Backend (Code.gs)

Salin kode di bawah ini dan tempel (paste) ke dalam file Code.gs di editor Apps Script Anda:

Code.gs
// =========================================================================
// BACKEND: GOOGLE APPS SCRIPT
// Simpan kode ini di dalam file "Code.gs"
// =========================================================================

var SHEET_NAME = "Data Tamu"; // Nama tab otomatis

// Fungsi bantuan untuk mengecek, membuat sheet, dan membuat header otomatis
function getSheet() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName(SHEET_NAME);
  
  // Jika sheet/tab belum ada, maka buat baru
  if (!sheet) {
    sheet = ss.insertSheet(SHEET_NAME);
    setupHeader(sheet);
  } else {
    // Jika sheet ada tapi kosong (tidak ada baris sama sekali)
    if (sheet.getLastRow() === 0) {
      setupHeader(sheet);
    }
  }
  return sheet;
}

// Fungsi untuk mengatur header
function setupHeader(sheet) {
  var headers = ["Waktu", "Nama Lengkap", "Instansi", "No Telp", "Tujuan"];
  sheet.appendRow(headers);
  
  // Mempercantik tampilan header di Google Sheets (Bold & Bekukan Baris 1)
  sheet.getRange("A1:E1").setFontWeight("bold");
  sheet.setFrozenRows(1);
}

// Fungsi utama saat web app diakses
function doGet() {
  // Panggil getSheet() agar saat pertama kali web dibuka, sheet otomatis dibuat (jika belum ada)
  getSheet(); 
  
  return HtmlService.createHtmlOutputFromFile('Index')
      .setTitle('Buku Tamu SMP Negeri 3 Kerinci')
      .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

// Fungsi menyimpan data
function saveData(nama, instansi, telp, tujuan) {
  var sheet = getSheet(); // Selalu gunakan getSheet untuk memastikan sheet siap
  
  // Format waktu sesuai zona waktu Indonesia (WIB)
  var timestamp = Utilities.formatDate(new Date(), "GMT+7", "dd MMM yyyy HH:mm");
  
  // Menambahkan baris baru ke dalam sheet
  sheet.appendRow([timestamp, nama, instansi, telp, tujuan]);
  
  // Mengembalikan data terbaru ke tabel halaman pengunjung
  return getLatestData();
}

// Fungsi mengambil data untuk ditampilkan di tabel html
function getLatestData() {
  var sheet = getSheet(); // Selalu pastikan sheet siap
  var guests = [];
  var lastRow = sheet.getLastRow();
  
  // Mulai dari baris ke-2 (mengabaikan header di baris 1)
  if (lastRow > 1) { 
    // Mengambil data spesifik dari baris 2 sampai baris terakhir, dari kolom 1 (A) sampai 5 (E)
    var data = sheet.getRange(2, 1, lastRow - 1, 5).getDisplayValues();
    
    for (var i = 0; i < data.length; i++) {
      // Pastikan baris tidak kosong
      if(data[i][0] !== "") { 
        guests.push({
          waktu: data[i][0],
          nama: data[i][1],
          instansi: data[i][2],
          telp: data[i][3],
          tujuan: data[i][4]
        });
      }
    }
  }
  return guests;
}

3. Source Code Frontend (Index.html)

Terakhir, buat file baru berjenis HTML bernama Index.html di Apps Script Anda, lalu salin dan tempel kode lengkap Tampilan Antarmuka di bawah ini:

Index.html
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Buku Tamu SMP Negeri 3 Kerinci</title>

    <!-- Bootstrap 5 CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
    <!-- Bootstrap Icons -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
    <!-- SweetAlert2 CSS -->
    <link href="https://cdn.jsdelivr.net/npm/sweetalert2@11.7.32/dist/sweetalert2.min.css" rel="stylesheet">
    
    <style>
        body {
            background-color: #f4f7f6;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            overflow-x: hidden;
        }
        
        /* Sidebar Styles */
        .sidebar {
            width: 260px;
            background: linear-gradient(180deg, #0d6efd 0%, #0a53be 100%);
            min-height: 100vh;
            transition: all 0.3s;
            z-index: 1000;
        }
        .sidebar .nav-link {
            color: rgba(255, 255, 255, 0.8);
            padding: 12px 20px;
            border-radius: 8px;
            margin: 4px 15px;
            font-weight: 500;
        }
        .sidebar .nav-link:hover, .sidebar .nav-link.active {
            color: #fff;
            background-color: rgba(255, 255, 255, 0.15);
        }
        .sidebar-brand {
            padding: 20px 15px;
            color: white;
            text-align: center;
            border-bottom: 1px solid rgba(255,255,255,0.1);
            margin-bottom: 15px;
        }

        /* Main Content */
        .main-content {
            flex-grow: 1;
            min-width: 0; /* Prevent flex overflow */
            padding-bottom: 50px;
        }

        /* Welcome Card */
        .welcome-card {
            background-color: #ffffff;
            border-left: 5px solid #0d6efd;
            border-radius: 10px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.03);
        }

        /* Standard Card */
        .card {
            border: none;
            border-radius: 12px;
            box-shadow: 0 5px 15px rgba(0,0,0,0.04);
            margin-bottom: 24px;
        }
        .card-header {
            background-color: #fff;
            border-bottom: 1px solid #f0f0f0;
            border-top-left-radius: 12px !important;
            border-top-right-radius: 12px !important;
            padding: 1rem 1.25rem;
            font-weight: 600;
        }

        /* Table & Layout */
        .table > :not(caption) > * > * {
            padding: 1rem 0.75rem;
            vertical-align: middle;
        }
        .empty-state {
            text-align: center;
            padding: 3rem 1rem;
            color: #6c757d;
        }
        .empty-state i {
            font-size: 3.5rem;
            color: #dee2e6;
            margin-bottom: 1rem;
        }
        
        /* Mobile Toggle Navbar */
        .mobile-navbar {
            display: none;
            background: #0d6efd;
            padding: 10px 20px;
            color: white;
        }

        /* Responsive adjustments */
        @media (max-width: 768px) {
            .sidebar.desktop-sidebar {
                display: none !important;
            }
            .mobile-navbar {
                display: flex;
                justify-content: space-between;
                align-items: center;
            }
            .main-content {
                padding: 15px;
            }
        }
    </style>
</head>
<body>

    <div class="d-flex flex-column flex-md-row">
        
        <!-- ================= SIDEBAR (DESKTOP) ================= -->
        <div class="sidebar desktop-sidebar d-none d-md-flex flex-column shadow">
            <div class="sidebar-brand">
                <i class="bi bi-journal-bookmark-fill fs-2 d-block mb-2"></i>
                <h5 class="fw-bold mb-0">SMPN 3 Kerinci</h5>
                <small class="text-white-50">Buku Tamu Digital</small>
            </div>
            <ul class="nav flex-column mb-auto">
                <li>
                    <a href="#" class="nav-link active">
                        <i class="bi bi-speedometer2 me-2"></i> Dashboard
                    </a>
                </li>
                <li>
                    <a href="#tabelData" class="nav-link">
                        <i class="bi bi-table me-2"></i> Data Pengunjung
                    </a>
                </li>
            </ul>
            <div class="p-3 mt-auto text-center text-white-50 small border-top border-light border-opacity-10">
                &copy; 2026 E-GuestBook
            </div>
        </div>

        <!-- ================= NAVBAR MOBILE ================= -->
        <div class="mobile-navbar shadow-sm sticky-top">
            <h5 class="mb-0 fw-bold"><i class="bi bi-journal-bookmark-fill me-2"></i>SMPN 3 Kerinci</h5>
            <button class="btn btn-outline-light btn-sm border-0" type="button" data-bs-toggle="offcanvas" data-bs-target="#mobileSidebar">
                <i class="bi bi-list fs-4"></i>
            </button>
        </div>

        <!-- Offcanvas Sidebar (Untuk HP) -->
        <div class="offcanvas offcanvas-start bg-primary text-white" tabindex="-1" id="mobileSidebar">
            <div class="offcanvas-header border-bottom border-light border-opacity-10">
                <h5 class="offcanvas-title fw-bold"><i class="bi bi-journal-bookmark-fill me-2"></i>Menu</h5>
                <button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas"></button>
            </div>
            <div class="offcanvas-body p-0 pt-3">
                <ul class="nav flex-column">
                    <li><a href="#" class="nav-link text-white active px-4 py-3"><i class="bi bi-speedometer2 me-2"></i> Dashboard</a></li>
                    <li><a href="#tabelData" class="nav-link text-white-50 px-4 py-3" data-bs-dismiss="offcanvas"><i class="bi bi-table me-2"></i> Data Pengunjung</a></li>
                </ul>
            </div>
        </div>

        <!-- ================= KONTEN UTAMA ================= -->
        <div class="main-content p-3 p-md-4">
            
            <!-- Ucapan Selamat Datang -->
            <div class="welcome-card p-4 mb-4">
                <div class="d-flex align-items-center">
                    <div class="flex-shrink-0 bg-primary bg-opacity-10 p-3 rounded-circle text-primary me-4 d-none d-sm-block">
                        <i class="bi bi-building fs-1"></i>
                    </div>
                    <div>
                        <h3 class="fw-bold text-dark mb-1">Selamat Datang di Buku Tamu!</h3>
                        <p class="text-muted mb-0">Sistem informasi pencatatan dan pemantauan kunjungan tamu secara *real-time* di lingkungan SMP Negeri 3 Kerinci.</p>
                    </div>
                </div>
            </div>

            <!-- Tombol Aksi Cepat -->
            <div class="d-flex flex-wrap gap-2 mb-4 justify-content-between align-items-center">
                <button type="button" class="btn btn-primary px-4 py-2 fw-semibold shadow-sm" data-bs-toggle="modal" data-bs-target="#modalTambahTamu">
                    <i class="bi bi-plus-circle me-2"></i>Isi Buku Tamu (Pop-up)
                </button>
                <button id="btnExportPDF" class="btn btn-danger px-4 py-2 fw-semibold shadow-sm" onclick="exportToPDF()" disabled>
                    <i class="bi bi-file-pdf me-2"></i>Cetak Laporan PDF
                </button>
            </div>

            <!-- Area Grafik Kunjungan -->
            <div class="card mb-4">
                <div class="card-header text-primary">
                    <i class="bi bi-bar-chart-fill me-2"></i>Statistik Kunjungan Harian
                </div>
                <div class="card-body">
                    <canvas id="chartKunjungan" height="80"></canvas>
                </div>
            </div>

            <!-- Area Tabel Data -->
            <div id="tabelData" class="card h-100">
                <div class="card-header text-primary">
                    <i class="bi bi-list-columns me-2"></i>Daftar Tamu Terbaru
                </div>
                <div class="card-body p-0">
                    
                    <!-- Loader saat mengambil data -->
                    <div id="tableLoader" class="text-center p-5 text-primary" style="display: none;">
                        <div class="spinner-border" role="status"></div>
                        <p class="mt-2 text-muted">Memuat data dari server...</p>
                    </div>

                    <!-- Tabel -->
                    <div class="table-responsive">
                        <table class="table table-hover table-striped mb-0" id="guestTable" style="display: none;">
                            <thead class="table-light">
                                <tr>
                                    <th scope="col" class="text-center" width="5%">No</th>
                                    <th scope="col" width="15%">Waktu</th>
                                    <th scope="col" width="25%">Nama & Instansi</th>
                                    <th scope="col" width="20%">Kontak</th>
                                    <th scope="col" width="35%">Tujuan</th>
                                </tr>
                            </thead>
                            <tbody id="tableBody">
                                <!-- Data dimuat via JS -->
                            </tbody>
                        </table>
                    </div>
                    
                    <!-- Empty state -->
                    <div id="emptyState" class="empty-state">
                        <i class="bi bi-inboxes"></i>
                        <h5>Belum ada data tamu</h5>
                        <p class="mb-0">Silakan klik tombol "Isi Buku Tamu" di atas.</p>
                    </div>
                </div>
            </div>

        </div> <!-- End Main Content -->
    </div> <!-- End Flex Wrapper -->


    <!-- ================= MODAL POP-UP FORM PENGISIAN ================= -->
    <div class="modal fade" id="modalTambahTamu" tabindex="-1" aria-labelledby="modalLabel" aria-hidden="true">
        <div class="modal-dialog modal-dialog-centered">
            <div class="modal-content border-0 shadow">
                <div class="modal-header bg-primary text-white border-0">
                    <h5 class="modal-title fw-bold" id="modalLabel"><i class="bi bi-pencil-square me-2"></i>Formulir Data Tamu</h5>
                    <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body p-4">
                    <form id="guestForm">
                        <div class="mb-3">
                            <label class="form-label text-muted small fw-bold">NAMA LENGKAP <span class="text-danger">*</span></label>
                            <input type="text" class="form-control" id="namaLengkap" placeholder="Masukkan nama Anda" required autofocus>
                        </div>
                        <div class="mb-3">
                            <label class="form-label text-muted small fw-bold">INSTANSI / ASAL <span class="text-danger">*</span></label>
                            <input type="text" class="form-control" id="instansi" placeholder="Contoh: Dinas Pendidikan / Pribadi" required>
                        </div>
                        <div class="mb-3">
                            <label class="form-label text-muted small fw-bold">NO. TELEPON / WA <span class="text-danger">*</span></label>
                            <input type="tel" class="form-control" id="noTelp" placeholder="Contoh: 08123456789" required>
                        </div>
                        <div class="mb-4">
                            <label class="form-label text-muted small fw-bold">TUJUAN KUNJUNGAN <span class="text-danger">*</span></label>
                            <textarea class="form-control" id="tujuan" rows="3" placeholder="Jelaskan keperluan Anda secara singkat" required></textarea>
                        </div>
                        <div class="d-grid">
                            <button type="submit" id="btnSubmit" class="btn btn-primary py-2 fw-semibold">
                                <i class="bi bi-send me-2"></i>Simpan Data Kunjungan
                            </button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>

    <!-- Bootstrap & SweetAlert JS -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.7.32/dist/sweetalert2.all.min.js"></script>

    <!-- jsPDF & AutoTable -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.31/jspdf.plugin.autotable.min.js"></script>

    <!-- Chart.js untuk Grafik -->
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

    <script>
        let guests = [];
        let chartInstance = null; // Variabel untuk menyimpan instance grafik

        // Referensi DOM
        const guestForm = document.getElementById('guestForm');
        const tableBody = document.getElementById('tableBody');
        const emptyState = document.getElementById('emptyState');
        const guestTable = document.getElementById('guestTable');
        const btnExportPDF = document.getElementById('btnExportPDF');
        const btnSubmit = document.getElementById('btnSubmit');
        const tableLoader = document.getElementById('tableLoader');

        // Fungsi Keamanan: Mencegah Eksekusi Skrip Berbahaya (XSS)
        function escapeHTML(str) {
            if (!str) return '';
            return str.toString().replace(/[&<>'"]/g, function(tag) {
                const charsToReplace = {
                    '&': '&amp;',
                    '<': '&lt;',
                    '>': '&gt;',
                    "'": '&#39;',
                    '"': '&quot;'
                };
                return charsToReplace[tag] || tag;
            });
        }

        // Event agar saat Modal Muncul, input nama langsung fokus (ready to type)
        document.getElementById('modalTambahTamu').addEventListener('shown.bs.modal', function () {
            document.getElementById('namaLengkap').focus();
        });

        // Fungsi Render Grafik (Chart.js)
        function renderChart(dataArray) {
            const ctx = document.getElementById('chartKunjungan').getContext('2d');
            
            // Mengelompokkan data berdasarkan Tanggal (Mengambil 11 karakter pertama dari waktu: "dd MMM yyyy")
            const dateCounts = {};
            dataArray.forEach(guest => {
                const dateOnly = guest.waktu.substring(0, 11);
                dateCounts[dateOnly] = (dateCounts[dateOnly] || 0) + 1;
            });

            // Memisahkan label (tanggal) dan nilai (jumlah)
            const labels = Object.keys(dateCounts);
            const dataPoints = Object.values(dateCounts);

            // Jika grafik sudah ada, hancurkan dulu agar tidak tumpang tindih
            if (chartInstance) {
                chartInstance.destroy();
            }

            // Buat grafik baru
            chartInstance = new Chart(ctx, {
                type: 'bar',
                data: {
                    labels: labels.length > 0 ? labels : ['Belum ada data'],
                    datasets: [{
                        label: 'Jumlah Kunjungan',
                        data: dataPoints.length > 0 ? dataPoints : [0],
                        backgroundColor: 'rgba(13, 110, 253, 0.7)',
                        borderColor: 'rgba(13, 110, 253, 1)',
                        borderWidth: 1,
                        borderRadius: 4
                    }]
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    scales: {
                        y: { 
                            beginAtZero: true,
                            ticks: { stepSize: 1 } // Agar sumbu Y bernilai bulat (1, 2, 3 kunjungan)
                        }
                    },
                    plugins: {
                        legend: { display: false }
                    }
                }
            });
        }

        // Render Data ke Tabel HTML
        function renderTable(dataArray) {
            if (dataArray) { guests = dataArray; }
            
            tableLoader.style.display = 'none';
            tableBody.innerHTML = ''; 

            if (guests.length === 0) {
                emptyState.style.display = 'block';
                guestTable.style.display = 'none';
                btnExportPDF.disabled = true;
                renderChart([]); // Render grafik kosong
                return;
            }

            emptyState.style.display = 'none';
            guestTable.style.display = 'table';
            btnExportPDF.disabled = false;

            // Render Grafik
            renderChart(guests);

            // Membalik array agar data terbaru di atas (di tabel saja, di GS tetap urut)
            const reversedGuests = [...guests].reverse();

            reversedGuests.forEach((guest, index) => {
                // Terapkan fungsi keamanan agar tidak ada skrip yang jalan saat dirender
                const safeWaktu = escapeHTML(guest.waktu);
                const safeNama = escapeHTML(guest.nama);
                const safeInstansi = escapeHTML(guest.instansi);
                const safeTelp = escapeHTML(guest.telp);
                const safeTujuan = escapeHTML(guest.tujuan);

                const tr = document.createElement('tr');
                tr.innerHTML = `
                    <td class="text-center">${guests.length - index}</td>
                    <td><span class="badge bg-light text-dark border"><i class="bi bi-clock me-1"></i>${safeWaktu}</span></td>
                    <td>
                        <strong class="text-primary">${safeNama}</strong><br>
                        <small class="text-muted"><i class="bi bi-building me-1"></i>${safeInstansi}</small>
                    </td>
                    <td>
                        <a href="https://wa.me/${encodeURIComponent(safeTelp.replace(/^0/, '62'))}" target="_blank" class="btn btn-sm btn-outline-success rounded-pill py-0 px-2">
                            <i class="bi bi-whatsapp"></i> ${safeTelp}
                        </a>
                    </td>
                    <td><small>${safeTujuan}</small></td>
                `;
                tableBody.appendChild(tr);
            });
        }

        // Ambil data pertama kali
        function loadInitialData() {
            tableLoader.style.display = 'block';
            emptyState.style.display = 'none';
            guestTable.style.display = 'none';

            if (typeof google !== 'undefined' && google.script) {
                google.script.run
                    .withSuccessHandler(renderTable)
                    .withFailureHandler(() => {
                        tableLoader.style.display = 'none';
                        Swal.fire('Error', 'Gagal memuat data dari server.', 'error');
                    })
                    .getLatestData();
            } else {
                // Fallback Lokal (Preview)
                setTimeout(() => { renderTable(); }, 500);
            }
        }

        // Saat Form Disubmit
        guestForm.addEventListener('submit', function(e) {
            e.preventDefault();

            const namaLengkap = document.getElementById('namaLengkap').value.trim();
            const instansi = document.getElementById('instansi').value.trim();
            const noTelp = document.getElementById('noTelp').value.trim();
            const tujuan = document.getElementById('tujuan').value.trim();

            const originalBtnHtml = btnSubmit.innerHTML;
            btnSubmit.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Menyimpan...';
            btnSubmit.disabled = true;

            if (typeof google !== 'undefined' && google.script) {
                google.script.run
                    .withSuccessHandler(function(updatedData) {
                        handleSuccess(updatedData, originalBtnHtml);
                    })
                    .withFailureHandler(function() {
                        btnSubmit.innerHTML = originalBtnHtml;
                        btnSubmit.disabled = false;
                        Swal.fire('Error!', 'Terjadi kesalahan saat menyimpan data.', 'error');
                    })
                    .saveData(namaLengkap, instansi, noTelp, tujuan);
            } else {
                // Fallback Lokal (Preview)
                const now = new Date();
                const waktuFormat = now.toLocaleDateString('id-ID', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' });
                guests.push({ waktu: waktuFormat, nama: namaLengkap, instansi: instansi, telp: noTelp, tujuan: tujuan });
                
                setTimeout(() => { handleSuccess(guests, originalBtnHtml); }, 800);
            }
        });

        // Fungsi bantuan setelah sukses simpan
        function handleSuccess(updatedData, originalBtnHtml) {
            renderTable(updatedData);
            guestForm.reset();
            btnSubmit.innerHTML = originalBtnHtml;
            btnSubmit.disabled = false;
            
            // Tutup Modal secara otomatis
            const modalElement = document.getElementById('modalTambahTamu');
            const modalInstance = bootstrap.Modal.getInstance(modalElement);
            if(modalInstance) { modalInstance.hide(); }

            Swal.fire({ icon: 'success', title: 'Tersimpan!', text: 'Data tamu berhasil ditambahkan.', timer: 2000, showConfirmButton: false });
        }

        // Ekspor ke PDF
        function exportToPDF() {
            if (guests.length === 0) return;
            const { jsPDF } = window.jspdf;
            const doc = new jsPDF({ orientation: 'landscape' });

            doc.setFontSize(18);
            doc.text("Laporan Buku Tamu SMP Negeri 3 Kerinci", 14, 22);
            doc.setFontSize(11);
            doc.setTextColor(100);
            const printDate = new Date().toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
            doc.text(`Dicetak pada: ${printDate}`, 14, 30);

            const tableColumn = ["No", "Waktu Kunjungan", "Nama Lengkap", "Instansi/Asal", "No. Telepon", "Tujuan Kunjungan"];
            const tableRows = [];
            guests.forEach((guest, index) => {
                tableRows.push([ index + 1, guest.waktu, guest.nama, guest.instansi, guest.telp, guest.tujuan ]);
            });

            doc.autoTable({
                head: [tableColumn], body: tableRows, startY: 36, theme: 'grid',
                styles: { font: 'helvetica', fontSize: 10, cellPadding: 4 },
                headStyles: { fillColor: [13, 110, 253], textColor: 255 },
                alternateRowStyles: { fillColor: [248, 249, 250] }
            });
            doc.save('Laporan_Kunjungan_SMPN3Kerinci.pdf');
        }

        // Mulai memuat
        document.addEventListener('DOMContentLoaded', loadInitialData);
    </script>
</body>
</html>