Source Code Aplikasi Absensi Siswa dengan Foto (Google Apps Script)
Source Code Aplikasi Absensi Siswa dengan Foto (Google Apps Script)
Di era digital saat ini, sistem administrasi sekolah menuntut efisiensi dan akurasi. Absensi manual menggunakan kertas seringkali rentan rusak, hilang, atau bahkan manipulasi. Solusi modern yang dapat diterapkan adalah Aplikasi Absensi Siswa Berbasis Web yang memanfaatkan teknologi Cloud Computing.
Dalam artikel ini, saya akan membagikan Source Code Lengkap untuk membuat sistem absensi canggih yang mendukung fitur foto (selfie), lokasi GPS, multi-kelas, dan laporan otomatis menggunakan Google Apps Script.
Fitur Utama Aplikasi Ini:
- Bukti Foto & GPS: Mencegah titip absen dengan validasi biometrik sederhana dan lokasi.
- Multi-Kelas & Siswa: Mendukung ribuan siswa dan banyak kelas dalam satu sistem.
- Input Massal: Fitur Admin untuk memasukkan data siswa dari Excel secara cepat.
- Laporan PDF Otomatis: Cetak rekapitulasi kehadiran dan riwayat harian langsung dari aplikasi.
- Responsif & Mobile Friendly: Tampilan modern yang nyaman diakses lewat HP (Android/iOS).
Apa itu Google Apps Script?
Google Apps Script (GAS) adalah platform pengembangan aplikasi ringan yang disediakan oleh Google. Bahasa pemrogramannya berbasis JavaScript dan berjalan di server Google (cloud). Keunggulan utamanya adalah integrasi tanpa batas dengan ekosistem Google Workspace seperti Google Sheets, Docs, Drive, dan Gmail.
Untuk aplikasi absensi ini, kita menggunakan GAS sebagai Backend untuk mengelola database (Google Sheets) dan menyajikan antarmuka pengguna (HTML/CSS/JS) sebagai Web App.
Langkah-Langkah Pembuatan
Buat Google Sheet Baru
Buka sheets.google.com dan buat spreadsheet kosong baru. Beri nama, misalnya "Database Absensi Sekolah". Anda tidak perlu membuat header kolom, sistem akan membuatnya otomatis.
Buka Editor Apps Script
Di menu Google Sheets, klik Ekstensi > Apps Script. Ini akan membuka tab baru tempat kita menulis kode.
Salin Kode Server (Backend)
Hapus semua kode yang ada di file Kode.gs, lalu salin dan tempel kode di bawah ini. Kode ini berfungsi mengatur database, menyimpan foto, dan logika admin.
/**
* Google Apps Script - Server Side V2.8
* Mendukung Multi-Kelas, Input Massal, dan Laporan Terpisah
*/
function doGet() {
return HtmlService.createTemplateFromFile('index')
.evaluate()
.setTitle('Sistem Absensi SMPN 3 Kerinci')
.addMetaTag('viewport', 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function getAppConfig() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getSheetByName('KONFIGURASI');
if (!sheet) {
sheet = ss.insertSheet('KONFIGURASI');
sheet.getRange(1,1,1,4).setValues([["NAMA SEKOLAH", "DAFTAR KELAS", "NAMA SISWA", "KELAS SISWA"]]).setFontWeight("bold");
sheet.getRange(2,1).setValue("SMP NEGERI 3 KERINCI");
SpreadsheetApp.flush();
}
const schoolName = sheet.getRange("A2").getValue();
const classes = sheet.getRange(2, 2, sheet.getLastRow(), 1).getValues().flat().filter(String);
const studentData = sheet.getRange(2, 3, sheet.getLastRow(), 2).getValues();
const students = studentData.filter(r => r[0] !== "").map(r => ({
nama: r[0],
kelas: r[1]
}));
return { school: schoolName, classes: [...new Set(classes)], students: students };
}
function updateSchoolName(newName) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('KONFIGURASI');
sheet.getRange("A2").setValue(newName);
return true;
}
function addNewClass(className) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('KONFIGURASI');
sheet.appendRow(["", className, "", ""]);
return true;
}
function addBulkStudents(namesArray, className) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('KONFIGURASI');
const rows = namesArray.map(name => ["", "", name.trim(), className]);
if (rows.length > 0) {
sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, 4).setValues(rows);
}
return true;
}
function simpanAbsensi(data) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getSheetByName('DataAbsensi');
if (!sheet) {
sheet = ss.insertSheet('DataAbsensi');
sheet.appendRow(['ID', 'Waktu', 'Kelas', 'Nama', 'Status', 'Keterangan', 'Foto', 'Lat', 'Lng']);
sheet.getRange(1, 1, 1, 9).setFontWeight('bold').setBackground('#f3f3f3');
}
sheet.appendRow([
Date.now(),
data.timestamp,
data.kelas,
data.name,
data.status,
data.keterangan || '-',
data.photo, // Link Foto Base64
data.lat,
data.lng
]);
return { success: true };
}
function ambilSemuaData() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('DataAbsensi');
return sheet ? sheet.getDataRange().getValues().slice(1) : [];
}
Buat File HTML (Tampilan)
Klik tanda (+) di sebelah "Files", pilih HTML, dan beri nama file tersebut index. Kemudian, salin kode antarmuka (UI) di bawah ini.
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<base target="_top">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.25/jspdf.plugin.autotable.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap');
body { font-family: 'Plus Jakarta Sans', sans-serif; background: #f1f5f9; }
.camera-box { border-radius: 2rem; overflow: hidden; background: #000; aspect-ratio: 4/3; position: relative; border: 4px solid white; }
#video { width: 100%; height: 100%; object-fit: cover; }
.mirror { transform: scaleX(-1); }
#loader { display: none; position: fixed; inset: 0; background: rgba(255,255,255,0.9); z-index: 999; flex-direction: column; align-items: center; justify-content: center; }
.tab-btn.active { border-bottom: 3px solid #2563eb; color: #2563eb; font-weight: 800; }
.hidden-element { display: none !important; }
#map { height: 150px; border-radius: 1.5rem; width: 100%; z-index: 10; margin-top: 10px; }
</style>
</head>
<body class="antialiased text-slate-900">
<div id="loader"><div class="animate-spin rounded-full h-12 w-12 border-t-4 border-blue-600"></div></div>
<nav class="bg-white border-b sticky top-0 z-50 p-4 shadow-sm">
<div class="max-w-md mx-auto flex justify-between items-center">
<div class="flex flex-col">
<h1 id="display-school" class="font-black text-blue-700 text-xs uppercase italic tracking-tighter">Memuat...</h1>
<p class="text-[9px] text-slate-400 font-bold uppercase tracking-[0.2em]">Sistem Absensi Pintar</p>
</div>
<div class="flex gap-4">
<button onclick="showTab('absensi')" id="btn-absensi" class="tab-btn active text-[10px] font-bold uppercase py-1">Absensi</button>
<button onclick="showTab('admin')" id="btn-admin" class="tab-btn text-[10px] font-bold uppercase py-1 text-slate-400">Admin</button>
</div>
</div>
</nav>
<main id="tab-absensi" class="max-w-md mx-auto p-4 space-y-4 pb-20">
<div class="bg-white rounded-[2.5rem] p-6 space-y-4 shadow-xl border">
<select id="class-select" onchange="filterSiswa()" class="w-full bg-slate-100 p-4 rounded-2xl font-bold outline-none border-none text-sm"></select>
<select id="student-select" class="w-full bg-slate-100 p-4 rounded-2xl font-bold outline-none border-none text-sm"></select>
<label class="text-[9px] font-bold text-slate-400 uppercase px-2">Status Kehadiran</label>
<select id="status-select" onchange="toggleCam()" class="w-full bg-blue-50 p-4 rounded-2xl font-black text-blue-700 outline-none border-none">
<option value="Hadir">HADIR (Wajib Foto)</option>
<option value="Sakit">SAKIT</option>
<option value="Izin">IZIN</option>
<option value="Alpa">ALPA</option>
<option value="Bolos">BOLOS</option>
<option value="Terlambat">TERLAMBAT</option>
</select>
<div id="camera-section" class="camera-box">
<video id="video" class="mirror" autoplay playsinline muted></video>
<button onclick="switchCamera()" class="absolute bottom-4 right-4 bg-white/30 backdrop-blur-lg text-white w-12 h-12 rounded-full flex items-center justify-center border border-white/40 active:scale-90 transition-all">
<i class="fas fa-camera-rotate text-lg"></i>
</button>
</div>
<div id="ket-section" class="hidden-element">
<textarea id="keterangan" class="w-full bg-slate-100 p-4 rounded-2xl h-20 outline-none text-sm" placeholder="Berikan alasan singkat..."></textarea>
</div>
<div id="map"></div>
<button onclick="kirimAbsensi()" class="w-full bg-blue-600 text-white py-5 rounded-[1.5rem] font-black shadow-lg shadow-blue-100 active:scale-95 transition-all uppercase tracking-widest text-sm">Kirim Data Absensi</button>
</div>
</main>
<main id="tab-admin" class="max-w-md mx-auto p-4 space-y-4 hidden-element pb-20">
<div class="bg-white rounded-[2.5rem] p-6 shadow-xl border space-y-4">
<h2 class="font-black text-xs uppercase tracking-[0.2em] text-slate-800 border-b pb-3">Konfigurasi Sistem</h2>
<div class="bg-slate-50 p-4 rounded-2xl">
<label class="text-[9px] font-bold text-slate-400 uppercase block mb-1">Nama Sekolah</label>
<div class="flex gap-2">
<input type="text" id="admin-school" class="flex-1 p-2 border rounded-xl text-xs font-bold outline-none">
<button onclick="upSekolah()" class="bg-blue-600 text-white px-4 py-2 rounded-xl text-[10px] font-bold">SIMPAN</button>
</div>
</div>
<div class="bg-slate-50 p-4 rounded-2xl">
<label class="text-[9px] font-bold text-slate-400 uppercase block mb-1">Tambah Kelas</label>
<div class="flex gap-2">
<input type="text" id="admin-class" placeholder="Contoh: 9D" class="flex-1 p-2 border rounded-xl text-xs font-bold outline-none">
<button onclick="addKelas()" class="bg-blue-600 text-white px-4 py-2 rounded-xl text-[10px] font-bold">TAMBAH</button>
</div>
</div>
<div class="bg-slate-50 p-4 rounded-2xl">
<label class="text-[9px] font-bold text-slate-400 uppercase block mb-1">Input Siswa Banyak (Massal)</label>
<select id="admin-class-list" class="w-full p-2 border rounded-xl text-xs font-bold mb-2 outline-none bg-white"></select>
<textarea id="admin-bulk-students" placeholder="Tempel banyak nama di sini
Contoh:
Andi Saputra
Budi Doremi" class="w-full p-3 border rounded-xl text-xs h-32 mb-2 outline-none font-medium"></textarea>
<button onclick="addSiswaMassal()" class="w-full bg-blue-700 text-white py-3 rounded-xl text-[10px] font-bold uppercase tracking-widest">Mulai Proses Import</button>
</div>
<div class="pt-4">
<button onclick="unduhLaporanPDF()" class="w-full bg-green-600 text-white py-5 rounded-2xl font-black text-xs uppercase shadow-lg shadow-green-100"><i class="fas fa-file-pdf mr-2"></i>Download Laporan Lengkap</button>
</div>
</div>
</main>
<footer class="text-center py-10 opacity-30 uppercase font-bold text-[9px] tracking-[0.4em] leading-relaxed">
2026 DESAIN OLEH YEFRI HARYANTO
</footer>
<canvas id="canvas" class="hidden-element"></canvas>
<script>
let config = {};
let coords = { lat: 0, lng: 0 };
let currentFacingMode = "user";
function showTab(type) {
document.getElementById('tab-absensi').classList.toggle('hidden-element', type !== 'absensi');
document.getElementById('tab-admin').classList.toggle('hidden-element', type !== 'admin');
document.getElementById('btn-absensi').classList.toggle('active', type === 'absensi');
document.getElementById('btn-admin').classList.toggle('active', type === 'admin');
}
function loadConfig() {
google.script.run.withSuccessHandler(res => {
config = res;
document.getElementById('display-school').innerText = res.school;
document.getElementById('admin-school').value = res.school;
const classSel = document.getElementById('class-select');
const adminClassList = document.getElementById('admin-class-list');
classSel.innerHTML = '<option value="">-- Pilih Kelas --</option>';
adminClassList.innerHTML = '<option value="">-- Pilih Kelas Untuk Import --</option>';
res.classes.forEach(c => {
const o = document.createElement('option'); o.value = o.text = c;
classSel.add(o.cloneNode(true));
adminClassList.add(o.cloneNode(true));
});
}).getAppConfig();
}
function filterSiswa() {
const kls = document.getElementById('class-select').value;
const stuSel = document.getElementById('student-select');
stuSel.innerHTML = '<option value="">-- Pilih Nama Siswa --</option>';
config.students.filter(s => s.kelas == kls).forEach(s => {
const o = document.createElement('option'); o.value = o.text = s.nama; stuSel.add(o);
});
}
async function startCamera(mode) {
const video = document.getElementById('video');
if (video.srcObject) { video.srcObject.getTracks().forEach(track => track.stop()); }
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: mode } });
video.srcObject = stream;
video.classList.toggle('mirror', mode === "user");
} catch (e) { console.error("Kamera tidak diizinkan", e); }
}
function switchCamera() {
currentFacingMode = (currentFacingMode === "user") ? "environment" : "user";
startCamera(currentFacingMode);
}
function toggleCam() {
const s = document.getElementById('status-select').value;
const isPresent = (s === 'Hadir' || s === 'Terlambat');
document.getElementById('camera-section').classList.toggle('hidden-element', !isPresent);
document.getElementById('ket-section').classList.toggle('hidden-element', isPresent);
}
function upSekolah() {
const n = document.getElementById('admin-school').value;
if(!n) return;
document.getElementById('loader').style.display = 'flex';
google.script.run.withSuccessHandler(() => {
document.getElementById('loader').style.display = 'none';
Swal.fire('Berhasil', 'Nama Sekolah Telah Diubah', 'success').then(loadConfig);
}).updateSchoolName(n);
}
function addKelas() {
const c = document.getElementById('admin-class').value;
if(!c) return;
document.getElementById('loader').style.display = 'flex';
google.script.run.withSuccessHandler(() => {
document.getElementById('loader').style.display = 'none';
document.getElementById('admin-class').value = "";
Swal.fire('Berhasil', 'Kelas Baru Ditambahkan', 'success').then(loadConfig);
}).addNewClass(c);
}
function addSiswaMassal() {
const kls = document.getElementById('admin-class-list').value;
const bulkText = document.getElementById('admin-bulk-students').value;
if(!kls || !bulkText) return Swal.fire('Error', 'Pilih kelas dan isi daftar nama!', 'error');
const namesArray = bulkText.split('\n').map(n => n.trim()).filter(n => n !== "");
Swal.fire({
title: 'Konfirmasi',
text: `Import ${namesArray.length} siswa ke kelas ${kls}?`,
icon: 'question',
showCancelButton: true
}).then((res) => {
if (res.isConfirmed) {
document.getElementById('loader').style.display = 'flex';
google.script.run.withSuccessHandler(() => {
document.getElementById('loader').style.display = 'none';
document.getElementById('admin-bulk-students').value = "";
Swal.fire('Selesai', 'Data siswa berhasil diimport.', 'success').then(loadConfig);
}).addBulkStudents(namesArray, kls);
}
});
}
function kirimAbsensi() {
const name = document.getElementById('student-select').value;
const kls = document.getElementById('class-select').value;
if(!name || !kls) return Swal.fire('Peringatan', 'Lengkapi Kelas dan Nama!', 'warning');
document.getElementById('loader').style.display = 'flex';
const stat = document.getElementById('status-select').value;
let photo = "-";
if(stat === 'Hadir' || stat === 'Terlambat') {
const canv = document.getElementById('canvas');
const vid = document.getElementById('video');
canv.width = vid.videoWidth; canv.height = vid.videoHeight;
const ctx = canv.getContext('2d');
if (currentFacingMode === "user") { ctx.translate(canv.width, 0); ctx.scale(-1, 1); }
ctx.drawImage(vid, 0, 0);
photo = canv.toDataURL('image/jpeg', 0.5);
}
const payload = {
kelas: kls, name: name, status: stat, lat: coords.lat, lng: coords.lng,
timestamp: new Date().toLocaleString('id-ID'),
keterangan: document.getElementById('keterangan').value,
photo: photo
};
google.script.run.withSuccessHandler(res => {
document.getElementById('loader').style.display = 'none';
if(res.success) {
Swal.fire({
title: 'SUKSES!',
text: `Absensi ${name} berhasil disimpan.`,
icon: 'success',
confirmButtonText: 'OK'
}).then(() => {
document.getElementById('student-select').value = "";
document.getElementById('keterangan').value = "";
toggleCam();
});
}
}).simpanAbsensi(payload);
}
function unduhLaporanPDF() {
document.getElementById('loader').style.display = 'flex';
google.script.run.withSuccessHandler(data => {
const { jsPDF } = window.jspdf;
const doc = new jsPDF('p', 'mm', 'a4');
const kop = (d) => {
d.setFontSize(14).setFont("helvetica","bold").text(config.school, 105, 15, {align:"center"});
d.setLineWidth(0.5).line(15, 20, 195, 20);
};
// HALAMAN 1: RIWAYAT GABUNGAN
kop(doc);
doc.setFontSize(10).setFont("helvetica","normal").text("LAPORAN RIWAYAT HARIAN (SEMUA KELAS)", 105, 27, {align:"center"});
doc.autoTable({
startY: 33,
head: [['No','Waktu','Kelas','Nama','Status']],
body: data.map((r,i)=>[i+1, r[1], r[2], r[3], r[4]]),
theme: 'grid', styles: {fontSize: 7}
});
// HALAMAN BERIKUTNYA: REKAP PER KELAS
const kelasList = [...new Set(data.map(r => r[2]))].sort();
kelasList.forEach(kls => {
doc.addPage();
kop(doc);
doc.setFontSize(10).text(`REKAPITULASI KEHADIRAN KELAS: ${kls}`, 105, 27, {align:"center"});
const rekap = {};
data.filter(r => r[2] === kls).forEach(r => {
const n = r[3]; const s = r[4];
if(!rekap[n]) rekap[n] = {H:0, I:0, S:0, A:0, B:0, T:0, total:0};
rekap[n].total++;
if(s==='Hadir') rekap[n].H++; else if(s==='Izin') rekap[n].I++; else if(s==='Sakit') rekap[n].S++; else if(s==='Alpa') rekap[n].A++; else if(s==='Bolos') rekap[n].B++; else if(s==='Terlambat') rekap[n].T++;
});
const rows = Object.keys(rekap).sort().map((n, i) => {
const r = rekap[n]; const p = (((r.H + r.T) / r.total) * 100).toFixed(1) + "%";
return [i+1, n, r.H, r.I, r.S, r.A, r.B, r.T, p];
});
doc.autoTable({
startY: 33,
head: [['No','Nama Siswa','H','I','S','A','B','T','%']],
body: rows,
theme: 'striped', styles: {fontSize: 7}
});
});
doc.save(`Laporan_Absensi_Lengkap.pdf`);
document.getElementById('loader').style.display = 'none';
}).ambilSemuaData();
}
window.onload = () => {
loadConfig();
startCamera("user");
navigator.geolocation.getCurrentPosition(p => {
coords.lat = p.coords.latitude; coords.lng = p.coords.longitude;
const m = L.map('map',{zoomControl:false, attributionControl:false}).setView([coords.lat, coords.lng], 16);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(m);
L.marker([coords.lat, coords.lng]).addTo(m);
});
};
</script>
</body>
</html>
Deploy (Publikasi)
Klik tombol Deploy (kanan atas) > New Deployment. Pilih jenis Web App. Set "Execute as" ke Me dan "Who has access" ke Anyone (Penting!). Salin URL yang dihasilkan dan bagikan ke siswa/guru.
Semoga bermanfaat! Jika Anda memiliki pertanyaan atau ingin request fitur tambahan, silakan tinggalkan komentar di bawah.
