Undangan dan Google Kalender

Sistem Otomatisasi Penjadwalan, Pengiriman Undangan & Pembaruan Google Calendar

Modul 1

Langkah 1: Membuat Google Sheets Baru & Mengatur Tab

Ikuti langkah dasar berikut dari nol untuk membuat lembar kerja Google Sheets:

  1. Buka browser dan akses sheets.new untuk membuat Google Sheet baru.
  2. Beri judul dokumen di bagian kiri atas, misalnya: Sistem Undangan dan Google Kalender.
  3. Ganti Nama Tab Sheet: Klik kanan tab di pojok kiri bawah (yang awalnya bernama Sheet1), lalu pilih Ganti Nama (Rename) menjadi Jadwal (harus persis diawali huruf kapital J, tanpa spasi tambahan).
  4. Isi Baris 1 (Header) dari Kolom A sampai I dengan judul berikut:
Kolom Nama Header Keterangan / Contoh Isi
AJudulJudul agenda (Contoh: Rapat Uji Coba)
BMulaiTanggal & waktu mulai (Format: YYYY-MM-DD HH:MM atau DD/MM/YYYY HH:MM)
CSelesaiTanggal & waktu selesai (Format: YYYY-MM-DD HH:MM atau DD/MM/YYYY HH:MM)
DLokasiNama tempat (Contoh: Gedung Serbaguna Disnaker Belitung Timur)
ELink Google MapsURL titik Google Maps lokasi (Contoh: https://maps.app.goo.gl/xxx)
FDeskripsiRincian pembahasan rapat
GDaftar PesertaNama & Email (Contoh: Sophia <sophia@gmail.com>, Budi <budi@gmail.com>)
HStatusDropdown: Baru, Terkirim, Update, Batal
IEvent IDID otomatis dari Google Calendar untuk sinkronisasi
📌 Pentingnya Nama Tab 'Jadwal':
Nama tab wajib persis Jadwal karena kode Google Apps Script mencari variabel const SHEET_NAME = 'Jadwal';.
Modul 2

Langkah 2: Pengaturan Format & Cara Membuat Dropdown Status

Lakukan konfigurasi berikut agar waktu pelaksanaan rapat tidak bergeser dan status dapat dipilih dengan dropdown:

  1. Format Tanggal & Waktu (Kolom B & C):
    • Blok seluruh kolom B dan C (klik huruf B lalu tahan Shift dan klik huruf C).
    • Klik menu Format → Angka → Tanggal dan waktu.
  2. Cara Membuat Dropdown Status (Kolom H):
    • Klik pada sel H2 terlebih dahulu.
    • Tekan kombinasi tombol Ctrl + Shift + Panah Bawah (atau Cmd + Shift + Panah Bawah di Mac) pada keyboard untuk menyeleksi/blok seluruh sel ke bawah dari H2 hingga baris terakhir secara cepat.
    • Klik menu Data → Validasi data → Klik tombol + Tambahkan aturan.
    • Pada bagian Kriteria, pilih Dropdown.
    • Masukkan 4 opsi status berikut secara berurutan:
      • Baru
      • Terkirim
      • Update
      • Batal
    • Klik Selesai. Kolom H sekarang telah memiliki tombol dropdown siap pakai.
  3. Setelan Zona Waktu Google Sheets:
    • Klik menu File → Setelan.
    • Ubah zona waktu ke (GMT+07:00) Jakarta (WIB) atau zona waktu daerah Anda. Klik Simpan setelan.
  4. Setelan Zona Waktu Google Apps Script:
    • Di Apps Script, klik ikon gerigi ⚙️ Project Settings di menu sebelah kiri.
    • Pastikan Time zone sudah diatur ke Asia/Jakarta.
Modul 3

Langkah 3: Pemasangan Layanan Calendar API & Kode Google Apps Script (`Kode.gs`)

Hapus semua kode bawaan lalu tempelkan kode utuh berikut:

/**
 * ==============================================================================
 * PROJECT: UNDANGAN OFFLINE (PERSONAL MAIL MERGE & LAMPIRAN KALENDER)
 * ==============================================================================
 */

const SHEET_NAME = 'Jadwal';

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('⚡ Menu Jadwal')
    .addItem('▶️ Proses Sekarang', 'prosesJadwal')
    .addItem('⏰ Aktifkan Otomatis (Trigger)', 'pasangTriggerOtomatis')
    .addToUi();
}

