Senin, 06 April 2026

Prompt Buku Tamu

 







/**
 * Konfigurasi Aplikasi
 * Pastikan ID Folder dan ID Template sudah benar.
 */
const CONFIG = {
  FOLDER_ID: '1S02hUM3cdjE-9Oc8nr-8FOh8nw_bCusB',
  TEMPLATE_ID: '1JLAQNVQupESFzfc1QAG01O3-rNS0bYxO_ZVC05Yr9xE',
  SHEET_NAME: 'DataTamu'
};


PROMPT

1. Saya seorang programmer google apps script. Buatkan spesifikasi aplikasi pencatatan buku tamu. Di dalamnya ada landing page, dan juga upload foto yang diupload ke google drive. Kemudian ada dashboard dan pencetakan file pdf.

2. Buatkan prompt untuk membuat aplikasi sesuai dengan spesifikasi tersebut

Prompt: Pembuatan Aplikasi Buku Tamu Digital (GAS)

Konteks & Peran

Bertindaklah sebagai Senior Full-stack Developer spesialis Google Apps Script (GAS). Bangun aplikasi Buku Tamu Digital yang terintegrasi dengan Google Sheets, Drive, dan Docs.

Arsitektur & Fitur Utama

  1. Frontend (SPA):

    • Gunakan Bootstrap 5 untuk UI yang modern dan responsif.

    • Landing Page: Formulir input (Nama, Instansi, Tujuan, Staff) dan fitur Upload Foto.

    • Client-side Optimization: Gunakan elemen <canvas> untuk melakukan resize dan kompresi gambar (maks 800px, quality 0.7) sebelum dikirim sebagai Base64 guna menghindari limitasi payload GAS.

    • Dashboard Admin: Tabel interaktif untuk melihat riwayat tamu secara real-time dengan tombol "Cetak PDF".

  2. Backend (Code.gs):

    • Database: Simpan data teks ke Google Sheets (ID, Timestamp, Nama, Instansi, Tujuan, Staff, URL Foto).

    • Penyimpanan: Unggah foto Base64 ke folder Google Drive spesifik dan atur izin aksesnya agar dapat dilihat melalui link.

    • PDF Engine: * Gunakan Google Docs sebagai template.

      • Lakukan cloning template, ganti placeholder teks ({{nama}}, {{instansi}}, dll).

      • Penyisipan Foto Dinamis: Cari placeholder {{foto}} di dalam dokumen, ambil file dari Drive, dan masukkan sebagai Inline Image dengan ukuran proporsional (lebar 150-180px).

      • Konversi hasil akhir ke PDF, simpan di Drive, dan hapus file dokumen sementara.

  3. Standar Kode:

    • Gunakan objek CONFIG untuk ID Folder, ID Template, dan Nama Sheet di bagian atas kode.

    • Komunikasi client-server wajib menggunakan google.script.run dengan withSuccessHandler.

    • Terapkan error handling (try-catch) pada setiap fungsi krusial di server-side.

Spesifikasi Template Google Doc

Buat template dokumen dengan tabel yang berisi placeholder berikut:

  • {{nama}}, {{instansi}}, {{tujuan}}, {{staff}}, {{tanggal}} untuk data teks.

  • {{foto}} diletakkan di dalam sel tabel untuk posisi foto yang konsisten.

Code.gs

/**
 * Konfigurasi Aplikasi
 * Pastikan ID Folder dan ID Template sudah benar.
 */
const CONFIG = {
  FOLDER_ID: '1S02hUM3cdjE-9Oc8nr-8FOh8nw_bCusB',
  TEMPLATE_ID: '1JLAQNVQupESFzfc1QAG01O3-rNS0bYxO_ZVC05Yr9xE',
  SHEET_NAME: 'DataTamu'
};

