Manajemen administrasi sekolah yang rumit kini bisa diatasi dengan teknologi sederhana namun sangat kuat. Anda tidak perlu menyewa server mahal atau membeli domain khusus untuk membangun Sistem Administrasi Guru yang lengkap, interaktif, dan mendukung banyak pengguna (multi-user). Rahasianya? Kombinasi maut antara Google Apps Script dan Blogger.
Mengenal Google Apps Script (Backend) dan Blogger (Frontend)
Apa itu Google Apps Script?
Google Apps Script (Appscript) adalah platform *scripting* berbasis cloud yang disediakan gratis oleh Google. Platform ini menggunakan bahasa JavaScript dan terintegrasi langsung dengan ekosistem Google Workspace (seperti Google Sheets, Docs, Drive). Dalam aplikasi ini, Appscript dan Google Sheets bertindak sebagai Server dan Database (Backend). Ia menyimpan data absensi, nilai, jadwal, dan menangani logika login tanpa batas penyimpanan (selama kapasitas Google Drive Anda memadai).
Apa itu Blogger?
Blogger adalah layanan *hosting* blog gratis milik Google. Namun, dengan trik tertentu, halaman kosong di Blogger bisa disulap menjadi Antarmuka Pengguna / UI (Frontend) yang premium layaknya aplikasi web profesional. Blogger akan menampilkan *dashboard*, tombol, tabel, dan grafik, lalu "berbicara" dengan database di Google Sheets melalui API yang kita buat di Appscript.
Manfaat Kombinasi Ini untuk Guru:
- 100% Gratis & Serverless: Tidak ada biaya hosting bulanan. Server ditangani langsung oleh infrastruktur raksasa Google.
- Aman (Anti-Down): Karena menumpang di server Google, aplikasi ini tidak akan *down* meskipun diakses puluhan guru secara bersamaan.
- Real-time & Mudah Dibackup: Semua data masuk seketika ke Google Sheets, sangat mudah diunduh (Excel/PDF) dan diprint.
- Akses di Mana Saja: Tampilan (UI) responsif di HP, tablet, maupun laptop.
Opsi 1: Buat Mandiri Menggunakan AI (Prompting)
Bagi Anda yang ingin belajar atau membuat aplikasi ini sendiri menggunakan bantuan AI (seperti Google Gemini atau ChatGPT), Anda harus memberikan instruksi yang sangat spesifik dan presisi. Berikut adalah *Mega Prompt* yang sudah disesuaikan agar AI menghasilkan kode yang *bug-free* dan responsif.
Bertindaklah sebagai Full Stack Web Developer ahli. Saya ingin membuat "Sistem Administrasi Guru" menggunakan Google Apps Script (Backend/Database via Google Sheets) dan Blogger XML (Frontend/UI). Gunakan arsitektur REST API (fetch/JSON).
FITUR YANG HARUS ADA:
1. Multi-user login (Admin dan Guru biasa).
2. Database mencakup: Config, Users, Kelas, Mapel, DataSiswa, Absensi, Nilai, Agenda, BimbinganWali, JadwalMengajar.
3. Dashboard statistik (Jumlah Rombel, Guru, Siswa).
4. Menu Guru: Input Absensi, Input Penilaian (Leger dinamis & Riwayat), Jadwal Mengajar, Jurnal Agenda, Bimbingan Wali.
5. Menu Admin: Rekap Guru Wali, Manajemen User (Tambah/Hapus), Import Siswa Massal, Konfigurasi Sekolah.
6. Fitur Cetak PDF (menggunakan jsPDF & jsPDF-AutoTable) lengkap dengan Kop Surat Sekolah, Logo (kiri/kanan), tanda tangan Kepsek, dan Auto Barcode generator.
7. Ambil data tanggal di GAS menggunakan getDisplayValues() untuk menghindari bug ISO string.
8. UI/UX: Desain "Platinum", Glassmorphism, warna primer #2C3E50. Harus 100% responsif di Mobile (Sidebar otomatis tutup/overlay). Gunakan Bootstrap 5 dan FontAwesome. Avatar user memakai Logo Sekolah dari Config.
Tolong berikan 2 kode terpisah:
1. Kode backend lengkap (Kode.gs) dengan fungsi doPost() sebagai API.
2. Kode frontend XML lengkap untuk di-paste ke Tema Blogger, lengkap dengan fungsi fetch() text/plain untuk menangani masalah CORS.
Opsi 2: Metode Copy-Paste Langsung (Siap Pakai)
Jika Anda tidak ingin repot, Anda bisa langsung menyalin kode siap pakai (Production Ready) v9.8 di bawah ini. Ikuti panduannya langkah demi langkah dengan teliti agar tidak terjadi error.
Langkah 1: Setup Backend (Google Sheets & Appscript)
- Buka Google Sheets dan buat *Spreadsheet* kosong baru.
- Klik menu Ekstensi > Apps Script.
- Hapus semua kode yang ada di layar, lalu paste (tempel) kode di bawah ini:
/**
* SISTEM ADMINISTRASI GURU v9.8 (FULL API BACKEND)
* Update: Sanitasi Data Import Siswa
*/
function doPost(e) {
var response = { status: 'error', message: 'Unknown action' };
try {
var request = JSON.parse(e.postData.contents);
var action = request.action;
var p = request.payload || {};
if (action === 'init') response = { status: 'success', data: getInitData() };
else if (action === 'login') response = verifikasiLogin(p.username, p.password);
else if (action === 'getSiswa') response = { status: 'success', data: getSiswa(p.kelas) };
else if (action === 'simpanAbsen') response = { status: 'success', message: simpanAbsen(p.kelas, p.mapel, p.guru, p.data) };
else if (action === 'getLaporanAbsen') response = { status: 'success', data: getLaporanAbsen(p.kelas, p.mapel, p.bln, p.thn, p.guru) };
else if (action === 'simpanNilai') response = { status: 'success', message: simpanNilai(p.jns, p.mapel, p.kelas, p.guru, p.data) };
else if (action === 'getLeger') response = { status: 'success', data: getLeger(p.kelas, p.mapel, p.guru) };
else if (action === 'getRiwayatNilai') response = { status: 'success', data: getRiwayatNilai(p.kelas, p.mapel, p.guru) };
else if (action === 'saveAgenda') response = { status: 'success', message: saveAgenda(p.data) };
else if (action === 'getAgenda') response = { status: 'success', data: getAgenda(p.guru) };
else if (action === 'simpanSiswaBimbingan') { simpanSiswaBimbingan(p.nama, p.kelas, p.guru); response = { status: 'success', message: 'Disimpan' }; }
else if (action === 'getSiswaBimbingan') response = { status: 'success', data: getSiswaBimbingan(p.guru) };
else if (action === 'simpanCatatanBimbingan') { simpanCatatanBimbingan(p.data); response = { status: 'success', message: 'Disimpan' }; }
else if (action === 'getRiwayatBimbingan') response = { status: 'success', data: getRiwayatBimbingan(p.guru) };
else if (action === 'getAllBimbingan') response = { status: 'success', data: getAllBimbingan() };
else if (action === 'saveConfig') response = { status: 'success', message: saveConfig(p.key, p.val) };
else if (action === 'manageItem') response = { status: 'success', message: manageItem(p.type, p.action, p.v1, p.v2, p.v3, p.v4) };
else if (action === 'importSiswa') response = { status: 'success', message: importSiswa(p.arr, p.kls) };
else if (action === 'simpanJadwal') response = { status: 'success', message: simpanJadwal(p.data) };
else if (action === 'getJadwal') response = { status: 'success', data: getJadwal(p.guru) };
else if (action === 'hapusJadwal') response = { status: 'success', message: hapusJadwal(p.guru, p.hari, p.jam) };
} catch (err) { response = { status: 'error', message: err.message }; }
return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);
}
function doGet(e) { return ContentService.createTextOutput("API Aktif. Gunakan metode POST."); }
function openDatabase() { return SpreadsheetApp.getActiveSpreadsheet(); }
function setupStructure(ss) {
const struct = [
{ name: "Config", headers: ["Key", "Value"] }, { name: "Users", headers: ["Nama", "Role", "MapelAmpuan", "Status", "NIP", "FotoURL", "Password"] },
{ name: "Kelas", headers: ["Nama Kelas"] }, { name: "Mapel", headers: ["Nama Mapel"] }, { name: "DataSiswa", headers: ["No", "Nama Siswa", "Kelas"] },
{ name: "Absensi", headers: ["Waktu", "Kelas", "Mapel", "Nama Siswa", "Status", "Nama Guru", "Bulan", "Tahun"] }, { name: "Nilai", headers: ["Waktu", "Jenis", "Mapel", "Kelas", "Nama Siswa", "Nilai", "Keterangan", "Nama Guru"] },
{ name: "Agenda", headers: ["Hari/Tgl", "Jam", "Kelas", "Mapel", "Materi", "Status", "Absen Siswa", "Ket", "Nama Guru"] }, { name: "BimbinganWali", headers: ["Tanggal", "Nama Siswa", "Kelas", "Jenis", "Masalah", "Solusi", "Guru Wali"] },
{ name: "SiswaBimbingan", headers: ["Nama Siswa", "Kelas", "Guru Wali"] }, { name: "JadwalMengajar", headers: ["Nama Guru", "Hari", "Jam Ke", "Kelas", "Mapel"] }
];
struct.forEach(s => {
let sheet = ss.getSheetByName(s.name);
if (!sheet) { sheet = ss.insertSheet(s.name); sheet.appendRow(s.headers); sheet.getRange(1, 1, 1, s.headers.length).setFontWeight("bold").setBackground("#2c3e50").setFontColor("white"); }
});
const uS = ss.getSheetByName("Users"); if (uS && uS.getLastRow() < 2) uS.appendRow(["Admin", "Admin", "-", "Aktif", "-", "", "123456"]);
const cS = ss.getSheetByName("Config");
if(cS.getLastRow() < 2) {
const defs = [ ["Nama Pemerintah", "PEMERINTAH KABUPATEN KERINCI"], ["Nama Sekolah", "SMP NEGERI 3 KERINCI"], ["Alamat Sekolah", "Jalan Muradi, Kec. Danau Kerinci Barat"], ["Nama Kepala Sekolah", "NAMA KEPSEK, M.Pd"], ["NIP Kepala Sekolah", "19800101 200001 1 001"], ["Logo Kiri", "https://upload.wikimedia.org/wikipedia/commons/e/ed/Logo_Kabupaten_Kerinci.png"], ["Logo Kanan", "https://upload.wikimedia.org/wikipedia/commons/9/9c/Logo_Tut_Wuri_Handayani.png"], ["Link Spreadsheet", ss.getUrl()], ["Tempat Tanda Tangan", "Kerinci"] ];
defs.forEach(d=>cS.appendRow(d));
}
}
function getInitData() {
const ss = openDatabase(); setupStructure(ss);
const conf = {}; ss.getSheetByName("Config").getDataRange().getDisplayValues().forEach(r => { if(r[0]) conf[r[0]] = r[1]; });
const mapel = ss.getSheetByName("Mapel").getDataRange().getDisplayValues().slice(1).flat().filter(String);
const kelas = ss.getSheetByName("Kelas").getDataRange().getDisplayValues().slice(1).flat().filter(String);
const users = ss.getSheetByName("Users").getDataRange().getDisplayValues().slice(1).filter(r=>r[0]).map(r => [r[0], r[1], r[2], r[3], r[4], r[5]]);
const jmlSiswa = ss.getSheetByName("DataSiswa").getLastRow() - 1;
return { config: conf, mapel: mapel, kelas: kelas, users: users, stats: {siswa: jmlSiswa > 0 ? jmlSiswa : 0, guru: users.length, rombel: kelas.length} };
}
function verifikasiLogin(username, passwordInput) {
const uData = openDatabase().getSheetByName("Users").getDataRange().getDisplayValues();
const passIdx = uData[0].indexOf("Password");
for(let i = 1; i < uData.length; i++) {
if(String(uData[i][0]).trim() === String(username).trim()) {
let realPass = passIdx !== -1 ? uData[i][passIdx] : "123456";
if(String(realPass).trim() === String(passwordInput).trim()) return { status: 'success' };
else return { status: 'error', message: "Password salah!" };
}
}
return { status: 'error', message: "User tidak ditemukan!" };
}
function saveConfig(key, val) {
const lock = LockService.getScriptLock();
try {
lock.waitLock(10000); const s = openDatabase().getSheetByName("Config"); const data = s.getDataRange().getValues(); let found = false;
for(let i=0; i=0; i--){ if(data[i][0] == v1) { sheet.deleteRow(i+1); break; } }
}
return "Sukses";
} catch(e) { return "Gagal menyimpan"; } finally { lock.releaseLock(); }
}
// PERBAIKAN: Sanitasi spasi ganda
function importSiswa(arr, kls) {
const lock = LockService.getScriptLock();
try {
lock.waitLock(10000);
const s = openDatabase().getSheetByName("DataSiswa");
let last = s.getLastRow(); let rows = [];
arr.forEach(n=>{
if(n && n.trim()) {
let cleanName = n.trim().replace(/\s+/g, ' ');
rows.push([last, cleanName, kls]);
last++;
}
});
if(rows.length) s.getRange(s.getLastRow()+1,1,rows.length,3).setValues(rows);
return rows.length + " Data Masuk";
} catch(e) { return "Gagal Import"; } finally { lock.releaseLock(); }
}
function simpanJadwal(d) { const lock = LockService.getScriptLock(); try { lock.waitLock(10000); openDatabase().getSheetByName("JadwalMengajar").appendRow(d); return "Tersimpan"; } finally { lock.releaseLock(); } }
function getJadwal(guru) {
const s = openDatabase().getSheetByName("JadwalMengajar"); if(s.getLastRow() < 2) return [];
const raw = s.getRange(2,1,s.getLastRow()-1,5).getDisplayValues(); if(guru === 'Admin') return raw; return raw.filter(r => r[0] == guru);
}
function hapusJadwal(guru, hari, jam) {
const lock = LockService.getScriptLock();
try {
lock.waitLock(10000); const s = openDatabase().getSheetByName("JadwalMengajar"); const data = s.getDataRange().getDisplayValues();
for(let i=data.length-1; i>=1; i--) { if(data[i][0] == guru && data[i][1] == hari && data[i][2] == jam) { s.deleteRow(i+1); return "Dihapus"; } }
return "Gagal Menghapus";
} finally { lock.releaseLock(); }
}
function getSiswa(k) {
const s = openDatabase().getSheetByName("DataSiswa"); if(s.getLastRow()<2) return [];
return s.getRange(2,2,s.getLastRow()-1,2).getDisplayValues().filter(r=>r[1]==k).map(r=>r[0]).sort();
}
function simpanAbsen(kls, mapel, guru, data) {
const lock = LockService.getScriptLock();
try {
lock.waitLock(10000); const s = openDatabase().getSheetByName("Absensi"); const time = new Date();
const rows = data.map(d=>[time, kls, mapel, d.nama, d.sts, guru, time.getMonth()+1, time.getFullYear()]);
s.getRange(s.getLastRow()+1,1,rows.length,8).setValues(rows); return "Tersimpan";
} catch(e) { return "Error: " + e.message; } finally { lock.releaseLock(); }
}
function getLaporanAbsen(kls, mapel, bln, thn, guru) {
const s = openDatabase().getSheetByName("Absensi"); const sList = getSiswa(kls); if(sList.length==0) return { error: "Siswa Kosong" }; let data = [];
if(s.getLastRow() > 1) {
const raw = s.getRange(2,1,s.getLastRow()-1,8).getValues();
if(bln === "ALL") data = raw.filter(r => r[1]==kls && r[2]==mapel && r[7]==thn && r[5]==guru); else data = raw.filter(r => r[1]==kls && r[2]==mapel && r[6]==bln && r[7]==thn && r[5]==guru);
}
let rekap = sList.map((nama, i) => {
let st = {H:0, S:0, I:0, A:0};
data.filter(r=>r[3]==nama).forEach(r=>{ let s=r[4].charAt(0); if(s=='S') st.S++; else if(s=='I') st.I++; else if(s=='A') st.A++; else st.H++; });
let tot = st.H+st.S+st.I+st.A; let pct = tot>0 ? Math.round((st.H/tot)*100) : 0; return { no:i+1, nama:nama, s:st.S, i:st.I, a:st.A, h:st.H, pct:pct };
});
let harian = data.map((r,i) => ({ no: i+1, waktu: Utilities.formatDate(new Date(r[0]), "Asia/Jakarta", "dd/MM/yyyy HH:mm"), nama: r[3], status: r[4] }));
return { rekap: rekap, harian: harian };
}
function simpanNilai(jns, mapel, kls, guru, data) {
const lock = LockService.getScriptLock();
try {
lock.waitLock(10000); const s = openDatabase().getSheetByName("Nilai"); const time = new Date();
const rows = data.map(d=>[time, jns, mapel, kls, d.nama, d.val, '', guru]);
s.getRange(s.getLastRow()+1,1,rows.length,8).setValues(rows); return "Tersimpan";
} catch(e) { return "Error"; } finally { lock.releaseLock(); }
}
function getLeger(kls, mapel, guru) {
const s = openDatabase().getSheetByName("Nilai"); const sList = getSiswa(kls); if(sList.length==0) return { error: "Siswa Kosong" }; let data = [];
if(s.getLastRow() > 1) data = s.getRange(2,1,s.getLastRow()-1,8).getValues().filter(r => r[3]==kls && r[2]==mapel && r[7]==guru);
let headers = [...new Set(data.map(r=>r[1]))].sort();
let res = sList.map((nama,i) => {
let row = { no:i+1, nama:nama, tot:0, cnt:0 };
headers.forEach(h => {
let f = data.filter(r => r[4]==nama && r[1]==h); let val = parseFloat(f.length ? String(f[f.length-1][5]).replace(',','.') : 0) || 0;
row[h] = val; if(val !== null) { row.tot+=val; row.cnt++; }
});
row.avg = (row.tot / (headers.length > 0 ? headers.length : 1)).toFixed(1); row.tot = row.tot.toFixed(0); return row;
});
return { headers: headers, data: res };
}
function getRiwayatNilai(kls, mapel, guru) {
const s = openDatabase().getSheetByName("Nilai"); if(s.getLastRow() < 2) return [];
return s.getRange(2,1,s.getLastRow()-1,8).getValues().filter(r => r[3]==kls && r[2]==mapel && r[7]==guru).sort((a,b) => new Date(b[0]) - new Date(a[0])).map(r => {
r[0] = Utilities.formatDate(new Date(r[0]), "Asia/Jakarta", "dd/MM/yyyy HH:mm"); return r;
});
}
function saveAgenda(d) { const lock = LockService.getScriptLock(); try { lock.waitLock(10000); openDatabase().getSheetByName("Agenda").appendRow(d); return "Tersimpan"; } finally { lock.releaseLock(); } }
function getAgenda(guru) {
const s = openDatabase().getSheetByName("Agenda"); if (s.getLastRow() < 2) return [];
const d = s.getRange(2, 1, s.getLastRow() - 1, 9).getDisplayValues(); if (guru === 'ALL') return d;
const searchKey = String(guru).toLowerCase().trim(); return d.filter(r => String(r[8]).toLowerCase().trim().includes(searchKey));
}
function simpanSiswaBimbingan(n, k, g) { const lock = LockService.getScriptLock(); try { lock.waitLock(10000); openDatabase().getSheetByName("SiswaBimbingan").appendRow([n, k, g]); } finally { lock.releaseLock(); } }
function getSiswaBimbingan(guru) {
const s = openDatabase().getSheetByName("SiswaBimbingan"); if (s.getLastRow() < 2) return [];
return s.getRange(2, 1, s.getLastRow() - 1, 3).getDisplayValues().filter(r => r[2] == guru).map(r => ({ nama: r[0], kelas: r[1] }));
}
function simpanCatatanBimbingan(d) { const lock = LockService.getScriptLock(); try { lock.waitLock(10000); openDatabase().getSheetByName("BimbinganWali").appendRow(d); } finally { lock.releaseLock(); } }
function getRiwayatBimbingan(g) {
const s = openDatabase().getSheetByName("BimbinganWali"); if(s.getLastRow()<2) return [];
return s.getRange(2,1,s.getLastRow()-1,7).getValues().filter(r=>r[6]==g).map(r => { r[0] = Utilities.formatDate(new Date(r[0]), "Asia/Jakarta", "dd/MM/yyyy"); return r; });
}
function getAllBimbingan() {
const s = openDatabase().getSheetByName("BimbinganWali"); if(s.getLastRow() < 2) return [];
return s.getRange(2, 1, s.getLastRow()-1, 7).getValues().map(r => { r[0] = Utilities.formatDate(new Date(r[0]), "Asia/Jakarta", "dd/MM/yyyy"); return r; });
}
- Klik tombol Terapkan (Deploy) di pojok kanan atas > Deployment Baru.
- Pilih jenis Aplikasi Web (Web App). Atur akses jalankan sebagai: Saya, dan Siapa yang memiliki akses: Semua Orang (Anyone).
- Klik Terapkan dan lakukan otorisasi izin email. Setelah selesai, Anda akan mendapatkan URL Aplikasi Web (Web App URL). Salin URL tersebut!
Langkah 2: Setup Frontend (Blogger Theme)
*Direct Download File XML Tema Premium*
- Masuk ke dasbor Blogger Anda.
- Pilih menu Tema, lalu klik tanda panah bawah di sebelah tombol Sesuaikan. Pilih Edit HTML.
- Hapus semua kode bawaan blogger, lalu buka file XML yang Anda download tadi menggunakan Notepad. Paste seluruh isinya ke editor HTML Blogger.
- Scroll (gulir) kursor Anda sampai ke bagian paling bawah. Temukan kode ini (sekitar baris 600-an):
const GAS_URL = "https://script.google.com/macros/..............."; - Ganti teks URL tersebut dengan URL Aplikasi Web (Web App URL) yang Anda dapatkan dari Langkah 1 sebelumnya. Pastikan tanda kutipnya
"..."tidak terhapus. - Klik tombol Simpan Tema (Save) di pojok kanan atas. Selesai! Buka URL Blog Anda untuk mencoba aplikasinya.
Tema ini dilindungi oleh watermark lisensi pencipta ("Desain oleh Yefri Haryanto"). Mohon untuk tidak menghapus baris footer tersebut. Penghapusan watermark akan memicu mekanisme pelindung layar yang akan membuat layar aplikasi menjadi *blank* putih secara permanen.
