Panduan Instalasi GAS WebApp – App Absensi Digital Murid
Developed by @Suyanto
Pembaruan Fitur:
- Daftar Nama: Nama anak kini bisa dipilih dari database (Sheet
DataAnak). - Ceklis Semua: Tombol cepat untuk mencentang semua kebiasaan.
- Cetak PDF: Fitur untuk mencetak riwayat jurnal ke format PDF.
Langkah Instalasi:
- Salin kode
Code.gsdanIndex.htmlterbaru ke editor Google Apps Script Anda. - WAJIB: Jalankan fungsi
setupDatabasekembali di editor Apps Script untuk membuat SheetDataAnak. - Buka Google Sheet Anda, cari sheet
DataAnak, dan isi kolom Nama serta Kelas di sana sebagai database pilihan. - Simpan (Ctrl+S) dan Deploy ulang (New Deployment) jika sudah pernah dideploy sebelumnya
Code.Gs
// Generated by GAS WebApp Builder | Created by @farhanalfaizi
function doGet() {
return HtmlService.createTemplateFromFile(‘Index’)
.evaluate()
.setTitle(‘Absensi SLB Negeri 1 Jembrana’)
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
.addMetaTag(‘viewport’, ‘width=device-width, initial-scale=1’);
}
/**
* SETUP DATABASE AWAL
*/
function setupDatabase() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sheetUsers = ss.getSheetByName(‘Users’);
if (!sheetUsers) {
sheetUsers = ss.insertSheet(‘Users’);
sheetUsers.appendRow([‘Username’, ‘Password’, ‘Nama’, ‘Role’]);
sheetUsers.appendRow([‘admin’, ‘admin’, ‘Administrator SLB’, ‘Admin’]);
sheetUsers.appendRow([‘ortu’, ‘OrtuSLB2026!’, ‘Orang Tua Murid’, ‘Ortu’]);
}
let sheetMurid = ss.getSheetByName(‘Murid’);
if (!sheetMurid) {
sheetMurid = ss.insertSheet(‘Murid’);
sheetMurid.appendRow([‘NIS’, ‘Nama Murid’, ‘Kelas’]);
sheetMurid.appendRow([‘1001’, ‘Budi Santoso’, ‘Kelas 1’]);
sheetMurid.appendRow([‘1002’, ‘Siti Aminah’, ‘Kelas 1’]);
sheetMurid.appendRow([‘1003’, ‘Rudi Hermawan’, ‘Kelas 2’]);
}
let sheetAbsen = ss.getSheetByName(‘Absensi’);
if (!sheetAbsen) {
sheetAbsen = ss.insertSheet(‘Absensi’);
sheetAbsen.appendRow([‘ID’, ‘Tanggal’, ‘NIS’, ‘Nama Murid’, ‘Kelas’, ‘Status’, ‘Keterangan’]);
}
return “Setup Berhasil! Silakan gunakan username ‘admin’ dan password ‘admin’.”;
}
function loginUser(formData) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const data = ss.getSheetByName(‘Users’).getDataRange().getValues();
data.shift();
for (let row of data) {
if (row[0] == formData.username && row[1] == formData.password) {
return {
success: true,
user: { username: row[0], nama: row[2], role: row[3] }
};
}
}
return { success: false, message: ‘Username atau Password salah!’ };
}
function getMurid() {
const data = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(‘Murid’).getDataRange().getValues();
return cleanData(data);
}
function getAbsensi() {
const data = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(‘Absensi’).getDataRange().getValues();
return cleanData(data);
}
function simpanAbsensi(listAbsen, tanggalTerpilih) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(‘Absensi’);
// Gunakan tanggal dari input jika tersedia, jika tidak gunakan hari ini
const tgl = tanggalTerpilih || Utilities.formatDate(new Date(), “GMT+8”, “yyyy-MM-dd”);
listAbsen.forEach(item => {
sheet.appendRow([
“ID-” + Date.now() + Math.floor(Math.random() * 1000),
tgl,
item.nis,
item.nama,
item.kelas,
item.status,
item.keterangan || “-“
]);
});
return { success: true };
}
function updateAbsensi(id, status) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(‘Absensi’);
const data = sheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0] == id) {
sheet.getRange(i + 1, 6).setValue(status);
return { success: true };
}
}
return { success: false, message: “ID tidak ditemukan” };
}
function cleanData(dataArray) {
if (!dataArray || dataArray.length < 1) return [];
const headers = dataArray[0];
const rows = dataArray.slice(1);
return rows.map(row => {
let obj = {};
headers.forEach((header, i) => {
let val = row[i];
if (val instanceof Date) {
val = Utilities.formatDate(val, “GMT+8”, “yyyy-MM-dd”);
}
obj[header.toLowerCase().replace(/\s/g, ‘_’)] = val === undefined || val === null ? “” : val;
});
return obj;
});
}
Index.html
<!DOCTYPE html>
<html lang=”id”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<title>Absensi SLBN 1 Jembrana</title>
<script src=”https://cdn.tailwindcss.com”></script>
<script src=”https://cdn.jsdelivr.net/npm/sweetalert2@11″></script>
<script src=”https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js”></script>
<link rel=”stylesheet” href=”https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css”>
<style>
@import url(‘https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap’);
body { font-family: ‘Inter’, sans-serif; background-color: #f8fafc; }
.sidebar { transition: all 0.3s; }
.loading-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.8); z-index: 9999; justify-content: center; align-items: center; flex-direction: column; }
.hero-gradient {
background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%);
}
@media print {
aside, nav, .no-print, button, .swal2-container, .filter-container-print, .action-col { display: none !important; }
main { padding: 0 !important; margin: 0 !important; background: white !important; width: 100% !important; max-width: 100% !important; }
#view-landing { display: block !important; }
.bg-white { border: none !important; box-shadow: none !important; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 11px; }
th, td { border: 1px solid #000 !important; padding: 6px !important; color: black !important; }
th { background-color: #f3f4f6 !important; -webkit-print-color-adjust: exact; }
.print-only { display: block !important; }
.status-badge { background: transparent !important; color: black !important; border: none !important; padding: 0 !important; }
.hero-gradient { background: transparent !important; color: black !important; padding: 0 !important; margin-bottom: 20px !important; }
.hero-gradient h1, .hero-gradient p { color: black !important; text-align: center; }
.grid { display: grid !important; grid-template-columns: repeat(4, 1fr) !important; gap: 10px !important; }
}
.print-only { display: none; }
</style>
</head>
<body>
<div id=”loadingOverlay” class=”loading-overlay”>
<div class=”animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mb-4″></div>
<p class=”text-blue-600 font-semibold” id=”loadingText”>Sedang Memproses…</p>
</div>
<div id=”app” class=”min-h-screen”>
<!– VIEW: LANDING PAGE –>
<div id=”view-landing” class=”min-h-screen flex flex-col”>
<nav class=”bg-white border-b border-gray-100 px-6 py-4 flex justify-between items-center sticky top-0 z-50 no-print”>
<div class=”flex items-center gap-2″>
<i class=”fas fa-user-graduate text-2xl text-blue-600″></i>
<span class=”font-bold text-xl text-gray-800″>SLBN 1 Jembrana</span>
</div>
<button onclick=”showLogin()” class=”bg-blue-600 text-white px-6 py-2 rounded-xl font-bold hover:bg-blue-700 transition shadow-lg shadow-blue-100 flex items-center gap-2″>
<i class=”fas fa-sign-in-alt”></i> Login
</button>
</nav>
<div class=”print-only text-center py-6 border-b-2 border-black mb-6″>
<h1 class=”text-2xl font-bold uppercase”>Laporan Kehadiran Murid</h1>
<h2 class=”text-xl font-semibold”>SLB Negeri 1 Jembrana</h2>
<p id=”print-landing-periode” class=”text-sm italic mt-1″></p>
</div>
<section class=”hero-gradient text-white py-12 md:py-16 px-6″>
<div class=”max-w-4xl mx-auto text-center”>
<h1 class=”text-3xl md:text-5xl font-black mb-4″>Rekap Kehadiran Murid</h1>
<p class=”text-blue-100 text-sm md:text-lg no-print”>Informasi kehadiran harian murid SLB Negeri 1 Jembrana secara transparan.</p>
<p id=”landing-current-date” class=”mt-2 font-bold text-blue-200″></p>
</div>
</section>
<main class=”flex-1 max-w-6xl mx-auto w-full p-6 -mt-10″>
<!– Stats Row Landing –>
<div class=”grid grid-cols-2 md:grid-cols-4 gap-4 mb-8″>
<div class=”bg-white p-6 rounded-3xl shadow-xl border border-gray-50 text-center”>
<p class=”text-gray-500 text-xs font-bold uppercase tracking-widest mb-2″>Hadir</p>
<h3 id=”landing-hadir” class=”text-4xl font-black text-green-600″>0</h3>
</div>
<div class=”bg-white p-6 rounded-3xl shadow-xl border border-gray-50 text-center”>
<p class=”text-gray-500 text-xs font-bold uppercase tracking-widest mb-2″>Izin</p>
<h3 id=”landing-izin” class=”text-4xl font-black text-amber-500″>0</h3>
</div>
<div class=”bg-white p-6 rounded-3xl shadow-xl border border-gray-50 text-center”>
<p class=”text-gray-500 text-xs font-bold uppercase tracking-widest mb-2″>Sakit</p>
<h3 id=”landing-sakit” class=”text-4xl font-black text-blue-600″>0</h3>
</div>
<div class=”bg-white p-6 rounded-3xl shadow-xl border border-gray-50 text-center”>
<p class=”text-gray-500 text-xs font-bold uppercase tracking-widest mb-2″>Alpa</p>
<h3 id=”landing-alpa” class=”text-4xl font-black text-red-600″>0</h3>
</div>
</div>
<div class=”bg-white rounded-3xl shadow-xl border border-gray-100 overflow-hidden”>
<div class=”p-6 border-b border-gray-50 space-y-4″>
<div class=”flex flex-col md:flex-row justify-between items-center gap-4″>
<h2 class=”font-bold text-gray-800 text-lg”>Daftar Kehadiran Murid</h2>
<button onclick=”window.print()” class=”no-print bg-green-600 text-white px-5 py-2 rounded-xl font-bold flex items-center gap-2 hover:bg-green-700 transition shadow-lg shadow-green-100″>
<i class=”fas fa-print”></i> Cetak Halaman
</button>
</div>
<div class=”no-print grid grid-cols-1 md:grid-cols-4 gap-3 bg-gray-50 p-4 rounded-2xl”>
<div class=”space-y-1″>
<label class=”text-[10px] font-bold text-gray-400 uppercase”>Dari Tanggal</label>
<input type=”date” id=”landing-date-start” onchange=”renderLandingTable()” class=”w-full px-3 py-2 border border-gray-200 rounded-xl outline-none text-sm”>
</div>
<div class=”space-y-1″>
<label class=”text-[10px] font-bold text-gray-400 uppercase”>Sampai Tanggal</label>
<input type=”date” id=”landing-date-end” onchange=”renderLandingTable()” class=”w-full px-3 py-2 border border-gray-200 rounded-xl outline-none text-sm”>
</div>
<div class=”space-y-1″>
<label class=”text-[10px] font-bold text-gray-400 uppercase”>Kelas</label>
<select id=”landing-filter-kelas” onchange=”renderLandingTable()” class=”w-full px-3 py-2 border border-gray-200 rounded-xl outline-none text-sm”>
<option value=””>Semua Kelas</option>
</select>
</div>
<div class=”flex items-end”>
<button onclick=”resetLandingFilter()” class=”w-full py-2 text-blue-600 font-bold text-sm hover:underline text-center”>Reset Hari Ini</button>
</div>
</div>
</div>
<div class=”overflow-x-auto”>
<table class=”w-full text-left”>
<thead class=”bg-gray-50″>
<tr>
<th class=”p-4 text-xs font-bold text-gray-400 uppercase”>Tanggal</th>
<th class=”p-4 text-xs font-bold text-gray-400 uppercase”>Nama Murid</th>
<th class=”p-4 text-xs font-bold text-gray-400 uppercase”>Kelas</th>
<th class=”p-4 text-xs font-bold text-gray-400 uppercase text-center”>Status</th>
</tr>
</thead>
<tbody id=”landing-table-body” class=”divide-y divide-gray-50″></tbody>
</table>
</div>
</div>
<div class=”print-only mt-12 grid grid-cols-2 text-center”>
<div></div>
<div>
<p>Jembrana, <span class=”print-date-now-val”></span></p>
<p class=”mb-20″>Kepala Sekolah,</p>
<p class=”font-bold border-b border-black inline-block kepala-sekolah-nama-val”>Nama Kepala Sekolah</p>
<p class=”text-xs”>NIP. <span class=”kepala-sekolah-nip-val”>…………………………</span></p>
</div>
</div>
</main>
</div>
<!– VIEW: LOGIN –>
<div id=”view-login” class=”flex items-center justify-center min-h-screen p-4 hidden bg-gray-50″>
<div class=”bg-white p-8 rounded-3xl shadow-2xl w-full max-w-md border border-gray-100 relative”>
<button onclick=”showLanding()” class=”absolute top-4 left-4 text-gray-400 hover:text-gray-600 transition”>
<i class=”fas fa-arrow-left”></i> Kembali
</button>
<div class=”text-center mb-8″>
<div class=”w-16 h-16 bg-blue-100 rounded-2xl flex items-center justify-center mx-auto mb-4″>
<i class=”fas fa-lock text-2xl text-blue-600″></i>
</div>
<h1 class=”text-2xl font-bold text-gray-800″>Login Pengajar</h1>
</div>
<form onsubmit=”handleLogin(event)” class=”space-y-4″>
<div>
<label class=”block text-xs font-bold text-gray-500 uppercase mb-2″>Username</label>
<input type=”text” id=”login-user” class=”w-full px-4 py-3 rounded-xl border border-gray-200 focus:ring-2 focus:ring-blue-500 outline-none transition bg-gray-50″ required>
</div>
<div>
<label class=”block text-xs font-bold text-gray-500 uppercase mb-2″>Password</label>
<input type=”password” id=”login-pass” class=”w-full px-4 py-3 rounded-xl border border-gray-200 focus:ring-2 focus:ring-blue-500 outline-none transition bg-gray-50″ required>
</div>
<button type=”submit” class=”w-full bg-blue-600 text-white py-4 rounded-xl font-bold hover:bg-blue-700 shadow-xl shadow-blue-200 transition”>Masuk Sekarang</button>
</form>
</div>
</div>
<!– VIEW: MAIN APP –>
<div id=”view-main” class=”flex flex-col md:flex-row hidden”>
<aside id=”sidebar” class=”sidebar w-full md:w-64 bg-white border-r border-gray-200 min-h-screen no-print”>
<div class=”p-6 border-b border-gray-50″>
<h2 class=”text-xl font-bold text-blue-600 flex items-center gap-2″>
<i class=”fas fa-check-circle”></i> Absensi SLB
</h2>
</div>
<nav class=”mt-4 px-4 space-y-1″>
<button onclick=”switchMenu(‘dashboard’)” class=”menu-item w-full flex items-center p-3 rounded-xl hover:bg-blue-50 text-gray-700 transition font-medium group”>
<i class=”fas fa-chart-line w-8 text-gray-400 group-hover:text-blue-500″></i> Dashboard
</button>
<div id=”menu-admin-only” class=”hidden space-y-1″>
<button onclick=”switchMenu(‘input’)” class=”menu-item w-full flex items-center p-3 rounded-xl hover:bg-blue-50 text-gray-700 transition font-medium group”>
<i class=”fas fa-calendar-check w-8 text-gray-400 group-hover:text-blue-500″></i> Input Absensi
</button>
<button onclick=”switchMenu(‘siswa’)” class=”menu-item w-full flex items-center p-3 rounded-xl hover:bg-blue-50 text-gray-700 transition font-medium group”>
<i class=”fas fa-user-graduate w-8 text-gray-400 group-hover:text-blue-500″></i> Data Murid
</button>
<button onclick=”switchMenu(‘guru’)” class=”menu-item w-full flex items-center p-3 rounded-xl hover:bg-blue-50 text-gray-700 transition font-medium group”>
<i class=”fas fa-chalkboard-teacher w-8 text-gray-400 group-hover:text-blue-500″></i> Data Guru
</button>
</div>
<button onclick=”switchMenu(‘rekap’)” class=”menu-item w-full flex items-center p-3 rounded-xl hover:bg-blue-50 text-gray-700 transition font-medium group”>
<i class=”fas fa-file-invoice w-8 text-gray-400 group-hover:text-blue-500″></i> Riwayat Kehadiran
</button>
<div class=”pt-4 mt-4 border-t border-gray-100″>
<button onclick=”handleLogout()” class=”w-full flex items-center p-3 rounded-xl text-red-600 hover:bg-red-50 transition font-medium”>
<i class=”fas fa-sign-out-alt w-8″></i> Keluar
</button>
</div>
</nav>
</aside>
<main class=”flex-1 p-6 lg:p-10″>
<header class=”flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4 no-print”>
<div>
<h1 id=”page-title” class=”text-3xl font-extrabold text-gray-900″>Dashboard</h1>
<p id=”user-welcome” class=”text-gray-500 font-medium”></p>
</div>
<div class=”flex items-center bg-white px-4 py-2 rounded-2xl shadow-sm border border-gray-100″>
<span id=”role-badge” class=”bg-blue-100 text-blue-700 px-3 py-1 rounded-full text-[10px] font-black tracking-wider uppercase”></span>
</div>
</header>
<div id=”page-content”>
<!– DASHBOARD –>
<div id=”page-dashboard” class=”space-y-6″>
<div class=”bg-blue-600 p-8 rounded-3xl text-white mb-8″>
<h2 class=”text-xl font-bold mb-2″>Ringkasan Hari Ini</h2>
<p id=”dashboard-date” class=”opacity-80″></p>
</div>
<div class=”grid grid-cols-2 md:grid-cols-4 gap-4″>
<div class=”bg-white p-6 rounded-3xl shadow-sm border border-gray-100 flex flex-col items-center text-center”>
<div class=”bg-green-100 p-4 rounded-2xl mb-4″><i class=”fas fa-user-check text-2xl text-green-600″></i></div>
<p class=”text-gray-500 text-xs font-semibold uppercase tracking-wide”>Hadir</p>
<h3 id=”stat-hadir” class=”text-3xl font-black text-gray-900″>0</h3>
</div>
<div class=”bg-white p-6 rounded-3xl shadow-sm border border-gray-100 flex flex-col items-center text-center”>
<div class=”bg-amber-100 p-4 rounded-2xl mb-4″><i class=”fas fa-envelope-open-text text-2xl text-amber-600″></i></div>
<p class=”text-gray-500 text-xs font-semibold uppercase tracking-wide”>Izin</p>
<h3 id=”stat-izin” class=”text-3xl font-black text-gray-900″>0</h3>
</div>
<div class=”bg-white p-6 rounded-3xl shadow-sm border border-gray-100 flex flex-col items-center text-center”>
<div class=”bg-blue-100 p-4 rounded-2xl mb-4″><i class=”fas fa-hand-holding-medical text-2xl text-blue-600″></i></div>
<p class=”text-gray-500 text-xs font-semibold uppercase tracking-wide”>Sakit</p>
<h3 id=”stat-sakit” class=”text-3xl font-black text-gray-900″>0</h3>
</div>
<div class=”bg-white p-6 rounded-3xl shadow-sm border border-gray-100 flex flex-col items-center text-center”>
<div class=”bg-red-100 p-4 rounded-2xl mb-4″><i class=”fas fa-user-times text-2xl text-red-600″></i></div>
<p class=”text-gray-500 text-xs font-semibold uppercase tracking-wide”>Alpa</p>
<h3 id=”stat-alpa” class=”text-3xl font-black text-gray-900″>0</h3>
</div>
</div>
<div class=”bg-white p-8 rounded-3xl border border-gray-100″>
<h3 class=”font-bold text-gray-800 mb-4″>Aktivitas Terakhir</h3>
<div id=”dashboard-recent” class=”divide-y divide-gray-50″>
<p class=”text-gray-400 italic text-sm py-4″>Belum ada aktivitas hari ini.</p>
</div>
</div>
</div>
<!– INPUT ABSENSI –>
<div id=”page-input” class=”hidden”>
<div class=”bg-white p-8 rounded-3xl shadow-sm border border-gray-100″>
<div class=”grid grid-cols-1 md:grid-cols-3 gap-6 mb-8 items-end”>
<div class=”space-y-2″>
<label class=”text-xs font-bold text-gray-500 uppercase”>Tanggal Absensi</label>
<input type=”date” id=”input-tanggal-absen” class=”w-full px-4 py-2 border border-gray-200 rounded-xl outline-none”>
</div>
<div class=”space-y-2″>
<label class=”text-xs font-bold text-gray-500 uppercase”>Filter Kelas</label>
<select id=”input-filter-kelas” onchange=”renderInputTable()” class=”w-full px-4 py-2 border border-gray-200 rounded-xl outline-none”></select>
</div>
<div class=”flex justify-end”>
<label class=”flex items-center gap-3 bg-blue-50 px-4 py-2 rounded-xl cursor-pointer hover:bg-blue-100 transition border border-blue-100″>
<input type=”checkbox” id=”check-all” onchange=”toggleCheckAll(this)” class=”w-5 h-5 accent-blue-600″>
<span class=”text-sm font-bold text-blue-700″>Semua Hadir</span>
</label>
</div>
</div>
<div class=”overflow-x-auto rounded-2xl border border-gray-100″>
<table class=”w-full text-left”>
<thead class=”bg-gray-50″>
<tr>
<th class=”p-4 text-xs font-bold text-gray-500 uppercase”>Nama Murid</th>
<th class=”p-4 text-center text-xs font-bold text-gray-500 uppercase”>Hadir</th>
<th class=”p-4 text-center text-xs font-bold text-gray-500 uppercase”>Izin</th>
<th class=”p-4 text-center text-xs font-bold text-gray-500 uppercase”>Sakit</th>
<th class=”p-4 text-center text-xs font-bold text-gray-500 uppercase”>Alpa</th>
</tr>
</thead>
<tbody id=”table-input-murid” class=”divide-y divide-gray-50″></tbody>
</table>
</div>
<button onclick=”submitAbsensi()” class=”mt-8 w-full bg-blue-600 text-white py-4 rounded-2xl font-black text-lg shadow-xl shadow-blue-200 hover:bg-blue-700 transition”>Simpan Absensi</button>
</div>
</div>
<!– DATA MURID –>
<div id=”page-siswa” class=”hidden”>
<div class=”bg-white p-8 rounded-3xl shadow-sm border border-gray-100″>
<div class=”flex flex-col md:flex-row justify-between items-center mb-8 gap-4″>
<h2 class=”text-xl font-bold text-gray-800″>Manajemen Data Murid</h2>
<div class=”flex gap-2″>
<label class=”bg-green-600 text-white px-5 py-2 rounded-xl font-bold hover:bg-green-700 transition flex items-center gap-2 cursor-pointer shadow-lg shadow-green-100″>
<i class=”fas fa-file-excel”></i> Import Excel
<input type=”file” id=”upload-excel” accept=”.xls,.xlsx” onchange=”handleExcelUpload(event)” class=”hidden”>
</label>
<button onclick=”openModalMurid()” class=”bg-blue-600 text-white px-5 py-2 rounded-xl font-bold hover:bg-blue-700 transition flex items-center gap-2 shadow-lg shadow-blue-100″>
<i class=”fas fa-plus”></i> Tambah Murid
</button>
</div>
</div>
<div class=”overflow-x-auto rounded-2xl border border-gray-100″>
<table class=”w-full text-left”>
<thead class=”bg-gray-50″>
<tr>
<th class=”p-4 text-xs font-bold text-gray-500 uppercase”>NISN</th>
<th class=”p-4 text-xs font-bold text-gray-500 uppercase”>Nama Murid</th>
<th class=”p-4 text-xs font-bold text-gray-500 uppercase”>Kelas</th>
<th class=”p-4 text-xs font-bold text-gray-500 uppercase text-center”>Aksi</th>
</tr>
</thead>
<tbody id=”table-murid-list” class=”divide-y divide-gray-50″></tbody>
</table>
</div>
</div>
</div>
<!– RIWAYAT KEHADIRAN –>
<div id=”page-rekap” class=”hidden”>
<div class=”bg-white p-8 rounded-3xl shadow-sm border border-gray-100″>
<div class=”print-only text-center mb-8″>
<h2 class=”text-2xl font-bold uppercase”>Laporan Kehadiran Murid (Internal)</h2>
<h3 class=”text-xl font-semibold”>SLB Negeri 1 Jembrana</h3>
<p id=”print-range-info” class=”text-sm italic mt-2″></p>
</div>
<!– PENGATURAN KEPALA SEKOLAH (ADMIN ONLY) –>
<div id=”admin-signature-settings” class=”no-print mb-6 p-4 bg-gray-50 rounded-2xl border border-gray-100 grid grid-cols-1 md:grid-cols-3 gap-4 hidden”>
<div class=”space-y-1″>
<label class=”text-[10px] font-bold text-gray-400 uppercase”>Nama Kepala Sekolah</label>
<input type=”text” id=”config-ks-nama” oninput=”updateSignatureConfig()” class=”w-full px-3 py-2 border border-gray-200 rounded-xl outline-none text-sm” placeholder=”Nama Kepala Sekolah”>
</div>
<div class=”space-y-1″>
<label class=”text-[10px] font-bold text-gray-400 uppercase”>NIP Kepala Sekolah</label>
<input type=”text” id=”config-ks-nip” oninput=”updateSignatureConfig()” class=”w-full px-3 py-2 border border-gray-200 rounded-xl outline-none text-sm” placeholder=”NIP. ……………..”>
</div>
</div>
<div class=”flex flex-wrap gap-4 mb-6 no-print items-end”>
<div class=”flex-1 min-w-[150px] space-y-1″>
<label class=”text-[10px] font-bold text-gray-400 uppercase”>Dari Tanggal</label>
<input type=”date” id=”filter-date-start” onchange=”renderRekapTable()” class=”w-full px-3 py-2 border border-gray-200 rounded-xl outline-none text-sm”>
</div>
<div class=”flex-1 min-w-[150px] space-y-1″>
<label class=”text-[10px] font-bold text-gray-400 uppercase”>Sampai Tanggal</label>
<input type=”date” id=”filter-date-end” onchange=”renderRekapTable()” class=”w-full px-3 py-2 border border-gray-200 rounded-xl outline-none text-sm”>
</div>
<div class=”flex-1 min-w-[150px] space-y-1″>
<label class=”text-[10px] font-bold text-gray-400 uppercase”>Kelas</label>
<select id=”filter-kelas” onchange=”renderRekapTable()” class=”w-full px-3 py-2 border border-gray-200 rounded-xl outline-none text-sm”></select>
</div>
<button onclick=”window.print()” class=”h-[42px] bg-green-600 text-white px-6 rounded-xl font-bold flex items-center gap-2 hover:bg-green-700 transition shadow-lg shadow-green-100″>
<i class=”fas fa-print”></i> Cetak Laporan
</button>
</div>
<div class=”overflow-x-auto rounded-2xl border border-gray-100″>
<table class=”w-full text-left”>
<thead class=”bg-gray-50″>
<tr>
<th class=”p-4 text-xs font-bold text-gray-500 uppercase”>Tanggal</th>
<th class=”p-4 text-xs font-bold text-gray-500 uppercase”>Nama Murid</th>
<th class=”p-4 text-xs font-bold text-gray-500 uppercase”>Kelas</th>
<th class=”p-4 text-xs font-bold text-gray-500 uppercase text-center”>Status</th>
<th class=”p-4 text-xs font-bold text-gray-500 uppercase text-center action-col”>Aksi</th>
</tr>
</thead>
<tbody id=”body-rekap” class=”divide-y divide-gray-50″></tbody>
</table>
</div>
<!– FOOTER CETAK INTERNAL –>
<div class=”print-only mt-12 grid grid-cols-2 text-center”>
<div></div>
<div>
<p>Jembrana, <span class=”print-date-now-val”></span></p>
<p class=”mb-20″>Kepala Sekolah,</p>
<p class=”font-bold border-b border-black inline-block kepala-sekolah-nama-val”>Nama Kepala Sekolah</p>
<p class=”text-xs”>NIP. <span class=”kepala-sekolah-nip-val”>…………………………</span></p>
</div>
</div>
</div>
</div>
<!– DATA GURU –>
<div id=”page-guru” class=”hidden”>
<div class=”bg-white p-8 rounded-3xl shadow-sm border border-gray-100″>
<div class=”flex justify-between items-center mb-6″>
<h2 class=”text-xl font-bold text-gray-800″>Manajemen Guru</h2>
<button onclick=”openModalGuru()” class=”bg-blue-600 text-white px-5 py-2 rounded-xl font-bold hover:bg-blue-700 transition flex items-center gap-2″>
<i class=”fas fa-plus”></i> Tambah Guru
</button>
</div>
<div class=”overflow-x-auto rounded-2xl border border-gray-100″>
<table class=”w-full text-left”>
<thead class=”bg-gray-50″>
<tr>
<th class=”p-4 text-xs font-bold text-gray-500 uppercase”>Nama Lengkap</th>
<th class=”p-4 text-xs font-bold text-gray-500 uppercase”>Username</th>
<th class=”p-4 text-xs font-bold text-gray-500 uppercase text-center”>Role</th>
<th class=”p-4 text-xs font-bold text-gray-500 uppercase text-center”>Aksi</th>
</tr>
</thead>
<tbody id=”table-guru-list” class=”divide-y divide-gray-50″></tbody>
</table>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
<script>
// Inisialisasi Data
let currentUser = JSON.parse(localStorage.getItem(‘slb_user_new’)) || null;
let mockMurid = JSON.parse(localStorage.getItem(‘slb_murid_storage’)) || [
{nis: ‘1001’, nama_murid: ‘Budi Santoso’, kelas: ‘Kelas 1’},
{nis: ‘1002’, nama_murid: ‘Siti Aminah’, kelas: ‘Kelas 1’},
{nis: ‘1003’, nama_murid: ‘Rudi Hermawan’, kelas: ‘Kelas 2’},
{nis: ‘1004’, nama_murid: ‘Dewi Lestari’, kelas: ‘Kelas 2’}
];
let mockAbsensi = JSON.parse(localStorage.getItem(‘slb_absensi_storage’)) || [];
let mockGuru = JSON.parse(localStorage.getItem(‘slb_guru_list’)) || [
{ nama: ‘Admin Utama’, username: ‘admin’, role: ‘Admin’, password: ‘admin’ }
];
let configKS = JSON.parse(localStorage.getItem(‘slb_config_ks’)) || {
nama: ‘Nama Kepala Sekolah, S.Pd’,
nip: ’19XXXXXXXXXXXXXXX’
};
window.onload = () => {
const today = new Date();
const dateStr = today.toISOString().split(‘T’)[0];
const dateDisplay = document.getElementById(‘landing-current-date’);
if (dateDisplay) dateDisplay.innerText = today.toLocaleDateString(‘id-ID’, { weekday: ‘long’, year: ‘numeric’, month: ‘long’, day: ‘numeric’ });
const dashDate = document.getElementById(‘dashboard-date’);
if (dashDate) dashDate.innerText = today.toLocaleDateString(‘id-ID’, { weekday: ‘long’, year: ‘numeric’, month: ‘long’, day: ‘numeric’ });
const inputTgl = document.getElementById(‘input-tanggal-absen’);
if (inputTgl) inputTgl.value = dateStr;
const lds = document.getElementById(‘landing-date-start’);
if (lds) lds.value = dateStr;
const lde = document.getElementById(‘landing-date-end’);
if (lde) lde.value = dateStr;
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(today.getDate() – 7);
const fds = document.getElementById(‘filter-date-start’);
if (fds) fds.value = sevenDaysAgo.toISOString().split(‘T’)[0];
const fde = document.getElementById(‘filter-date-end’);
if (fde) fde.value = dateStr;
populateKelasFilters();
checkState();
applySignatureUI();
updateDashboardStats();
renderLandingTable();
};
function startLoading(text = “Sedang Memproses…”) {
document.getElementById(‘loadingText’).innerText = text;
document.getElementById(‘loadingOverlay’).style.display = ‘flex’;
}
function endLoading() {
document.getElementById(‘loadingOverlay’).style.display = ‘none’;
}
function checkState() {
if (currentUser) {
showDashboard();
} else {
showLanding();
}
}
function showLanding() {
hideAllViews();
document.getElementById(‘view-landing’).classList.remove(‘hidden’);
renderLandingTable();
}
function showDashboard() {
hideAllViews();
document.getElementById(‘view-main’).classList.remove(‘hidden’);
document.getElementById(‘user-welcome’).innerText = “Halo, ” + currentUser.nama;
document.getElementById(‘role-badge’).innerText = currentUser.role;
if (currentUser.role === ‘Admin’) {
document.getElementById(‘menu-admin-only’).classList.remove(‘hidden’);
const sigSet = document.getElementById(‘admin-signature-settings’);
if(sigSet) sigSet.classList.remove(‘hidden’);
} else {
document.getElementById(‘menu-admin-only’).classList.add(‘hidden’);
const sigSet = document.getElementById(‘admin-signature-settings’);
if(sigSet) sigSet.classList.add(‘hidden’);
}
switchMenu(‘dashboard’);
}
function hideAllViews() {
[‘view-landing’, ‘view-login’, ‘view-main’].forEach(id => {
const el = document.getElementById(id);
if(el) el.classList.add(‘hidden’);
});
}
function switchMenu(menu) {
const titleEl = document.getElementById(‘page-title’);
const titles = {
‘dashboard’: ‘Dashboard’,
‘input’: ‘Input Absensi’,
‘siswa’: ‘Manajemen Murid’,
‘rekap’: ‘Riwayat Kehadiran’,
‘guru’: ‘Manajemen Guru’
};
if(titleEl) titleEl.innerText = titles[menu] || ‘Menu’;
[‘dashboard’, ‘input’, ‘siswa’, ‘rekap’, ‘guru’].forEach(m => {
const el = document.getElementById(‘page-‘ + m);
if(el) el.classList.add(‘hidden’);
});
const activePage = document.getElementById(‘page-‘ + menu);
if(activePage) activePage.classList.remove(‘hidden’);
if (menu === ‘dashboard’) updateDashboardStats();
if (menu === ‘input’) renderInputTable();
if (menu === ‘siswa’) renderMuridTable();
if (menu === ‘rekap’) renderRekapTable();
if (menu === ‘guru’) renderGuruTable();
}
function updateDashboardStats() {
const today = new Date().toISOString().split(‘T’)[0];
const todayData = mockAbsensi.filter(a => a.tanggal === today);
const stats = { ‘Hadir’: 0, ‘Izin’: 0, ‘Sakit’: 0, ‘Alpa’: 0 };
todayData.forEach(item => {
if (stats.hasOwnProperty(item.status)) stats[item.status]++;
});
document.getElementById(‘stat-hadir’).innerText = stats[‘Hadir’];
document.getElementById(‘stat-izin’).innerText = stats[‘Izin’];
document.getElementById(‘stat-sakit’).innerText = stats[‘Sakit’];
document.getElementById(‘stat-alpa’).innerText = stats[‘Alpa’];
const recentBox = document.getElementById(‘dashboard-recent’);
if (todayData.length > 0) {
recentBox.innerHTML = todayData.slice(-5).reverse().map(a => `
<div class=”py-3 flex justify-between items-center”>
<div>
<p class=”font-bold text-gray-800″>${a.nama_murid}</p>
<p class=”text-[10px] text-gray-400 uppercase”>${a.kelas}</p>
</div>
<span class=”px-2 py-1 rounded-lg text-[10px] font-bold ${getStatusStyle(a.status)}”>${a.status}</span>
</div>
`).join(”);
} else {
recentBox.innerHTML = ‘<p class=”text-gray-400 italic text-sm py-4″>Belum ada absensi masuk hari ini.</p>’;
}
}
function renderLandingTable() {
const tbody = document.getElementById(‘landing-table-body’);
if(!tbody) return;
const filterKelas = document.getElementById(‘landing-filter-kelas’).value;
const start = document.getElementById(‘landing-date-start’).value;
const end = document.getElementById(‘landing-date-end’).value;
let filtered = […mockAbsensi];
if (start) filtered = filtered.filter(a => a.tanggal >= start);
if (end) filtered = filtered.filter(a => a.tanggal <= end);
if (filterKelas) filtered = filtered.filter(a => a.kelas === filterKelas);
const stats = { ‘Hadir’: 0, ‘Izin’: 0, ‘Sakit’: 0, ‘Alpa’: 0 };
filtered.forEach(item => { if (stats.hasOwnProperty(item.status)) stats[item.status]++; });
document.getElementById(‘landing-hadir’).innerText = stats[‘Hadir’];
document.getElementById(‘landing-izin’).innerText = stats[‘Izin’];
document.getElementById(‘landing-sakit’).innerText = stats[‘Sakit’];
document.getElementById(‘landing-alpa’).innerText = stats[‘Alpa’];
tbody.innerHTML = filtered.length > 0 ? filtered.sort((a,b) => new Date(b.tanggal) – new Date(a.tanggal)).map(a => `
<tr>
<td class=”p-4 text-xs text-gray-500″>${a.tanggal}</td>
<td class=”p-4 font-bold text-gray-800″>${a.nama_murid}</td>
<td class=”p-4 text-gray-500 text-sm”>${a.kelas}</td>
<td class=”p-4 text-center”>
<span class=”px-3 py-1 rounded-full text-[10px] font-black uppercase ${getStatusStyle(a.status)}”>${a.status}</span>
</td>
</tr>
`).join(”) : ‘<tr><td colspan=”4″ class=”p-10 text-center text-gray-400 italic”>Data tidak ditemukan.</td></tr>’;
}
function resetLandingFilter() {
const todayStr = new Date().toISOString().split(‘T’)[0];
document.getElementById(‘landing-date-start’).value = todayStr;
document.getElementById(‘landing-date-end’).value = todayStr;
document.getElementById(‘landing-filter-kelas’).value = “”;
renderLandingTable();
}
function showLogin() {
hideAllViews();
document.getElementById(‘view-login’).classList.remove(‘hidden’);
}
function handleLogin(e) {
e.preventDefault();
const user = document.getElementById(‘login-user’).value;
const pass = document.getElementById(‘login-pass’).value;
startLoading(“Memverifikasi…”);
setTimeout(() => {
endLoading();
const foundGuru = mockGuru.find(g => g.username === user && g.password === pass);
if (foundGuru) {
currentUser = { …foundGuru };
localStorage.setItem(‘slb_user_new’, JSON.stringify(currentUser));
showDashboard();
} else {
Swal.fire(‘Login Gagal’, ‘Username atau Password salah.’, ‘error’);
}
}, 800);
}
function handleLogout() {
localStorage.removeItem(‘slb_user_new’);
currentUser = null;
showLanding();
}
function getStatusStyle(status) {
switch(status) {
case ‘Hadir’: return ‘bg-green-100 text-green-700’;
case ‘Izin’: return ‘bg-amber-100 text-amber-700’;
case ‘Sakit’: return ‘bg-blue-100 text-blue-700’;
case ‘Alpa’: return ‘bg-red-100 text-red-700’;
default: return ‘bg-gray-100 text-gray-700’;
}
}
function populateKelasFilters() {
const listKelas = […new Set(mockMurid.map(m => m.kelas))].sort();
[‘filter-kelas’, ‘input-filter-kelas’, ‘landing-filter-kelas’].forEach(id => {
const el = document.getElementById(id);
if (el) {
const current = el.value;
el.innerHTML = ‘<option value=””>Semua Kelas</option>’;
listKelas.forEach(k => el.innerHTML += `<option value=”${k}”>${k}</option>`);
el.value = current;
}
});
}
function renderMuridTable() {
const tbody = document.getElementById(‘table-murid-list’);
if(!tbody) return;
tbody.innerHTML = mockMurid.map((m, index) => `
<tr>
<td class=”p-4 text-xs font-mono text-gray-500″>${m.nis}</td>
<td class=”p-4 font-bold text-gray-800″>${m.nama_murid}</td>
<td class=”p-4 text-gray-600″>${m.kelas}</td>
<td class=”p-4 text-center”>
<button onclick=”hapusMurid(${index})” class=”text-red-500 hover:text-red-700 transition”><i class=”fas fa-trash”></i></button>
</td>
</tr>
`).join(”) || ‘<tr><td colspan=”4″ class=”p-10 text-center text-gray-400″>Belum ada data murid.</td></tr>’;
}
function openModalMurid() {
Swal.fire({
title: ‘Tambah Murid’,
html: `
<input id=”swal-nisn” placeholder=”NISN” class=”swal2-input”>
<input id=”swal-nama” placeholder=”Nama Lengkap” class=”swal2-input”>
<input id=”swal-kelas” placeholder=”Kelas (Contoh: Kelas 1)” class=”swal2-input”>
`,
preConfirm: () => {
const nis = document.getElementById(‘swal-nisn’).value;
const nama_murid = document.getElementById(‘swal-nama’).value;
const kelas = document.getElementById(‘swal-kelas’).value;
if (!nis || !nama_murid || !kelas) Swal.showValidationMessage(‘Semua wajib diisi’);
return { nis, nama_murid, kelas };
}
}).then(res => {
if(res.isConfirmed) {
mockMurid.push(res.value);
localStorage.setItem(‘slb_murid_storage’, JSON.stringify(mockMurid));
renderMuridTable();
populateKelasFilters();
Swal.fire(‘Sukses’, ‘Murid ditambahkan’, ‘success’);
}
});
}
function hapusMurid(index) {
Swal.fire({
title: ‘Hapus Murid?’,
icon: ‘warning’,
showCancelButton: true,
confirmButtonText: ‘Ya, Hapus’
}).then(res => {
if(res.isConfirmed) {
mockMurid.splice(index, 1);
localStorage.setItem(‘slb_murid_storage’, JSON.stringify(mockMurid));
renderMuridTable();
populateKelasFilters();
}
});
}
function handleExcelUpload(event) {
const file = event.target.files[0];
if (!file) return;
startLoading(“Membaca Excel…”);
const reader = new FileReader();
reader.onload = (e) => {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: ‘array’ });
const jsonData = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]], { header: 1 });
const imported = [];
for (let i = 1; i < jsonData.length; i++) {
if (jsonData[i][1]) {
imported.push({ nis: String(jsonData[i][0] || ‘0’), nama_murid: jsonData[i][1], kelas: jsonData[i][2] || ‘?’ });
}
}
if (imported.length > 0) {
mockMurid = […mockMurid, …imported];
localStorage.setItem(‘slb_murid_storage’, JSON.stringify(mockMurid));
renderMuridTable();
populateKelasFilters();
endLoading();
Swal.fire(‘Import Berhasil’, `${imported.length} murid ditambahkan`, ‘success’);
} else {
endLoading();
Swal.fire(‘Gagal’, ‘Format file tidak sesuai atau kosong’, ‘error’);
}
event.target.value = ”;
};
reader.readAsArrayBuffer(file);
}
function renderInputTable() {
const tbody = document.getElementById(‘table-input-murid’);
if(!tbody) return;
const filterKelas = document.getElementById(‘input-filter-kelas’).value;
const filtered = filterKelas ? mockMurid.filter(m => m.kelas === filterKelas) : mockMurid;
if (filtered.length === 0) {
tbody.innerHTML = ‘<tr><td colspan=”5″ class=”p-10 text-center text-gray-400″>Pilih kelas yang memiliki murid.</td></tr>’;
return;
}
tbody.innerHTML = filtered.map(m => `
<tr>
<td class=”p-4″><div class=”font-bold text-gray-800″>${m.nama_murid}</div><div class=”text-[10px] text-gray-400 uppercase”>${m.nis} • ${m.kelas}</div></td>
<td class=”text-center p-4″><input type=”radio” name=”st-${m.nis}” value=”Hadir” checked class=”w-5 h-5 accent-green-600″></td>
<td class=”text-center p-4″><input type=”radio” name=”st-${m.nis}” value=”Izin” class=”w-5 h-5 accent-amber-500″></td>
<td class=”text-center p-4″><input type=”radio” name=”st-${m.nis}” value=”Sakit” class=”w-5 h-5 accent-blue-600″></td>
<td class=”text-center p-4″><input type=”radio” name=”st-${m.nis}” value=”Alpa” class=”w-5 h-5 accent-red-600″></td>
</tr>
`).join(”);
}
function toggleCheckAll(el) {
if(el.checked) document.querySelectorAll(‘input[type=”radio”][value=”Hadir”]’).forEach(r => r.checked = true);
}
function submitAbsensi() {
const date = document.getElementById(‘input-tanggal-absen’).value;
const filterKelas = document.getElementById(‘input-filter-kelas’).value;
const filtered = filterKelas ? mockMurid.filter(m => m.kelas === filterKelas) : mockMurid;
if (filtered.length === 0) return;
mockAbsensi = mockAbsensi.filter(a => !(a.tanggal === date && filtered.some(f => f.nis === a.nis)));
const newEntries = filtered.map(m => ({
id: Date.now() + Math.random(),
tanggal: date,
nis: m.nis,
nama_murid: m.nama_murid,
kelas: m.kelas,
status: document.querySelector(`input[name=”st-${m.nis}”]:checked`).value
}));
mockAbsensi.push(…newEntries);
saveAbsensi();
updateDashboardStats();
Swal.fire(‘Tersimpan’, ‘Absensi berhasil dicatat.’, ‘success’);
switchMenu(‘dashboard’);
}
function renderRekapTable() {
const tbody = document.getElementById(‘body-rekap’);
if(!tbody) return;
const filterKelas = document.getElementById(‘filter-kelas’).value;
const start = document.getElementById(‘filter-date-start’).value;
const end = document.getElementById(‘filter-date-end’).value;
let filtered = […mockAbsensi];
if (start) filtered = filtered.filter(a => a.tanggal >= start);
if (end) filtered = filtered.filter(a => a.tanggal <= end);
if (filterKelas) filtered = filtered.filter(a => a.kelas === filterKelas);
tbody.innerHTML = filtered.sort((a,b) => new Date(b.tanggal) – new Date(a.tanggal)).map(a => `
<tr>
<td class=”p-4 text-xs text-gray-500″>${a.tanggal}</td>
<td class=”p-4 font-bold text-gray-800″>${a.nama_murid}</td>
<td class=”p-4 text-gray-600″>${a.kelas}</td>
<td class=”p-4 text-center”><span class=”px-3 py-1 rounded-full text-[10px] font-black uppercase ${getStatusStyle(a.status)}”>${a.status}</span></td>
<td class=”p-4 text-center action-col”>
<div class=”flex justify-center gap-3″>
<button onclick=”editAbsensi(${a.id})” class=”text-amber-500 hover:text-amber-700 transition” title=”Edit Status”>
<i class=”fas fa-edit”></i>
</button>
<button onclick=”hapusAbsensi(${a.id})” class=”text-red-500 hover:text-red-700 transition” title=”Hapus Riwayat”>
<i class=”fas fa-trash”></i>
</button>
</div>
</td>
</tr>
`).join(”) || ‘<tr><td colspan=”5″ class=”p-10 text-center text-gray-400″>Data riwayat kosong.</td></tr>’;
}
function editAbsensi(id) {
const data = mockAbsensi.find(a => a.id === id);
if(!data) return;
Swal.fire({
title: ‘Edit Status Kehadiran’,
text: `Ubah status untuk ${data.nama_murid} (${data.tanggal})`,
input: ‘select’,
inputOptions: {
‘Hadir’: ‘Hadir’,
‘Izin’: ‘Izin’,
‘Sakit’: ‘Sakit’,
‘Alpa’: ‘Alpa’
},
inputValue: data.status,
showCancelButton: true,
confirmButtonColor: ‘#3b82f6’,
confirmButtonText: ‘Simpan Perubahan’,
cancelButtonText: ‘Batal’
}).then((result) => {
if (result.isConfirmed) {
const index = mockAbsensi.findIndex(a => a.id === id);
if (index !== -1) {
mockAbsensi[index].status = result.value;
saveAbsensi();
renderRekapTable();
updateDashboardStats();
Swal.fire({
title: ‘Berhasil!’,
text: ‘Status kehadiran telah diperbarui.’,
icon: ‘success’,
timer: 1500,
showConfirmButton: false
});
}
}
});
}
function hapusAbsensi(id) {
Swal.fire({
title: ‘Hapus Riwayat?’,
text: “Data yang dihapus tidak bisa dikembalikan.”,
icon: ‘warning’,
showCancelButton: true,
confirmButtonColor: ‘#ef4444’,
confirmButtonText: ‘Ya, Hapus’
}).then(res => {
if(res.isConfirmed) {
mockAbsensi = mockAbsensi.filter(a => a.id !== id);
saveAbsensi();
renderRekapTable();
updateDashboardStats();
}
});
}
function saveAbsensi() {
localStorage.setItem(‘slb_absensi_storage’, JSON.stringify(mockAbsensi));
}
function applySignatureUI() {
const nowStr = new Date().toLocaleDateString(‘id-ID’, {day:’numeric’, month:’long’, year:’numeric’});
document.querySelectorAll(‘.print-date-now-val’).forEach(el => el.innerText = nowStr);
document.querySelectorAll(‘.kepala-sekolah-nama-val’).forEach(el => el.innerText = configKS.nama);
document.querySelectorAll(‘.kepala-sekolah-nip-val’).forEach(el => el.innerText = configKS.nip);
const inNama = document.getElementById(‘config-ks-nama’);
const inNip = document.getElementById(‘config-ks-nip’);
if(inNama) inNama.value = configKS.nama;
if(inNip) inNip.value = configKS.nip;
}
function updateSignatureConfig() {
configKS.nama = document.getElementById(‘config-ks-nama’).value;
configKS.nip = document.getElementById(‘config-ks-nip’).value;
localStorage.setItem(‘slb_config_ks’, JSON.stringify(configKS));
applySignatureUI();
}
function renderGuruTable() {
const tbody = document.getElementById(‘table-guru-list’);
if(!tbody) return;
tbody.innerHTML = mockGuru.map((g, index) => `
<tr>
<td class=”p-4 font-bold text-gray-800″>${g.nama}</td>
<td class=”p-4 text-gray-600″>${g.username}</td>
<td class=”p-4 text-center”><span class=”bg-blue-50 text-blue-600 px-3 py-1 rounded-full text-[10px] font-black uppercase”>${g.role}</span></td>
<td class=”p-4 text-center”>
${g.username === ‘admin’ ? ‘-‘ : `<button onclick=”hapusGuru(${index})” class=”text-red-500″><i class=”fas fa-trash”></i></button>`}
</td>
</tr>
`).join(”);
}
function openModalGuru() {
Swal.fire({
title: ‘Tambah Guru’,
html: `
<input id=”swal-nama” placeholder=”Nama Lengkap” class=”swal2-input”>
<input id=”swal-user” placeholder=”Username” class=”swal2-input”>
<input id=”swal-pass” type=”password” placeholder=”Password” class=”swal2-input”>
`,
preConfirm: () => {
const nama = document.getElementById(‘swal-nama’).value;
const user = document.getElementById(‘swal-user’).value;
const pass = document.getElementById(‘swal-pass’).value;
if (!nama || !user || !pass) Swal.showValidationMessage(‘Lengkapi data’);
return { nama, username: user, password: pass, role: ‘Admin’ };
}
}).then(res => {
if(res.isConfirmed) {
mockGuru.push(res.value);
localStorage.setItem(‘slb_guru_list’, JSON.stringify(mockGuru));
renderGuruTable();
}
});
}
function hapusGuru(index) {
mockGuru.splice(index, 1);
localStorage.setItem(‘slb_guru_list’, JSON.stringify(mockGuru));
renderGuruTable();
}
</script>
</body>
</html>