function doGet(e) {
  return HtmlService.createHtmlOutputFromFile('index')
    .setTitle('Buku Tamu Digital')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
    .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

/**
 * Memproses pendaftaran tamu (Teks & Foto)
 */
function processForm(data) {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    let sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
   
    if (!sheet) {
      sheet = ss.insertSheet(CONFIG.SHEET_NAME);
      sheet.appendRow(["ID", "Timestamp", "Nama", "Instansi", "Tujuan", "Staff", "Foto URL"]);
    }

    const id = Utilities.getUuid();
    const timestamp = new Date();
   
    // Upload Foto
    let photoUrl = "";
    if (data.photoBase64) {
      photoUrl = uploadPhoto(data.photoBase64, `Tamu_${data.nama}_${id}`);
    }

    // Simpan ke Sheets
    sheet.appendRow([id, timestamp, data.nama, data.instansi, data.tujuan, data.staff, photoUrl]);

    return { success: true, message: "Pendaftaran berhasil!" };
  } catch (e) {
    return { success: false, message: e.toString() };
  }
}

/**
 * Upload Base64 ke Drive
 */
function uploadPhoto(base64Data, fileName) {
  const folder = DriveApp.getFolderById(CONFIG.FOLDER_ID);
  const contentType = base64Data.substring(5, base64Data.indexOf(';'));
  const bytes = Utilities.base64Decode(base64Data.split(',')[1]);
  const blob = Utilities.newBlob(bytes, contentType, fileName + ".jpg");
  const file = folder.createFile(blob);
  file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
  return file.getUrl();
}

/**
 * Ambil data untuk Dashboard
 */
function getGuestData() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG.SHEET_NAME);
  if (!sheet) return [];
  const data = sheet.getDataRange().getDisplayValues();
  const headers = data.shift();
  return data.map(row => {
    let obj = {};
    headers.forEach((h, i) => obj[h] = row[i]);
    return obj;
  }).reverse();
}

/**
 * Membuat PDF dengan Foto Dinamis
 */
function generatePDF(rowId) {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
    const data = sheet.getDataRange().getValues();
    const headers = data[0];
    const rowIndex = data.findIndex(row => row[0] === rowId);
   
    if (rowIndex === -1) throw new Error("Data tidak ditemukan");
   
    const guest = {};
    headers.forEach((h, i) => guest[h] = data[rowIndex][i]);

    // 1. Copy Template Doc
    const templateFile = DriveApp.getFileById(CONFIG.TEMPLATE_ID);
    const tempFile = templateFile.makeCopy(`Laporan_${guest.Nama}`);
    const doc = DocumentApp.openById(tempFile.getId());
    const body = doc.getBody();

    // 2. Ganti Placeholder Teks
    body.replaceText('{{nama}}', guest.Nama);
    body.replaceText('{{instansi}}', guest.Instansi);
    body.replaceText('{{tujuan}}', guest.Tujuan);
    body.replaceText('{{staff}}', guest.Staff);
    body.replaceText('{{tanggal}}', Utilities.formatDate(new Date(guest.Timestamp), "GMT+7", "dd/MM/yyyy HH:mm"));

    // 3. Sisipkan Foto
    const photoUrl = guest["Foto URL"];
    if (photoUrl) {
      const fileId = photoUrl.match(/[-\w]{25,}/);
      if (fileId) {
        try {
          const imageBlob = DriveApp.getFileById(fileId[0]).getBlob();
          const placeholder = body.findText('{{foto}}');
         
          if (placeholder) {
            const element = placeholder.getElement();
            const parent = element.getParent();
            const image = parent.asParagraph().appendInlineImage(imageBlob);
           
            // Atur ukuran foto (Lebar 150px)
            const width = 150;
            const height = (image.getHeight() / image.getWidth()) * width;
            image.setWidth(width).setHeight(height);
           
            // Hapus tag {{foto}}
            body.replaceText('{{foto}}', '');
          }
        } catch (e) {
          body.replaceText('{{foto}}', '(Gagal memuat foto)');
        }
      }
    } else {
      body.replaceText('{{foto}}', '(Tidak ada foto)');
    }

    doc.saveAndClose();

    // 4. Konversi ke PDF
    const pdfBlob = tempFile.getAs('application/pdf');
    const folder = DriveApp.getFolderById(CONFIG.FOLDER_ID);
    const pdfFile = folder.createFile(pdfBlob).setName(`Laporan_Tamu_${guest.Nama}.pdf`);
   
    // 5. Cleanup
    tempFile.setTrashed(true);
   
    return { success: true, url: pdfFile.getDownloadUrl().replace("?e=download", "") };
  } catch (e) {
    return { success: false, message: e.toString() };
  }
}

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 Digital</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
  <style>
    :root {
      --primary-color: #1a73e8;
      --bg-color: #f8f9fa;
    }
    body { background-color: var(--bg-color); font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
    .card { border-radius: 15px; border: none; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
    .btn-primary { background-color: var(--primary-color); border: none; padding: 10px 25px; border-radius: 8px; }
    #canvas { display: none; }
    #photo-preview { width: 100%; border-radius: 12px; display: none; margin-top: 15px; border: 1px solid #ddd; }
    .nav-custom { background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin-bottom: 30px; }
    .loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.8); display: none; align-items: center; justify-content: center; z-index: 9999; }
  </style>
