Sistem Otomatisasi Penjadwalan, Pengiriman Undangan & Pembaruan Google Calendar
Ikuti langkah dasar berikut dari nol untuk membuat lembar kerja Google Sheets:
Jadwal (harus persis diawali huruf kapital J, tanpa spasi tambahan).| Kolom | Nama Header | Keterangan / Contoh Isi |
|---|---|---|
| A | Judul | Judul agenda (Contoh: Rapat Uji Coba) |
| B | Mulai | Tanggal & waktu mulai (Format: YYYY-MM-DD HH:MM atau DD/MM/YYYY HH:MM) |
| C | Selesai | Tanggal & waktu selesai (Format: YYYY-MM-DD HH:MM atau DD/MM/YYYY HH:MM) |
| D | Lokasi | Nama tempat (Contoh: Gedung Serbaguna Disnaker Belitung Timur) |
| E | Link Google Maps | URL titik Google Maps lokasi (Contoh: https://maps.app.goo.gl/xxx) |
| F | Deskripsi | Rincian pembahasan rapat |
| G | Daftar Peserta | Nama & Email (Contoh: Sophia <sophia@gmail.com>, Budi <budi@gmail.com>) |
| H | Status | Dropdown: Baru, Terkirim, Update, Batal |
| I | Event ID | ID otomatis dari Google Calendar untuk sinkronisasi |
Jadwal karena kode Google Apps Script mencari variabel const SHEET_NAME = 'Jadwal';.
Lakukan konfigurasi berikut agar waktu pelaksanaan rapat tidak bergeser dan status dapat dipilih dengan dropdown:
Baru
Terkirim
Update
Batal
Asia/Jakarta.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.');
}
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.
Ctrl + S).