function getSheet() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sh = ss.getSheetByName(SHEET_NAME);
  if (!sh) {
    throw new Error('Tab "' + SHEET_NAME + '" tidak ditemukan!');
  }
  return sh;
}

function parseParticipants(rawText) {
  if (!rawText) return [];
  const items = String(rawText).split(',');
  const list = [];
  
  items.forEach(item => {
    let text = item.trim();
    if (!text) return;
    
    const match = text.match(/^(.*?)\s*<([^>]+)>$/);
    if (match) {
      list.push({
        name: match[1].trim(),
        email: match[2].trim().toLowerCase()
      });
    } else {
      list.push({
        name: '',
        email: text.toLowerCase()
      });
    }
  });
  
  return list;
}

/**
 * Format tanggal ke standar iCalendar UTC (YYYYMMDDTHHMMSSZ)
 */
function formatDateToICS(date) {
  return date.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
}

/**
 * Fungsi utama untuk memproses seluruh baris secara personal
 */
function prosesJadwal() {
  // Mencegah tombol "Proses Sekarang" dan trigger berjalan bersamaan.
  const lock = LockService.getScriptLock();
  if (!lock.tryLock(1000)) return;

  try {
    const sh = getSheet();
    const lastRow = sh.getLastRow();
    
    if (lastRow < 2) return;

    const range = sh.getRange(2, 1, lastRow - 1, 9);
    const rows = range.getValues();
    
    for (let i = 0; i < rows.length; i++) {
      const [judul, mulai, selesai, lokasi, linkMaps, deskripsi, rawPeserta, status, rawEventId] = rows[i];
      const rowNum = i + 2;
      
      if (!judul) continue;
      if (status === 'Terkirim' || status === 'Dibatalkan') continue;
      
      try {
        const pesertaList = parseParticipants(rawPeserta);
        if (pesertaList.length === 0) continue;

        const startDate = new Date(mulai);
        const endDate = new Date(selesai);
        const dtStart = formatDateToICS(startDate);
        const dtEnd = formatDateToICS(endDate);
        const dtStamp = formatDateToICS(new Date());

        // URL Maps murni untuk lokasi
        let cleanLocation = (linkMaps && String(linkMaps).trim() !== '') ? String(linkMaps).trim() : String(lokasi).trim();

        let eventMap = {};
        if (rawEventId) {
          try { eventMap = JSON.parse(rawEventId); } catch (e) { eventMap = {}; }
        }

        if (status === '' || status === 'Baru' || status === 'Update') {
          let newEventMap = {};

          pesertaList.forEach((peserta, idx) => {
            if (!peserta.email) return;

            // Buat UID unik untuk setiap peserta/acara
            const uid = eventMap[peserta.email] || ("INV-" + Date.now() + "-" + idx + "@undangan");
            const sapaan = peserta.name ? ("Halo " + peserta.name + ",") : "Kepada Yth. Peserta Rapat,";

            const bodyText = sapaan + "\n\n" +
              "Anda diundang untuk menghadiri pertemuan (Offline) dengan rincian sebagai berikut:\n\n" +
              (deskripsi || '') + "\n\n" +
              "----------------------------------------\n" +
              "Nama Tempat: " + (lokasi || '-') + "\n" +
              "Maps: " + (linkMaps || '-') + "\n" +
              "----------------------------------------\n\n" +
              "Silakan buka lampiran undangan kalender untuk menambahkan acara ini ke kalender pribadi Anda.";

            const icsContent = 
              "BEGIN:VCALENDAR\r\n" +
              "PRODID:-//Sistem Undangan Offline//ID\r\n" +
              "VERSION:2.0\r\n" +
              "METHOD:REQUEST\r\n" +
              "BEGIN:VEVENT\r\n" +
              "UID:" + uid + "\r\n" +
              "SEQUENCE:0\r\n" +
              "STATUS:CONFIRMED\r\n" +
              "DTSTAMP:" + dtStamp + "\r\n" +
              "DTSTART:" + dtStart + "\r\n" +
              "DTEND:" + dtEnd + "\r\n" +
              "SUMMARY:" + judul + "\r\n" +
              "DESCRIPTION:" + bodyText.replace(/\n/g, "\\n") + "\r\n" +
              "LOCATION:" + cleanLocation + "\r\n" +
              "ORGANIZER;CN=Panitia:mailto:" + Session.getActiveUser().getEmail() + "\r\n" +
              "ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=" + (peserta.name || peserta.email) + ":mailto:" + peserta.email + "\r\n" +
              "END:VEVENT\r\n" +
              "END:VCALENDAR";

            const icsBlob = Utilities.newBlob(icsContent, "application/ics", "invite.ics");

            // Kirim email undangan dengan lampiran iCalendar resmi
            GmailApp.sendEmail(peserta.email, judul, bodyText, {
              htmlBody: bodyText.replace(/\n/g, "
"), attachments: [icsBlob] }); newEventMap[peserta.email] = uid; }); sh.getRange(rowNum, 8, 1, 2).setValues([['Terkirim', JSON.stringify(newEventMap)]]); } else if (status === 'Batal') { pesertaList.forEach(peserta => { if (!peserta.email) return; const uid = eventMap[peserta.email]; if (!uid) return; const cancelIcs = "BEGIN:VCALENDAR\r\n" + "METHOD:CANCEL\r\n" + "PRODID:-//Sistem Undangan Offline//ID\r\n" + "VERSION:2.0\r\n" + "BEGIN:VEVENT\r\n" + "UID:" + uid + "\r\n" + "DTSTAMP:" + dtStamp + "\r\n" + "SUMMARY:DIBATALKAN: " + judul + "\r\n" + "STATUS:CANCELLED\r\n" + "ORGANIZER;CN=Panitia:mailto:" + Session.getActiveUser().getEmail() + "\r\n" + "ATTENDEE;CN=" + peserta.email + ":mailto:" + peserta.email + "\r\n" + "END:VEVENT\r\n" + "END:VCALENDAR"; const icsBlob = Utilities.newBlob(cancelIcs, "application/ics", "cancel.ics"); GmailApp.sendEmail(peserta.email, "DIBATALKAN: " + judul, "Acara berikut telah dibatalkan oleh panitia.", { attachments: [icsBlob] }); }); sh.getRange(rowNum, 8).setValue('Dibatalkan'); } } catch (err) { sh.getRange(rowNum, 8).setValue('Error: ' + err.message); } } } finally { lock.releaseLock(); } } function pasangTriggerOtomatis() { ScriptApp.getProjectTriggers().forEach(t => ScriptApp.deleteTrigger(t)); ScriptApp.newTrigger('prosesJadwal').timeBased().everyMinutes(10).create(); SpreadsheetApp.getUi().alert('✅ Otomatisasi Aktif!\nJadwal akan diperiksa dan dikirim otomatis setiap 10 menit.'); }

⚠️ PERINGATAN PENTING SAAT PRAKTIK

1. Etika Pengisian Email Tujuan:
Setiap kali script dijalankan, undangan email resmi dikirimkan dari akun Google pengirim. Dilarang mengisikan alamat email orang asing secara sembarangan saat latihan/praktik karena dapat mengganggu penerima dan dikategorikan sebagai Spam.

2. Akun Google Sheets Adalah Pengirim Resmi (Sender):
Akun tempat Google Sheet dibuat akan menjadi pengirim resmi notifikasi email. Saat latihan, gunakan alamat email pribadi Anda sendiri atau rekan sekelompok Anda.

Modul 4

Langkah 4: Otorisasi & Pengujian Sistem Penuh

  1. Simpan kode di Apps Script dengan menekan tombol Simpan (Ctrl + S).
  2. Kembali ke tab Google Sheets Anda, lalu lakukan Refresh / Reload (F5) pada browser.
  3. Tunggu hingga menu kustom ⚡ Menu Jadwal muncul di baris atas menu Google Sheets.
  4. Klik ⚡ Menu Jadwal → ▶️ Proses Sekarang.
  5. Lakukan Otorisasi Izin Aplikasi (Authorization Required):
    • Klik Lanjutkan → Pilih Akun Google Anda.
    • Klik Lanjutan (Advanced) di pojok kiri bawah dialog.
    • Klik Buka Project (tidak aman) / Go to project.
    • Klik Izinkan (Allow).
  6. Setelah otorisasi selesai, klik ⚡ Menu Jadwal → ⏰ Aktifkan Otomatis (Trigger) agar sistem memeriksa dan memproses seluruh baris jadwal baru setiap 10 menit.