</head>
<body>

<div class="loading-overlay" id="loading">
  <div class="spinner-border text-primary" role="status"></div>
  <span class="ms-2">Mohon tunggu...</span>
</div>

<nav class="navbar navbar-expand-lg nav-custom">
  <div class="container">
    <a class="navbar-brand fw-bold text-primary" href="#" onclick="showView('landing')">GUESTBOOK</a>
    <button class="btn btn-outline-primary btn-sm" onclick="showView('dashboard')">Admin Dashboard</button>
  </div>
</nav>

<div class="container mb-5">
  <!-- View: Landing Page / Form -->
  <div id="view-landing">
    <div class="row justify-content-center">
      <div class="col-md-6">
        <div class="card p-4">
          <h3 class="text-center mb-4 fw-bold">Selamat Datang</h3>
          <p class="text-muted text-center mb-4">Silakan isi data kunjungan Anda</p>
         
          <form id="guestForm">
            <div class="mb-3">
              <label class="form-label">Nama Lengkap</label>
              <input type="text" class="form-control" name="nama" required placeholder="Contoh: Budi Santoso">
            </div>
            <div class="mb-3">
              <label class="form-label">Instansi / Perusahaan</label>
              <input type="text" class="form-control" name="instansi" required placeholder="Contoh: PT. Maju Jaya">
            </div>
            <div class="mb-3">
              <label class="form-label">Tujuan Kunjungan</label>
              <input type="text" class="form-control" name="tujuan" required placeholder="Contoh: Meeting Koordinasi">
            </div>
            <div class="mb-3">
              <label class="form-label">Staff yang Ditemui</label>
              <input type="text" class="form-control" name="staff" required placeholder="Contoh: Ibu Siti (HRD)">
            </div>

            <div class="mb-4">
              <label class="form-label">Upload Foto Identitas / Tamu</label>
              <input type="file" id="photo-input" accept="image/*" class="form-control" required>
              <canvas id="canvas"></canvas>
              <img id="photo-preview" src="" alt="Pratinjau Foto">
              <div id="file-help" class="form-text">Pilih file gambar (JPG/PNG). Foto akan dikompres secara otomatis.</div>
            </div>

            <button type="submit" class="btn btn-primary w-100">Kirim Data</button>
          </form>
        </div>
      </div>
    </div>
  </div>

  <!-- View: Dashboard -->
  <div id="view-dashboard" style="display:none;">
    <div class="card p-4">
      <div class="d-flex justify-content-between align-items-center mb-4">
        <h3 class="fw-bold mb-0">Riwayat Tamu</h3>
        <button class="btn btn-sm btn-outline-secondary" onclick="loadDashboardData()">Refresh Data</button>
      </div>
      <div class="table-responsive">
        <table class="table table-hover">
          <thead class="table-light">
            <tr>
              <th>Waktu</th>
              <th>Nama</th>
              <th>Instansi</th>
              <th>Tujuan</th>
              <th>Aksi</th>
            </tr>
          </thead>
          <tbody id="dashboard-body">
            <!-- Data dimuat di sini -->
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>

