Log pesan otomatis yang dikirim saat siswa scan QR atau tindak lanjut
Administrator Control Panel
Manajemen Akun Login Wali Kelas & User (Sheet `Users`)
Kelola akun login untuk masing-masing Wali Kelas (Kelas 1 – 6) maupun Administrator
Nama Guru / Pengguna
Username
Password
Peran (Role)
Akses Kelas
Status
Aksi
Identitas & Parameter Jam Absensi
Atur nama sekolah, kepala sekolah, serta batas jam terlambat otomatis
Manajemen Database & Sinkronisasi 2 Arah
Tarik perubahan dari Google Sheets, kirim pembaruan, atau unduh cadangan JSON
Tambah Data Siswa
Tambah Akun Wali Kelas / User
Kartu QR Siswa
Nama Siswa
NISN: -
Kode Backend `Code.gs` (Multi-User & Sheet `Users`)
Otomatis membuat Sheet Users, DataSiswa, dan LogAbsensi di Google Spreadsheet
💡 Struktur Sheet `Users` Otomatis:
Saat disinkronkan atau dijalankan pertama kali, fungsi ensureDefaultSheets() di bawah ini otomatis membuat tab sheet Users dengan kolom: ID | Username | Password | NamaLengkap | Role | Kelas | Status.
// =========================================================================
// CODE.GS TERBARU - TWO-WAY SYNC (TARIK & KIRIM DATA GOOGLE SHEETS)
// Mendukung Sheet: Users, DataSiswa, dan LogAbsensi
// =========================================================================
function doGet(e) {
ensureDefaultSheets();
// Jika aplikasi (Cloudflare/Web) meminta tarik data terbaru (?action=PULL)
if (e && e.parameter && e.parameter.action === 'PULL') {
var payload = getAllStateFromSheets();
return ContentService.createTextOutput(JSON.stringify(payload))
.setMimeType(ContentService.MimeType.JSON);
}
return HtmlService.createHtmlOutputFromFile('Index')
.setTitle('Absensi QR — WA Gateway | Sistem Absensi Siswa')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
function doPost(e) {
ensureDefaultSheets();
var payload = JSON.parse(e.postData.contents);
if (payload.action === 'LOGIN') {
return ContentService.createTextOutput(JSON.stringify(authenticateUserFromSheet(payload.username, payload.password)))
.setMimeType(ContentService.MimeType.JSON);
}
if (payload.action === 'PULL') {
return ContentService.createTextOutput(JSON.stringify(getAllStateFromSheets()))
.setMimeType(ContentService.MimeType.JSON);
}
return ContentService.createTextOutput(JSON.stringify(syncAppStateToSheets(JSON.stringify(payload))))
.setMimeType(ContentService.MimeType.JSON);
}
// 1. Otomatis membuat Sheet Users, DataSiswa, dan LogAbsensi jika belum ada
function ensureDefaultSheets() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var userSheet = ss.getSheetByName('Users');
if (!userSheet) {
userSheet = ss.insertSheet('Users');
userSheet.appendRow(['ID', 'Username', 'Password', 'NamaLengkap', 'Role', 'Kelas', 'Status']);
userSheet.appendRow(['U01', 'admin', 'admin123', 'Administrator Sekolah', 'ADMIN', 'ALL', 'Aktif']);
userSheet.appendRow(['U02', 'walikelas1', 'kelas1', 'Ibu Siti Rahma, S.Pd.', 'WALIKELAS', 'Kelas 1', 'Aktif']);
userSheet.appendRow(['U03', 'walikelas2', 'kelas2', 'Ibu Rahmawati, S.Pd.', 'WALIKELAS', 'Kelas 2', 'Aktif']);
userSheet.appendRow(['U04', 'walikelas3', 'kelas3', 'Bpk. Hendra Wijaya, S.Pd.', 'WALIKELAS', 'Kelas 3', 'Aktif']);
userSheet.appendRow(['U05', 'walikelas4', 'kelas4', 'Ibu Dewi Sartika, S.Pd.', 'WALIKELAS', 'Kelas 4', 'Aktif']);
userSheet.appendRow(['U06', 'walikelas5', 'kelas5', 'Bpk. Ahmad Fauzi, S.Pd.', 'WALIKELAS', 'Kelas 5', 'Aktif']);
userSheet.appendRow(['U07', 'walikelas6', 'kelas6', 'Ibu Hj. Nurhayati, S.Pd.', 'WALIKELAS', 'Kelas 6', 'Aktif']);
}
if (!ss.getSheetByName('DataSiswa')) {
var sSheet = ss.insertSheet('DataSiswa');
sSheet.appendRow(['ID', 'NISN', 'Nama', 'Kelas', 'Gender', 'OrangTua', 'NoWA']);
}
if (!ss.getSheetByName('LogAbsensi')) {
var lSheet = ss.insertSheet('LogAbsensi');
lSheet.appendRow(['ID', 'Tanggal', 'Waktu', 'NISN', 'Nama', 'Kelas', 'Tipe', 'Status', 'Keterangan']);
}
}
// 2. Fungsi TARIK DATA (PULL): Membaca seluruh baris dari Google Sheets ke Aplikasi
function getAllStateFromSheets() {
ensureDefaultSheets();
var ss = SpreadsheetApp.getActiveSpreadsheet();
// Baca Sheet Users
var userRows = ss.getSheetByName('Users').getDataRange().getValues();
var users = [];
for (var i = 1; i < userRows.length; i++) {
if (!userRows[i][1]) continue;
users.push({
id: String(userRows[i][0] || 'U' + i),
username: String(userRows[i][1]).trim(),
password: String(userRows[i][2]).trim(),
fullName: String(userRows[i][3] || ''),
role: String(userRows[i][4] || 'WALIKELAS').trim().toUpperCase(),
kelas: String(userRows[i][5] || 'Kelas 1').trim(),
status: String(userRows[i][6] || 'Aktif').trim()
});
}
// Baca Sheet DataSiswa
var siswaRows = ss.getSheetByName('DataSiswa').getDataRange().getValues();
var students = [];
for (var j = 1; j < siswaRows.length; j++) {
if (!siswaRows[j][1] && !siswaRows[j][2]) continue;
students.push({
id: String(siswaRows[j][0] || 'S' + j),
nisn: String(siswaRows[j][1]).trim(),
nama: String(siswaRows[j][2]).trim(),
kelas: String(siswaRows[j][3] || 'Kelas 1').trim(),
gender: String(siswaRows[j][4] || 'L').trim().toUpperCase(),
ortu: String(siswaRows[j][5] || '').trim(),
phone: String(siswaRows[j][6] || '').trim()
});
}
// Baca Sheet LogAbsensi
var logRows = ss.getSheetByName('LogAbsensi').getDataRange().getValues();
var attendance = [];
for (var k = 1; k < logRows.length; k++) {
if (!logRows[k][3] && !logRows[k][4]) continue;
attendance.push({
id: String(logRows[k][0] || 'A' + k),
date: String(logRows[k][1]).trim(),
time: String(logRows[k][2]).trim(),
nisn: String(logRows[k][3]).trim(),
nama: String(logRows[k][4]).trim(),
kelas: String(logRows[k][5]).trim(),
type: String(logRows[k][6] || 'MASUK').trim(),
status: String(logRows[k][7] || 'Hadir').trim(),
note: String(logRows[k][8] || '')
});
}
return {
ok: true,
pulledAt: new Date().toISOString(),
users: users,
students: students,
attendance: attendance
};
}
// 3. Validasi Login User langsung dari Sheet Users
function authenticateUserFromSheet(username, password) {
var state = getAllStateFromSheets();
for (var i = 0; i < state.users.length; i++) {
var u = state.users[i];
if (u.username.toLowerCase() === String(username).trim().toLowerCase() && u.password === String(password).trim()) {
if (u.status !== 'Aktif') return { ok: false, message: 'Akun sedang dinonaktifkan oleh Administrator.' };
return { ok: true, user: u };
}
}
return { ok: false, message: 'Username atau kata sandi tidak ditemukan di Sheet Users.' };
}
// 4. Fungsi KIRIM DATA (PUSH): Menyimpan perubahan dari Aplikasi ke Google Sheets
function syncAppStateToSheets(payloadJson) {
ensureDefaultSheets();
var data = JSON.parse(payloadJson);
var ss = SpreadsheetApp.getActiveSpreadsheet();
if (data.users && data.users.length) {
var userSheet = ss.getSheetByName('Users');
userSheet.clearContents();
userSheet.appendRow(['ID', 'Username', 'Password', 'NamaLengkap', 'Role', 'Kelas', 'Status']);
data.users.forEach(function(u) {
userSheet.appendRow([u.id, u.username, u.password, u.fullName, u.role, u.kelas, u.status]);
});
}
if (data.students && data.students.length) {
var siswaSheet = ss.getSheetByName('DataSiswa');
siswaSheet.clearContents();
siswaSheet.appendRow(['ID', 'NISN', 'Nama', 'Kelas', 'Gender', 'OrangTua', 'NoWA']);
data.students.forEach(function(s) {
siswaSheet.appendRow([s.id, s.nisn, s.nama, s.kelas, s.gender, s.ortu, s.phone]);
});
}
var logSheet = ss.getSheetByName('LogAbsensi');
logSheet.clearContents();
logSheet.appendRow(['ID', 'Tanggal', 'Waktu', 'NISN', 'Nama', 'Kelas', 'Tipe', 'Status', 'Keterangan']);
(data.attendance || []).forEach(function(r) {
logSheet.appendRow([r.id, r.date, r.time, r.nisn, r.nama, r.kelas, r.type, r.status, r.note || '']);
});
return { ok: true, syncedAt: new Date().toISOString() };
}
// 5. Pengiriman Pesan WhatsApp via Fonnte
function sendWaViaFonnte(target, message, token) {
var res = UrlFetchApp.fetch('https://api.fonnte.com/send', {
method: 'post',
headers: { Authorization: token },
payload: { target: target, message: message }
});
return res.getContentText();
}