Cara Membuat Absensi Online dengan GPS dan Selfie di Google Apps Script
Absensi online tidak harus pakai server dan database sendiri. Dengan Google Apps Script, kita bisa membuat web app yang dibuka dari HP, membaca lokasi GPS, mengambil foto selfie, lalu menyimpan semuanya ke Google Sheets dan Google Drive. Semuanya gratis dan cukup memakai akun Google.
Di tutorial ini kita bahas bagian-bagian intinya. Contohnya diambil dari aplikasi Presensi Karyawan GPS + Selfie yang bisa kamu download gratis di Papicode (link di akhir artikel).
1. Struktur project
Buat spreadsheet baru, lalu buka Ekstensi → Apps Script. Kita butuh beberapa file:
| File | Isi |
|---|---|
Code.gs | Logika server |
Index.html | Halaman absen karyawan |
appsscript.json | Zona waktu & pengaturan web app |
Atur zona waktu di appsscript.json agar jam absen tidak meleset:
{
"timeZone": "Asia/Jakarta",
"runtimeVersion": "V8",
"webapp": { "executeAs": "USER_DEPLOYING", "access": "ANYONE_ANONYMOUS" }
}
Dengan executeAs: USER_DEPLOYING, script berjalan sebagai pemilik spreadsheet. Karyawan tidak perlu punya akses ke sheet atau bahkan akun Google; mereka hanya melihat halaman web.
2. Menampilkan halaman web
Fungsi doGet() dipanggil setiap kali URL web app dibuka:
function doGet(e) {
const t = HtmlService.createTemplateFromFile('Index');
t.serverTime = new Date().getTime(); // dipakai untuk jam di layar
return t.evaluate()
.setTitle('Presensi')
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
Tag viewport wajib ada. Tanpa tag ini, halaman akan tampil kecil sekali di HP.
3. Ambil lokasi dan foto di browser
Di Index.html, baca lokasi dengan watchPosition supaya posisi terus diperbarui saat GPS makin akurat:
let pos = null;
navigator.geolocation.watchPosition(
p => { pos = p.coords; },
e => alert('Izinkan akses lokasi'),
{ enableHighAccuracy: true, maximumAge: 10000, timeout: 20000 }
);
Untuk selfie, nyalakan kamera depan lalu "potret" isi video ke <canvas>:
const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' } });
video.srcObject = stream;
function ambilFoto() {
const c = document.createElement('canvas');
c.width = 360; c.height = 480; // cukup kecil, tetap jelas
c.getContext('2d').drawImage(video, 0, 0, 360, 480);
return c.toDataURL('image/jpeg', 0.72).split(',')[1]; // base64 tanpa awalan
}
Ukuran 360×480 dengan kualitas 0,72 menghasilkan sekitar 20–40 KB per foto. Hasilnya cepat dikirim lewat sinyal seadanya dan tidak memenuhi kuota Drive.
Kirim ke server dengan google.script.run:
google.script.run
.withSuccessHandler(r => tampilkanSukses(r))
.withFailureHandler(e => alert(e.message))
.absen({ token, lat: pos.latitude, lng: pos.longitude, akurasi: pos.accuracy, foto: ambilFoto() });
4. Validasi jarak di server
Jangan mengandalkan pengecekan di browser, karena JavaScript di HP bisa diubah. Hitung ulang jaraknya di Code.gs dengan rumus Haversine:
function jarak_(lat1, lng1, lat2, lng2) {
const R = 6371000, rad = x => x * Math.PI / 180;
const dLat = rad(lat2 - lat1), dLng = rad(lng2 - lng1);
const a = Math.sin(dLat / 2) ** 2 + Math.cos(rad(lat1)) * Math.cos(rad(lat2)) * Math.sin(dLng / 2) ** 2;
return Math.round(2 * R * Math.asin(Math.sqrt(a))); // meter
}
function absen(p) {
const cfg = getConfig_();
if (p.akurasi > 200) throw new Error('Sinyal GPS kurang akurat, pindah ke area terbuka.');
const jarak = jarak_(p.lat, p.lng, cfg.lat, cfg.lng);
if (jarak > cfg.radius) throw new Error('Anda berada ' + jarak + ' m dari kantor.');
// ...
}
Fungsi yang namanya diakhiri garis bawah (jarak_, getConfig_) bersifat privat: tidak bisa dipanggil dari browser lewat google.script.run. Biasakan memberi akhiran _ pada fungsi yang hanya dipakai di server.
5. Simpan foto ke Drive dan data ke Sheets
const blob = Utilities.newBlob(Utilities.base64Decode(p.foto), 'image/jpeg', tgl + '_' + nip + '.jpg');
const fotoId = DriveApp.getFolderById(folderId).createFile(blob).getId();
SpreadsheetApp.getActive().getSheetByName('Presensi')
.appendRow([id, tgl, nip, nama, 'MASUK', jam, status, telat, p.lat, p.lng, jarak, fotoId]);
Yang disimpan di sheet cukup ID file, bukan link publik. Saat admin ingin melihat foto, ambil isinya lewat server lalu kirim sebagai base64. Dengan cara ini foto karyawan tidak perlu dibagikan ke "siapa saja yang punya link".
6. Cegah absen dobel dengan LockService
Karyawan yang menekan tombol dua kali dengan cepat bisa memicu dua request bersamaan. Keduanya mengecek "sudah absen?" lalu sama-sama mendapat jawaban "belum". Solusinya, kunci bagian cek dan simpan:
const lock = LockService.getScriptLock();
lock.waitLock(20000); // request lain menunggu di sini
try {
if (sudahAbsenMasuk_(nip)) throw new Error('Anda sudah absen masuk.');
sheet.appendRow([...]);
SpreadsheetApp.flush(); // pastikan tertulis sebelum kunci dilepas
} finally {
lock.releaseLock();
}
SpreadsheetApp.flush() penting di sini. Tanpanya, data bisa belum benar-benar tertulis ketika request berikutnya mulai membaca.
7. Jebakan: jam di Google Sheets
Kalau kamu menulis teks "08:00" ke sel berformat biasa, Sheets akan mengubahnya menjadi nilai jam dengan tanggal dasar tahun 1899. Saat dibaca script, nilai itu bisa bergeser beberapa menit karena zona waktu historis (Jakarta tahun 1899 memakai offset +07:07). Akibatnya jam masuk 08:00 bisa terbaca tidak tepat 08:00.
Solusinya, atur kolomnya sebagai teks sebelum menulis:
sheet.getRange('B:B').setNumberFormat('@'); // dulu
sheet.getRange(2, 1, n, 3).setValues(data); // baru isi
Hal yang sama berlaku untuk kolom tanggal dan NIP. Kalau tidak diformat teks, NIP 0012 akan berubah menjadi angka 12.
8. Sesi login lebih dari 6 jam
CacheService hanya bisa menyimpan data paling lama 6 jam. Kalau token login karyawan disimpan di cache, karyawan yang login pukul 07.30 akan otomatis keluar sebelum jam pulang.
Gunakan token yang ditandatangani dengan HMAC. Server tidak perlu menyimpan apa pun, cukup memverifikasi tanda tangannya:
function buatToken_(nip, pin, hari) {
const isi = nip + '.' + (Date.now() + hari * 86400000);
const sig = Utilities.base64EncodeWebSafe(
Utilities.computeHmacSha256Signature(isi + '|' + pin, rahasia_()));
return isi + '.' + sig;
}
rahasia_() adalah string acak yang dibuat sekali lalu disimpan di PropertiesService. Karena PIN ikut ditandatangani, mengganti PIN otomatis membatalkan semua token lama karyawan tersebut.
Ringkasan
- Web app dengan
executeAs: USER_DEPLOYINGdan akses Anyone membuat karyawan bisa absen tanpa akun Google. - Kompres selfie di browser lewat
<canvas>sebelum dikirim. - Selalu hitung ulang jarak di server dengan rumus Haversine dan tolak akurasi GPS yang buruk.
- Pakai LockService +
SpreadsheetApp.flush()untuk mencegah absen dobel. - Format kolom jam, tanggal, dan NIP sebagai teks sebelum diisi.
- Untuk sesi lebih dari 6 jam, gunakan token HMAC, bukan CacheService.
Download source code lengkapnya
Semua teknik di atas sudah dipakai di Aplikasi Presensi Karyawan GPS + Selfie (Google Apps Script): login NIP + PIN, radius kantor, selfie ke Drive, hitung terlambat dan pulang cepat, izin/sakit, riwayat, halaman admin dengan grafik, serta rekap bulanan yang bisa disimpan ke sheet atau diunduh sebagai CSV. Gratis, tanpa hosting.
Source code lengkapnya gratis
Lihat semua yang dibahas di artikel ini langsung berjalan.
Tutorial lainnya
Cara Membuat Nomor Antrean Harian yang Aman dari Duplikat di Laravel (Studi Kasus Aplikasi Kasir)
20 Oktober 2026
Cara Membuat Link Undangan Personal per Tamu dan Melacak Siapa yang Sudah Membuka (PHP & MySQL)
20 Oktober 2026
Cara Membuat Checkout Order via WhatsApp di WordPress Tanpa WooCommerce
19 Oktober 2026