<script>
  let photoBase64 = "";
  const canvas = document.getElementById('canvas');
  const photoPreview = document.getElementById('photo-preview');
  const photoInput = document.getElementById('photo-input');
  const loading = document.getElementById('loading');

  // --- Navigasi View ---
  function showView(view) {
    document.getElementById('view-landing').style.display = view === 'landing' ? 'block' : 'none';
    document.getElementById('view-dashboard').style.display = view === 'dashboard' ? 'block' : 'none';
    if(view === 'dashboard') loadDashboardData();
  }

  // --- Logika Unggah File ---
  photoInput.addEventListener('change', function(e) {
    const file = e.target.files[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = function(event) {
      const img = new Image();
      img.onload = function() {
        // Logika Resize untuk menjaga ukuran file tetap aman di GAS (Max 800px)
        const maxWidth = 800;
        const maxHeight = 800;
        let width = img.width;
        let height = img.height;

        if (width > height) {
          if (width > maxWidth) {
            height *= maxWidth / width;
            width = maxWidth;
          }
        } else {
          if (height > maxHeight) {
            width *= maxHeight / height;
            height = maxHeight;
          }
        }

        canvas.width = width;
        canvas.height = height;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0, width, height);
       
        // Kompresi kualitas menjadi 0.7
        photoBase64 = canvas.toDataURL('image/jpeg', 0.7);
       
        photoPreview.src = photoBase64;
        photoPreview.style.display = "block";
      };
      img.src = event.target.result;
    };
    reader.readAsDataURL(file);
  });

  // --- Logika Formulir ---
  document.getElementById('guestForm').addEventListener('submit', function(e) {
    e.preventDefault();
    if(!photoBase64) return alert("Silakan pilih foto terlebih dahulu.");
   
    loading.style.display = "flex";
    const formData = {
      nama: this.nama.value,
      instansi: this.instansi.value,
      tujuan: this.tujuan.value,
      staff: this.staff.value,
      photoBase64: photoBase64
    };

    google.script.run
      .withSuccessHandler(res => {
        loading.style.display = "none";
        if(res.success) {
          alert("Terima kasih! Data Anda telah tersimpan.");
          this.reset();
          photoPreview.style.display = "none";
          photoBase64 = "";
        } else {
          alert("Error: " + res.message);
        }
      })
      .processForm(formData);
  });

  // --- Logika Dashboard ---
  function loadDashboardData() {
    loading.style.display = "flex";
    google.script.run
      .withSuccessHandler(data => {
        const tbody = document.getElementById('dashboard-body');
        tbody.innerHTML = "";
        data.forEach(row => {
          tbody.innerHTML += `
            <tr>
              <td><small>${row.Timestamp}</small></td>
              <td><strong>${row.Nama}</strong></td>
              <td>${row.Instansi}</td>
              <td>${row.Tujuan}</td>
              <td>
                <button class="btn btn-sm btn-primary" onclick="printPDF('${row.ID}')">Cetak PDF</button>
              </td>
            </tr>
          `;
        });
        loading.style.display = "none";
      })
      .getGuestData();
  }

  function printPDF(id) {
    loading.style.display = "flex";
    google.script.run
      .withSuccessHandler(res => {
        loading.style.display = "none";
        if(res.success) {
          window.open(res.url, '_blank');
        } else {
          alert("Gagal cetak: " + res.message);
        }
      })
      .generatePDF(id);
  }
</script>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Doc Template

LAPORAN KUNJUNGAN TAMU

Program Digital Skills Kerjasama ITS - UNICEF

DATA PENGUNJUNG

FOTO IDENTITAS / TAMU

Nama Lengkap:

{{nama}}


Instansi / Perusahaan:

{{instansi}}


Tujuan Kunjungan:

{{tujuan}}

{{foto}}

Staff yang Ditemui:

{{staff}}


Waktu Kedatangan:

{{tanggal}}


Catatan Keamanan & Tata Tertib:

  1. Tamu wajib mengenakan kartu identitas selama berada di area kantor.

  2. Mohon kembalikan dokumen ini atau lapor ke resepsionis saat akan meninggalkan gedung.

  3. Selalu patuhi protokol keselamatan yang berlaku.

Petugas Penerima Tamu, {{tanggal}}

(_______________________)


Tidak ada komentar:

Posting Komentar

Web Technology

  Front End https://www.geeksforgeeks.org/web-tech/web-technology/ Back End https://www.geeksforgeeks.org/web-tech/web-technology/