Cara Membuat Sistem Booking Ruang Anti Bentrok dengan Google Apps Script dan LockService
Rebutan ruang rapat adalah drama klasik kantor: dua tim datang ke ruangan yang sama karena sama-sama "sudah bilang ke admin". Grup WhatsApp atau Google Calendar bersama sering tidak cukup, karena tidak ada yang benar-benar mencegah dua orang memesan jam yang sama.
Di tutorial ini kita membuat inti sistem booking ruang dengan Google Apps Script yang menolak bentrok di server, termasuk ketika dua orang menekan tombol pesan pada detik yang sama. Contohnya diambil dari aplikasi Booking Ruang Rapat yang bisa kamu download gratis di Papicode (link di akhir artikel).
1. Simpan jam sebagai teks "HH:mm"
Google Sheets suka mengubah 09:30 menjadi nilai waktu, dan 2026-10-20 menjadi tanggal. Hasilnya, saat dibaca dari Apps Script kamu mendapat objek Date dengan zona waktu yang membingungkan. Paksa kolomnya menjadi teks sebelum menulis:
var sh = SpreadsheetApp.getActive().getSheetByName('Booking');
sh.getRange('A:J').setNumberFormat('@'); // ID, tanggal, jam mulai/selesai, dst sebagai teks
Lalu ubah jam menjadi menit sejak tengah malam supaya mudah dibandingkan:
function menit_(hhmm) {
var m = String(hhmm || '').match(/^(\d{1,2}):(\d{2})$/);
return m && +m[1] < 24 && +m[2] < 60 ? +m[1] * 60 + +m[2] : NaN;
}
menit_('09:30'); // 570
2. Rumus bentrok cukup satu baris
Dua jadwal di ruang dan tanggal yang sama bentrok jika yang satu mulai sebelum yang lain selesai, dan sebaliknya:
function bentrok_(a, b) {
return a.ruang === b.ruang && a.tanggal === b.tanggal &&
menit_(a.mulai) < menit_(b.selesai) &&
menit_(b.mulai) < menit_(a.selesai);
}
Perhatikan tanda < (bukan <=). Rapat 09:00-10:00 dan 10:00-11:00 tidak bentrok, jadi ruang bisa dipakai berurutan tanpa jeda.
3. Validasi semuanya di server
Form di browser boleh membatasi pilihan jam, tapi jangan dipercaya. Fungsi ajukan mengecek ulang semuanya:
var op = { buka: menit_('07:00'), tutup: menit_('18:00'), slot: 30 };
var m = menit_(data.mulai), s = menit_(data.selesai);
if (isNaN(m) || isNaN(s) || m % op.slot || s % op.slot) throw new Error('Jam harus kelipatan 30 menit.');
if (s <= m) throw new Error('Jam selesai harus setelah jam mulai.');
if (m < op.buka || s > op.tutup) throw new Error('Di luar jam operasional.');
if (s - m > 4 * 60) throw new Error('Maksimal 4 jam per pemesanan.');
if (peserta > ruang.kapasitas) throw new Error(ruang.nama + ' hanya muat ' + ruang.kapasitas + ' orang.');
Kelipatan slot membuat jadwal rapi (tidak ada rapat 09:07-09:52), dan batas durasi mencegah satu unit "mengunci" ruang seharian.
4. Kunci dengan LockService sebelum cek dan tulis
Ini bagian terpenting. Tanpa kunci, urutannya bisa seperti ini: A membaca sheet (jam kosong), B membaca sheet (jam masih kosong), A menulis, B menulis. Dua booking di jam yang sama.
LockService.getScriptLock() memastikan hanya satu eksekusi yang berada di bagian kritis pada satu waktu:
var lock = LockService.getScriptLock();
lock.waitLock(20000); // tunggu maksimal 20 detik
try {
var tabrak = bacaBooking_()
.filter(function (b) { return b.status === 'Menunggu' || b.status === 'Disetujui'; })
.filter(function (b) { return bentrok_(b, baru); })[0];
if (tabrak) throw new Error('Sudah dipesan pukul ' + tabrak.mulai + ' - ' + tabrak.selesai + '.');
sh.getRange(sh.getLastRow() + 1, 1, 1, baris.length).setValues([baris]);
SpreadsheetApp.flush(); // pastikan tertulis sebelum kunci dilepas
} finally {
lock.releaseLock();
}
Dua hal yang sering terlupa: baca data di dalam kunci (bukan sebelum waitLock), dan panggil SpreadsheetApp.flush() sebelum melepas kunci. Tanpa flush, eksekusi berikutnya bisa membaca sheet sebelum data baru benar-benar tersimpan.
5. Setujui satu, tolak yang bentrok
Jika memakai mode persetujuan, pengajuan yang masih "Menunggu" juga ikut menahan slot. Saat admin menyetujui satu pengajuan, pengajuan lain yang bentrok sebaiknya langsung ditolak supaya pemesannya tidak menunggu tanpa kepastian:
semua.filter(function (x) { return x.id !== b.id && x.status === 'Menunggu' && bentrok_(x, b); })
.forEach(function (x) {
sh.getRange(x.baris, 12, 1, 3).setValues([['Ditolak', x.kodeBatal, 'Jam sudah dipakai ' + b.unit + '.']]);
});
6. Kode batal tanpa akun
Pemesan tidak perlu login. Beri dua kode: kode booking (boleh terlihat) dan kode batal (rahasia). Simpan kode batal sebagai hash HMAC, jadi orang yang bisa membuka sheet pun tidak bisa memakainya:
function tanda_(isi, extra) {
return Utilities.base64EncodeWebSafe(Utilities.computeHmacSha256Signature(isi + '|' + extra, rahasia_()));
}
// saat menyimpan
baris[12] = tanda_(kodeBatal, id);
// saat membatalkan
if (b.kodeBatal !== tanda_(kodeInput, id)) throw new Error('Kode tidak cocok.');
rahasia_() adalah string acak yang dibuat sekali dan disimpan di PropertiesService. Tambahkan batas percobaan dengan CacheService agar kode tidak bisa ditebak dengan mencoba berulang-ulang.
Ringkasan
- Simpan tanggal dan jam sebagai teks, lalu bandingkan dalam satuan menit.
- Rumus bentrok:
mulaiA < selesaiB && mulaiB < selesaiA. - Validasi jam operasional, slot, durasi, dan kapasitas di server.
- Bungkus baca-cek-tulis dengan LockService dan
flush()sebelum melepas kunci. - Pakai kode batal ber-hash HMAC supaya pemesan bisa membatalkan tanpa akun.
Download source code lengkapnya
Semua teknik di atas dipakai di Booking Ruang Rapat (Google Apps Script): papan status ruang real-time ala papan stasiun, timeline jadwal 14 hari, pemesanan dengan klik jam kosong, cek dan batal mandiri, plus panel admin dengan persetujuan, notifikasi WhatsApp, statistik pemakaian ruang, dan ekspor Excel. Gratis dan tanpa hosting.
Source code lengkapnya gratis
Lihat semua yang dibahas di artikel ini langsung berjalan.