:root {
–primary: #2563eb;
–primary-dark: #1d4ed8;
–glass-bg: rgba(255, 255, 255, 0.85);
–glass-border: rgba(255, 255, 255, 0.5);
}
html, body {
margin: 0 !important; padding: 0 !important;
width: 100% !important; height: 100% !important;
overflow: hidden !important;
font-family: ‘Plus Jakarta Sans’, sans-serif;
background-color: #f8fafc; color: #1e293b;
-webkit-font-smoothing: antialiased;
}
/* HIDE BLOGGER OVERLAYS */
#navbar-iframe, .Navbar, .navbar, .blog-header, .blog-footer, .header-outer, .footer-outer {
display: none !important; height: 0 !important; visibility: hidden !important;
}
#app-container {
position: fixed !important; top: 0 !important; left: 0 !important;
width: 100vw !important; height: 100vh !important;
background: radial-gradient(circle at top right, #f1f5f9, #e2e8f0);
z-index: 2147483647 !important; overflow-y: auto !important;
display: flex; flex-direction: column;
}
/* ANIMATIONS */
@keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
@keyframes scaleIn { from { transform: scale(0.95); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@keyframes pulse-soft { 0%, 100% { opacity: 1; } 50% { opacity: 0.8; } }
@keyframes spin-slow { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.animate-fade-in { animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
.animate-scale-in { animation: scaleIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
.animate-pulse-slow { animation: pulse-soft 3s ease-in-out infinite; }
.animate-spin-slow { animation: spin-slow 8s linear infinite; }
.glass-card {
background: var(–glass-bg); backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid var(–glass-border);
box-shadow: 0 12px 40px -10px rgba(0, 0, 0, 0.08);
}
.custom-scrollbar::-webkit-scrollbar { width: 5px; height: 5px; }
.custom-scrollbar::-webkit-scrollbar-track { background: rgba(0,0,0,0.02); }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
.view-section { display: none; min-height: 100vh; width: 100%; flex-direction: column; overflow-x: hidden; }
.view-section.active { display: flex; }
/* SIDEBAR DESKTOP */
.sidebar-container { transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); width: 18rem; position: relative; z-index: 60; }
.sidebar-collapsed { width: 5.5rem !important; }
.sidebar-collapsed .nav-text, .sidebar-collapsed .nav-badge, .sidebar-collapsed .nav-subtitle { display: none; }
.sidebar-collapsed .admin-tab { justify-content: center; padding-left: 0; padding-right: 0; margin-left: 0.5rem; margin-right: 0.5rem; }
.sidebar-collapsed .nav-icon { margin-right: 0; font-size: 1.5rem; }
/* MOBILE BOTTOM NAVIGATION FIXES */
@media (max-width: 767px) {
.mobile-bottom-nav {
position: fixed; bottom: 0; left: 0; right: 0; z-index: 1000;
flex-direction: row; justify-content: space-between; padding: 0.5rem;
background: rgba(255, 255, 255, 0.98); backdrop-filter: blur(15px);
box-shadow: 0 -10px 30px rgba(0, 0, 0, 0.08); border-top: 1px solid #f1f5f9;
overflow-x: auto; flex-wrap: nowrap;
}
.mobile-bottom-nav .admin-tab {
flex-direction: column; padding: 0.5rem 0.25rem; font-size: 0.6rem;
gap: 0.25rem; min-width: 4rem; flex: 1; border-radius: 1rem;
}
.mobile-bottom-nav .nav-icon { margin-right: 0 !important; font-size: 1.25rem; width: auto; }
.mobile-bottom-nav .nav-icon i { transform: scale(1) !important; }
.mobile-bottom-nav .nav-text { text-align: center; font-size: 0.6rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.mobile-bottom-nav .nav-subtitle, .mobile-bottom-nav .nav-badge { display: none; }
.content-area-mobile-fix { padding-bottom: 6rem !important; }
#btn-collapse-sidebar { display: none !important; }
.sidebar-container { display: none !important; }
/* Responsive Video Camera Wrapper */
.video-box-wrapper { border-width: 4px; border-radius: 1rem; }
}
.video-box-wrapper { position: relative; background: #0f172a; border-radius: 2rem; overflow: hidden; box-shadow: 0 20px 40px -12px rgba(0, 0, 0, 0.4); border: 8px solid #1e293b; }
.btn-switch-camera {
position: absolute; top: 1rem; right: 1rem; z-index: 100; width: 3.5rem; height: 3.5rem;
border-radius: 50%; background: rgba(255, 255, 255, 0.2); backdrop-filter: blur(15px);
border: 1px solid rgba(255, 255, 255, 0.3); color: white; display: flex; align-items: center; justify-content: center;
font-size: 1.4rem; cursor: pointer; transition: all 0.3s;
}
.btn-switch-camera:active { transform: scale(0.9) rotate(180deg); }
#toast-container { position: fixed; top: 1rem; right: 1rem; z-index: 2147483647; pointer-events: none; width: calc(100% – 2rem); max-width: 400px; display: flex; flex-direction: column; align-items: flex-end;}
.toast { pointer-events: auto; background: #ffffff; color: #1e293b; padding: 1rem 1.5rem; border-radius: 1rem; margin-bottom: 0.75rem; box-shadow: 0 10px 30px rgba(0,0,0,0.1); display: flex; align-items: center; gap: 1rem; border-left: 5px solid #3b82f6; transform: translateX(120%); transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55); width: 100%;}
.toast.show { transform: translateX(0); }
.toast.error { border-left-color: var(–danger); }
.toast.success { border-left-color: var(–success); }
.toast.warning { border-left-color: var(–warning); }
#loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.98); z-index: 2147483647; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; padding: 2rem;}
.loader-ring { width: 60px; height: 60px; border: 6px solid #f1f5f9; border-top: 6px solid #2563eb; border-radius: 50%; animation: spin 1s linear infinite; }
.custom-table { width: 100%; border-collapse: separate; border-spacing: 0 6px; }
.custom-table tr { background: rgba(255, 255, 255, 0.7); transition: all 0.2s; }
.custom-table th { padding: 1rem 0.75rem; font-weight: 800; text-transform: uppercase; font-size: 0.65rem; color: #64748b; white-space: nowrap; }
.custom-table td { padding: 1rem 0.75rem; border-top: 1px solid #f1f5f9; border-bottom: 1px solid #f1f5f9; white-space: nowrap; }
.custom-table td:first-child { border-left: 1px solid #f1f5f9; border-top-left-radius: 0.75rem; border-bottom-left-radius: 0.75rem; }
.custom-table td:last-child { border-right: 1px solid #f1f5f9; border-top-right-radius: 0.75rem; border-bottom-right-radius: 0.75rem; }
.stat-card { background: white; border: 1px solid #f1f5f9; border-radius: 1.5rem; padding: 1.25rem; display: flex; flex-direction: column; gap: 0.5rem; transition: all 0.3s; }
.modal-overlay { position: fixed; inset: 0; background: rgba(15, 23, 42, 0.7); backdrop-filter: blur(5px); z-index: 2147483647 !important; display: none; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.3s; padding: 1rem; }
.modal-overlay.show { display: flex; opacity: 1; }
.modal-content { background: white; border-radius: 1.5rem; padding: 1.5rem; max-width: 600px; width: 100%; max-height: 90vh; overflow-y: auto; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); transform: scale(0.95); transition: transform 0.3s; }
.modal-overlay.show .modal-content { transform: scale(1); }
@media (min-width: 768px) { .modal-content { padding: 2.5rem; border-radius: 2rem; } }
Smart AI System
Menyiapkan modul biometrik…
Absensi Siswa AI
Sistem Kiosk cerdas menggunakan teknologi Biometrik Wajah 128-Descriptor untuk memastikan data kehadiran siswa yang 100% akurat dan terintegrasi 3 Sesi (Datang, Sholat, Pulang).
Mobile First
Dukungan penuh antarmuka responsif.
Tiga Sesi Harian
Catat absensi pagi, ibadah, dan pulang otomatis.
Administrator
Kelola data siswa dan akses menu kiosk scanner.
Otentikasi Admin
Scanner Kiosk
Pilih sesi sebelum mengaktifkan kamera.
Berhenti
Aktivitas Realtime
/**
* ============================================================================
* JAVASCRIPT ENGINE (O(1) PERFORMANCE OPTIMIZED + ANTI-SPAM)
* ============================================================================
*/
const IS_PREVIEW = false;
const GAS_URL = “https://script.google.com/macros/s/AKfycbw38LjuuM5VW_iTfAJJFU_4BXRKj–BD68gkrcF40lUhZE0o9NN4jZAZaABrC8Xn6yRhg/exec”;
let currentStream = null, faceMatcher = null, scannerInterval = null;
let labeledDescriptors = [], isSidebarCollapsed = false, currentFacingMode = “user”, recapMode = ‘harian’;
let cachedSiswa = [], cachedKelas = [], cachedAbsensi = [];
let todayAttended = new Set(), cooldownMap = {};
window.onload = async () => {
const loadText = document.getElementById(‘loading-text’);
try {
try { loadLocalSettings(); } catch(e) { localStorage.removeItem(‘app_v2_libur_mingguan_arr’); localStorage.removeItem(‘app_v2_libur_nasional’); }
updateSystemClock(); setInterval(updateSystemClock, 1000);
const now = new Date();
const y = now.getFullYear(), m = String(now.getMonth()+1).padStart(2,’0′), d = String(now.getDate()).padStart(2,’0′);
if(document.getElementById(‘filter-date’)) document.getElementById(‘filter-date’).value = `${y}-${m}-${d}`;
if(document.getElementById(‘filter-month’)) document.getElementById(‘filter-month’).value = `${y}-${m}`;
if(document.getElementById(‘manual-tanggal’)) document.getElementById(‘manual-tanggal’).value = `${y}-${m}-${d}`;
loadText.innerText = “Mengunduh modul biometrik AI (±5MB)…”;
const MODEL_URL = ‘https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model/’;
await faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL);
await faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL);
await faceapi.nets.faceRecognitionNet.loadFromUri(MODEL_URL);
if (localStorage.getItem(‘app_v2_logged’) === ‘true’) navigate(‘admin’);
else navigate(‘landing’);
document.getElementById(‘loading-overlay’).style.display = ‘none’;
} catch (err) {
loadText.innerHTML = `Terjadi Kesalahan:
${err.message}`;
setTimeout(() => { document.getElementById(‘loading-overlay’).style.display = ‘none’; if (localStorage.getItem(‘app_v2_logged’) === ‘true’) navigate(‘admin’); else navigate(‘landing’); showToast(“Sistem Safe Mode”, “warning”); }, 3000);
}
};
function updateSystemClock() {
const c = document.getElementById(‘ui-clock’);
if(c) c.innerText = new Date().toLocaleTimeString(‘id-ID’, { hour12: false });
}
function navigate(viewId) {
stopCamera(‘all’); document.querySelectorAll(‘.view-section’).forEach(el => el.classList.remove(‘active’)); document.getElementById(`view-${viewId}`).classList.add(‘active’);
if (viewId === ‘admin’) { fetchSiswaData(); fetchRemoteSettings(); switchAdminTab(‘scanner’); }
}
function switchAdminTab(tabName) {
document.querySelectorAll(‘.admin-tab’).forEach(el => { el.classList.remove(‘text-blue-700’, ‘bg-blue-50’, ‘shadow-sm’, ‘border’, ‘border-blue-100’); el.classList.add(‘text-slate-500’); });
const activeTab = document.getElementById(`tab-${tabName}`);
if(activeTab) { activeTab.classList.remove(‘text-slate-500’); activeTab.classList.add(‘text-blue-700’, ‘bg-blue-50’, ‘shadow-sm’, ‘border’, ‘border-blue-100’); }
document.querySelectorAll(‘.admin-content’).forEach(el => { el.classList.remove(‘block’); el.classList.add(‘hidden’); });
const contentTab = document.getElementById(`admin-content-${tabName}`);
if(contentTab) contentTab.classList.replace(‘hidden’, ‘block’);
stopCamera(‘all’);
if (tabName === ‘register’) { updateRegDropdowns(); startCamera(‘reg’); }
if (tabName === ‘siswa’) renderSiswaTable();
if (tabName === ‘absensi’) loadAndRenderRecap();
if (tabName === ‘manual’) updateManualDropdowns();
if (tabName === ‘settings’) renderKelasSettings();
}
function toggleSidebar() {
isSidebarCollapsed = !isSidebarCollapsed;
document.getElementById(‘admin-sidebar’).classList.toggle(‘sidebar-collapsed’, isSidebarCollapsed);
}
function showToast(message, type = ‘info’) {
const container = document.getElementById(‘toast-container’); const el = document.createElement(‘div’); el.className = `toast ${type}`;
const icons = { success: ‘fa-circle-check’, error: ‘fa-triangle-exclamation’, info: ‘fa-circle-info’, warning: ‘fa-exclamation-circle’ };
el.innerHTML = ` ${message}`;
container.appendChild(el);
setTimeout(() => el.classList.add(‘show’), 10); setTimeout(() => { el.classList.remove(‘show’); setTimeout(() => el.remove(), 500); }, 3000);
}
async function fetchSiswaData() {
try {
if(!IS_PREVIEW) {
const res = await fetch(GAS_URL, {method:’POST’, body:JSON.stringify({action:’getGlobalData’})});
const json = await res.json();
if(json.status === ‘success’) { cachedSiswa = json.data.siswa; cachedKelas = json.data.kelas; cachedAbsensi = json.data.absensi; }
}
} catch(e) {}
updateGlobalDropdowns(); if(!document.getElementById(‘admin-content-siswa’).classList.contains(‘hidden’)) renderSiswaTable();
}
function updateGlobalDropdowns() {
const htmlKelas = cachedKelas.map(k => `${k.nama}`).join(”);
const allOp = ‘– Pilih Kelas –‘ + htmlKelas;
const reqOp = ‘– Pilih Kelas –‘ + htmlKelas;
[‘db-filter-kelas’, ‘recap-filter-kelas’, ‘reg-filter-kelas’].forEach(id => { if(document.getElementById(id)) document.getElementById(id).innerHTML = allOp; });
[‘add-sis-kelas’, ‘manual-kelas’].forEach(id => { if(document.getElementById(id)) document.getElementById(id).innerHTML = reqOp; });
}
function renderSiswaTable() {
const tb = document.getElementById(‘table-siswa’); if (!tb) return;
const filter = document.getElementById(‘db-filter-kelas’)?.value || ‘-‘;
const search = document.getElementById(‘db-search’)?.value.toLowerCase().trim() || ”;
if (filter === ‘-‘ && search === ”) { tb.innerHTML = `
`; return; }
let filtered = (cachedSiswa || []).filter(s => filter === ‘-‘ || String(s.kelas||”).trim() === filter.trim());
if(search) filtered = filtered.filter(s => String(s.nama||”).toLowerCase().includes(search) || String(s.identitas||”).toLowerCase().includes(search));
if(filtered.length === 0) { tb.innerHTML = `
`; return; }
tb.innerHTML = filtered.map(s => {
let isRec = false; try { if (s.descriptor && s.descriptor.length > 10) isRec = JSON.parse(s.descriptor).length > 10; } catch(e) {}
const badge = isRec ? `Terekam` : `Belum`;
return `
`;
}).join(”);
}
function goToEnroll(id) {
const s = cachedSiswa.find(x => x.id === id); if(!s) return;
switchAdminTab(‘register’);
setTimeout(() => {
const elKelas = document.getElementById(‘reg-filter-kelas’); if(elKelas) { elKelas.value = s.kelas; onRegKelasChange(); }
const elNama = document.getElementById(‘reg-nama’); if(elNama) { elNama.value = s.nama; onRegNamaChange(); }
showToast(`Kamera siap merekam wajah ${s.nama}`, “info”);
}, 400);
}
function openAddSiswaModal() { document.getElementById(‘modal-add-siswa’).classList.add(‘show’); document.getElementById(‘add-sis-nisn’).value = “”; document.getElementById(‘add-sis-nama’).value = “”; }
function openImportModal() { document.getElementById(‘modal-import’).classList.add(‘show’); document.getElementById(‘import-text-area’).value = “”; }
function closeModals() { document.querySelectorAll(‘.modal-overlay’).forEach(el => el.classList.remove(‘show’)); }
async function submitAddSiswa() {
const identitas = document.getElementById(‘add-sis-nisn’).value.trim(), nama = document.getElementById(‘add-sis-nama’).value.trim(), kelas = document.getElementById(‘add-sis-kelas’).value;
if(!identitas || !nama || !kelas) return showToast(“Isi semua data!”, “error”);
if(!IS_PREVIEW) await fetch(GAS_URL, {method:’POST’, body:JSON.stringify({action:’addSiswa’, identitas, nama, kelas})});
showToast(`Siswa ditambahkan!`, “success”); closeModals(); fetchSiswaData();
}
async function processTextImport() {
const txt = document.getElementById(‘import-text-area’).value.trim(); if(!txt) return showToast(“Kosong!”, “warning”);
const rows = txt.split(‘n’); if(rows.length > 100) return showToast(“Maksimal 100 data.”, “error”);
let buf = [];
for (let r of rows) {
let cols = r.trim().split(‘,’); if(cols.length < 3 && r.trim()!=='') return showToast(`Format salah. Gunakan koma.`, "error");
if(r.trim()!=='') buf.push({identitas: cols[0].trim(), nama: cols[1].trim(), kelas: cols[2].trim()});
}
if(buf.length === 0) return;
const btn = document.getElementById('btn-process-import'); btn.innerText = 'Memproses…'; btn.disabled = true;
try {
if(!IS_PREVIEW) await fetch(GAS_URL, {method:'POST', body:JSON.stringify({action:'importSiswa', data:buf})});
showToast(`${buf.length} Data sukses!`, "success"); closeModals(); fetchSiswaData();
} catch(e) {} finally { btn.innerText = 'Proses Import'; btn.disabled = false; }
}
async function deleteSiswa(id) {
if(!confirm("Hapus permanen?")) return;
if(!IS_PREVIEW) await fetch(GAS_URL, {method:'POST', body:JSON.stringify({action:'deleteSiswa', id})});
showToast("Dihapus.", "success"); fetchSiswaData();
}
function updateRegDropdowns() { document.getElementById('reg-nama').innerHTML = '– Pilih –‘; document.getElementById(‘reg-identitas’).value = “”; }
function onRegKelasChange() {
const k = document.getElementById(‘reg-filter-kelas’).value; if(k === ‘-‘) return updateRegDropdowns();
document.getElementById(‘reg-nama’).innerHTML = ‘– Pilih –‘ + cachedSiswa.filter(s=>s.kelas===k).map(s=>`${s.nama}`).join(”);
document.getElementById(‘reg-identitas’).value = “”;
}
function onRegNamaChange() {
const s = cachedSiswa.find(x => x.nama === document.getElementById(‘reg-nama’).value);
document.getElementById(‘reg-identitas’).value = s ? s.identitas : “”;
}
async function captureAndRegister() {
const kls = document.getElementById(‘reg-filter-kelas’).value, n = document.getElementById(‘reg-nama’).value, id = document.getElementById(‘reg-identitas’).value;
if(!kls || kls===’-‘ || !n || !id) return showToast(“Pilih data valid!”, “warning”);
const v = document.getElementById(‘video-reg’), b = document.getElementById(‘btn-rekam’);
b.disabled = true; b.innerHTML = ‘ Memproses…’;
try {
const d = await faceapi.detectSingleFace(v, new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks().withFaceDescriptor();
if (!d) throw new Error(“Wajah tidak terdeteksi jelas.”);
const desc = JSON.stringify(Array.from(d.descriptor));
if(!IS_PREVIEW) {
const r = await fetch(GAS_URL, { method: ‘POST’, body: JSON.stringify({ action: ‘updateFaceDescriptor’, identitas: id, descriptor: desc }) });
if((await r.json()).status !== ‘success’) throw new Error(“Gagal server”);
}
showToast(`Wajah tersimpan!`, “success”); document.getElementById(‘reg-filter-kelas’).value = “-“; updateRegDropdowns(); fetchSiswaData();
} catch (e) { showToast(e.message, “error”); } finally { b.disabled = false; b.innerHTML = ‘ Simpan Wajah’; }
}
function updateManualDropdowns() { document.getElementById(‘manual-nama’).innerHTML = ‘– Pilih –‘; }
function onManualKelasChange() {
const k = document.getElementById(‘manual-kelas’).value; if(!k) return updateManualDropdowns();
document.getElementById(‘manual-nama’).innerHTML = ‘– Pilih –‘ + cachedSiswa.filter(s=>s.kelas===k).map(s=>`${s.nama} (${s.identitas})`).join(”);
}
// ================= PENCEGAHAN DOUBLE SCAN MANUAL =================
async function submitManualAbsen() {
const tgl = document.getElementById(‘manual-tanggal’).value;
const sesi = document.getElementById(‘manual-sesi’).value;
const kls = document.getElementById(‘manual-kelas’).value;
const nama = document.getElementById(‘manual-nama’).value;
const status = document.querySelector(‘input[name=”man-status”]:checked’).value;
const btn = document.getElementById(‘btn-submit-manual’);
if(!tgl || !kls || !nama) return showToast(“Pilih Tanggal, Kelas, dan Siswa!”, “warning”);
const [y, m, d] = tgl.split(‘-‘);
const dInt = parseInt(d); const mInt = parseInt(m);
const dateVars = [`${dInt}/${mInt}/${y}`, `${d}/${m}/${y}`, `${y}-${m}-${d}`, `${y}-${mInt}-${dInt}`, `${d}-${m}-${y}`, `${dInt}-${mInt}-${y}`];
// Mencegah input ganda dari menu manual
const isDuplicate = cachedAbsensi.find(a => a.nama === nama && parseAbsen(a.ket).mode === sesi && dateVars.some(fmt => String(a.timestamp).includes(fmt)));
if (isDuplicate) {
return showToast(`Data Ditolak: Siswa tersebut sudah memiliki data absen ${sesi} pada tanggal ini!`, “error”);
}
btn.disabled = true; btn.innerHTML = ‘ Menyimpan…’;
const timeStr = new Date().toLocaleTimeString(‘id-ID’, { hour12: false });
const timestamp = `${dInt}/${mInt}/${y}, ${timeStr}`;
const ketDatabase = `${sesi}-${status}`;
cachedAbsensi.push({ timestamp, nama, kelas: kls, ket: ketDatabase }); // Update memori lokal segera
if(!IS_PREVIEW) await fetch(GAS_URL, {method:’POST’, body:JSON.stringify({action:’recordAbsen’, nama, kelas: kls, ket: ketDatabase, timestamp})});
setTimeout(() => {
showToast(`Manual: ${nama} (${sesi} – ${status}) disimpan.`, “success”);
btn.disabled = false; btn.innerHTML = ‘ Simpan Data Absen’;
document.getElementById(‘manual-kelas’).value = “”; updateManualDropdowns();
document.querySelector(‘input[name=”man-status”][value=”Hadir”]’).checked = true;
renderRecap();
}, 800);
}
async function startCamera(mode) {
const v = document.getElementById(mode === ‘reg’ ? ‘video-reg’ : ‘video-kiosk’), s = document.getElementById(mode === ‘reg’ ? null : ‘kiosk-status’), sl = document.getElementById(‘kiosk-scan-line’);
try {
if (currentStream) stopCamera(‘all’);
currentStream = await navigator.mediaDevices.getUserMedia({video:{facingMode:currentFacingMode, width:{ideal:640}, height:{ideal:480}}, audio:false});
v.srcObject = currentStream;
if (s) { s.innerHTML = `
Aktif`; s.className = “px-4 md:px-6 py-2 md:py-3 bg-emerald-50 text-emerald-600 rounded-xl text-[10px] md:text-xs font-black uppercase flex items-center gap-2”; if(sl) sl.style.display = ‘block’; }
} catch (e) { showToast(“Kamera error/ditolak.”, “error”); }
}
function toggleCameraMode(mode) { currentFacingMode = currentFacingMode === “user” ? “environment” : “user”; startCamera(mode); }
function stopCamera(mode) {
if (currentStream) { currentStream.getTracks().forEach(t => t.stop()); currentStream = null; }
if (scannerInterval) { clearInterval(scannerInterval); scannerInterval = null; }
const ks = document.getElementById(‘kiosk-status’), sl = document.getElementById(‘kiosk-scan-line’);
if (ks && (mode === ‘kiosk’ || mode === ‘all’)) {
ks.innerHTML = `
Berhenti`; ks.className = “px-4 md:px-6 py-2 md:py-3 bg-rose-50 text-rose-600 rounded-xl text-[10px] md:text-xs font-black uppercase flex items-center gap-2”;
if(sl) sl.style.display = ‘none’;
const cv = document.getElementById(‘canvas-kiosk’); if(cv) cv.getContext(‘2d’).clearRect(0,0,cv.width,cv.height);
}
}
function prepareAIDescriptors() {
labeledDescriptors = [];
cachedSiswa.forEach(p => { try { let d = JSON.parse(p.descriptor); if(d.length===128) labeledDescriptors.push(new faceapi.LabeledFaceDescriptors(p.nama, [new Float32Array(d)])); } catch(e){} });
}
async function startKioskScanner() {
prepareAIDescriptors();
if (labeledDescriptors.length === 0) return showToast(“Tidak ada wajah terekam.”, “error”);
faceMatcher = new faceapi.FaceMatcher(labeledDescriptors, 0.55);
await startCamera(‘kiosk’);
const v = document.getElementById(‘video-kiosk’), cv = document.getElementById(‘canvas-kiosk’);
v.onplay = () => {
const resizeCanvas = () => { const s = { width: v.clientWidth, height: v.clientHeight }; faceapi.matchDimensions(cv, s); return s; };
let ds = resizeCanvas(); window.addEventListener(‘resize’, () => { ds = resizeCanvas(); });
scannerInterval = setInterval(async () => {
if(!v.paused && !v.ended) {
const det = await faceapi.detectAllFaces(v, new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks().withFaceDescriptors();
const res = faceapi.resizeResults(det, ds);
const ctx = cv.getContext(‘2d’); ctx.clearRect(0,0,cv.width,cv.height);
res.forEach(d => {
const m = faceMatcher.findBestMatch(d.descriptor); let txt = m.label, col = “#10b981″;
const modeSesi = document.querySelector(‘input[name=”kiosk-mode”]:checked’).value;
if (txt === ‘unknown’) { txt = “??”; col = “#ef4444”; }
else {
triggerAttendanceSuccess(txt, cachedSiswa.find(x=>x.nama===txt)?.kelas || “-“, `${modeSesi}-Hadir`, modeSesi);
}
new faceapi.draw.DrawBox(d.detection.box, {label: txt, boxColor: col, lineWidth:2}).draw(cv);
});
}
}, 800);
};
}
// ================= PENCEGAHAN DOUBLE SCAN KIOSK =================
async function triggerAttendanceSuccess(nm, kl, ket, modeSesi) {
const n = Date.now();
const now = new Date();
const yStr = now.getFullYear();
const dInt = now.getDate();
const mInt = now.getMonth() + 1;
const dStr = String(dInt).padStart(2,’0′);
const mStr = String(mInt).padStart(2,’0′);
const todayVars = [`${dInt}/${mInt}/${yStr}`, `${dStr}/${mStr}/${yStr}`, `${yStr}-${mStr}-${dStr}`, `${yStr}-${mInt}-${dInt}`, `${dStr}-${mStr}-${yStr}`, `${dInt}-${mInt}-${yStr}`];
// Cek apakah siswa sudah punya data sesi ini hari ini
const isDuplicate = cachedAbsensi.find(a => a.nama === nm && parseAbsen(a.ket).mode === modeSesi && todayVars.some(fmt => String(a.timestamp).includes(fmt)));
if (isDuplicate) {
const spamKey = nm + modeSesi + “_spam”;
if (!cooldownMap[spamKey] || (n – cooldownMap[spamKey] > 5000)) {
showToast(`Wajah ditolak: ${nm} sudah absen ${modeSesi} hari ini.`, “warning”);
cooldownMap[spamKey] = n;
}
return;
}
const logKey = nm + modeSesi;
if (cooldownMap[logKey] && (n – cooldownMap[logKey] < 15000)) return;
cooldownMap[logKey] = n;
todayAttended.add(logKey);
const timeStr = now.toLocaleTimeString('id-ID', { hour12: false });
const ts = `${dInt}/${mInt}/${yStr}, ${timeStr}`;
// Update cache seketika (menghentikan double record biarpun belum di refresh)
cachedAbsensi.push({ timestamp: ts, nama: nm, kelas: kl, ket: ket });
logToKioskDisplay(nm, kl, ts, modeSesi);
playSuccessNotification();
if (!IS_PREVIEW) fetch(GAS_URL, { method: 'POST', body: JSON.stringify({ action: 'recordAbsen', nama:nm, kelas:kl, timestamp:ts, ket:ket }) });
}
function logToKioskDisplay(nm, kl, ts, modeSesi) {
const lg = document.getElementById('kiosk-log'); if (lg.innerHTML.includes('Belum Ada')) lg.innerHTML = "";
let icon = "fa-check"; let color = "text-emerald-600 bg-emerald-50 border-emerald-100";
if(modeSesi === 'Sholat') { icon = "fa-mosque"; color = "text-blue-600 bg-blue-50 border-blue-100"; }
else if(modeSesi === 'Pulang') { icon = "fa-house"; color = "text-amber-600 bg-amber-50 border-amber-100"; }
const el = document.createElement('div'); el.className = "p-3 bg-white border border-slate-100 rounded-xl flex items-center gap-3 shadow-sm";
el.innerHTML = `
${nm}
${kl} • ${ts.split(‘,’)[1]} • ${modeSesi}
`;
lg.prepend(el);
}
function playSuccessNotification() { try { const a=new(window.AudioContext||window.webkitAudioContext)(); const o=a.createOscillator(); o.type=”sine”; o.frequency.setValueAtTime(800,a.currentTime); o.connect(a.destination); o.start(); o.stop(a.currentTime+0.1); }catch(e){} }
function parseAbsen(ketRaw) {
if (!ketRaw) return { mode: ‘Datang’, status: ‘Alpa’ };
if (!ketRaw.includes(‘-‘)) {
if ([‘Hadir’, ‘Sakit’, ‘Izin’, ‘Alpa’].includes(ketRaw)) return { mode: ‘Datang’, status: ketRaw };
return { mode: ‘Datang’, status: ketRaw };
}
const parts = ketRaw.split(‘-‘);
return { mode: parts[0], status: parts[1] };
}
function setRecapMode(m) {
recapMode = m; const h = document.getElementById(‘btn-mode-harian’), b = document.getElementById(‘btn-mode-bulanan’);
if (m === ‘harian’) {
h.className = “flex-1 lg:w-32 py-2 md:py-3 bg-white text-blue-700 shadow rounded-lg md:rounded-xl font-black text-[10px] md:text-xs uppercase transition-all”; b.className = “flex-1 lg:w-32 py-2 md:py-3 text-slate-500 rounded-lg md:rounded-xl font-black text-[10px] md:text-xs uppercase transition-all”;
document.getElementById(‘f-d-container’).classList.remove(‘hidden’); document.getElementById(‘f-m-container’).classList.add(‘hidden’);
document.getElementById(‘recap-legend’).classList.add(‘hidden’);
} else {
b.className = “flex-1 lg:w-32 py-2 md:py-3 bg-white text-blue-700 shadow rounded-lg md:rounded-xl font-black text-[10px] md:text-xs uppercase transition-all”; h.className = “flex-1 lg:w-32 py-2 md:py-3 text-slate-500 rounded-lg md:rounded-xl font-black text-[10px] md:text-xs uppercase transition-all”;
document.getElementById(‘f-m-container’).classList.remove(‘hidden’); document.getElementById(‘f-d-container’).classList.add(‘hidden’);
document.getElementById(‘recap-legend’).classList.remove(‘hidden’);
}
renderRecap();
}
async function loadAndRenderRecap() {
document.getElementById(‘table-recap-harian’).innerHTML = ‘
‘;
if (!IS_PREVIEW) try { const r=await fetch(GAS_URL,{method:’POST’,body:JSON.stringify({action:’getGlobalData’})}); const j=await r.json(); if(j.status===’success’) cachedAbsensi=j.data.absensi; }catch(e){}
renderRecap();
}
function getHariEfektifCount(y, m) {
const t = new Date(y, m, 0).getDate(); let a = 0;
const lNas = (localStorage.getItem(‘app_v2_libur_nasional’)||””).split(‘,’).map(s=>s.trim());
const lMin = JSON.parse(localStorage.getItem(‘app_v2_libur_mingguan_arr’)||”[0]”);
for(let i=1; i<=t; i++) {
const d = new Date(y, m-1, i), ymd = `${y}-${String(m).padStart(2,'0')}-${String(i).padStart(2,'0')}`;
if(!lMin.includes(d.getDay()) && !lNas.includes(ymd)) a++;
} return { total: t, aktif: a };
}
function getStandardDate(tsStr) {
let raw = String(tsStr).split(',')[0].split('T')[0].trim();
if(raw.includes(' ')) raw = raw.split(' ')[0];
if (raw.includes('/')) {
const p = raw.split('/');
if (p[2] && p[2].length === 4) return `${p[2]}-${p[1].padStart(2, '0')}-${p[0].padStart(2, '0')}`;
}
if (raw.includes('-')) {
const p = raw.split('-');
if (p[0] && p[0].length === 4) return `${p[0]}-${p[1].padStart(2, '0')}-${p[2].padStart(2, '0')}`;
else if (p[2] && p[2].length === 4) return `${p[2]}-${p[1].padStart(2, '0')}-${p[0].padStart(2, '0')}`;
}
return raw;
}
function renderRecap() {
const tb = document.getElementById('table-recap-harian'); if (!tb) return;
try {
const k = document.getElementById('recap-filter-kelas').value, th = document.getElementById('recap-table-head'), st = document.getElementById('stat-container');
if (k === '-') { th.innerHTML=''; tb.innerHTML='
‘; st.innerHTML=”; return; }
const abL = cachedAbsensi || [], sis = (cachedSiswa||[]).filter(p=>String(p.kelas||”).trim()===k.trim());
const showD = document.getElementById(‘filter-show-datang’).checked;
const showS = document.getElementById(‘filter-show-sholat’).checked;
const showP = document.getElementById(‘filter-show-pulang’).checked;
const logMap = {};
abL.forEach(a => {
const stdDate = getStandardDate(a.timestamp);
const key = `${a.nama}_${stdDate}`;
if (!logMap[key]) logMap[key] = [];
logMap[key].push(a);
});
if (recapMode === ‘harian’) {
const fd = document.getElementById(‘filter-date’).value; if(!fd){ tb.innerHTML=’
‘; return; }
let htmlTh = ‘
‘;
if(showD) htmlTh += ‘
‘;
if(showS) htmlTh += ‘
‘;
if(showP) htmlTh += ‘
‘;
th.innerHTML = htmlTh;
let h=0,i=0,s=0,a=0,html=”;
if(sis.length===0) html=’
‘;
else {
sis.forEach(p => {
const logs = logMap[`${p.nama}_${fd}`] || [];
const renderCell = (modeName) => {
const abs = logs.find(l => parseAbsen(l.ket).mode === modeName);
let bdg = `A`;
let jam = ‘-‘;
if(abs) {
const parsed = parseAbsen(abs.ket);
if(parsed.status===’Hadir’){ bdg=`Hadir`; h++;}
else if(parsed.status===’Izin’){ bdg=`Izin`; i++;h++;}
else if(parsed.status===’Sakit’){ bdg=`Sakit`; s++;h++;}
else bdg=`${parsed.status}`;
// PERBAIKAN FORMAT JAM REGEX
const tsStr = String(abs.timestamp);
const timeMatch = tsStr.match(/d{2}[:.]d{2}[:.]d{2}/);
if (timeMatch) jam = timeMatch[0].replace(/./g, ‘:’);
} else a++;
return `
`;
};
html+=`
${showD ? renderCell(‘Datang’) : ”}
${showS ? renderCell(‘Sholat’) : ”}
${showP ? renderCell(‘Pulang’) : ”}
`;
});
} tb.innerHTML = html;
const targetSesi = (showD?1:0)+(showS?1:0)+(showP?1:0);
const pct = (sis.length*targetSesi)>0?Math.round((h/(sis.length*targetSesi))*100):0;
st.innerHTML=`
Siswa
${sis.length}
Total Log Hadir
${h}
Total Log Alpa
${a}
% Kedisiplinan
${pct}%
`;
} else {
// ========== MODE BULANAN ==========
const mv = document.getElementById(‘filter-month’).value; if (!mv) { tb.innerHTML = ‘
‘; return; }
const ym = mv.split(‘-‘); const yI=parseInt(ym[0]), mI=parseInt(ym[1]);
const {total, aktif} = getHariEfektifCount(yI, mI);
let thHtml = ‘
‘;
for(let i=1; i<=total; i++) thHtml += `
`;
thHtml += `
`;
th.innerHTML = thHtml;
let tkH = 0, html = ”;
if(sis.length===0) html=`
`;
else {
sis.forEach(p => {
let tr = `
`;
let cH=0, cI=0, cS=0, cA=0;
for(let i=1; ix.trim());
const isNasional = lN.includes(targetDate);
const isMingguan = lM.includes(d.getDay());
if (isNasional || isMingguan) {
[…(showD?[‘Datang’]:[]), …(showS?[‘Sholat’]:[]), …(showP?[‘Pulang’]:[])].forEach(modeName => {
const abs = logs.find(l => parseAbsen(l.ket).mode === modeName);
if (abs) {
const st = parseAbsen(abs.ket).status;
if(st===’Hadir’) cH++; else if(st===’Izin’) cI++; else if(st===’Sakit’) cS++;
}
});
const textLibur = isNasional ? “Libur” : [‘Minggu’, ‘Senin’, ‘Selasa’, ‘Rabu’, ‘Kamis’, ‘Jumat’, ‘Sabtu’][d.getDay()];
const colorClass = isNasional ? “text-rose-400” : “text-slate-400”;
tr+=`
`;
} else {
let dayH = 0, dayI = 0, dayS = 0, dayA = 0;
const getStatusHuruf = (modeName) => {
const abs = logs.find(l => parseAbsen(l.ket).mode === modeName);
if(abs) {
const st = parseAbsen(abs.ket).status;
if(st===’Hadir’){ dayH++; return `H`; }
if(st===’Izin’){ dayI++; return `I`; }
if(st===’Sakit’){ dayS++; return `S`; }
dayA++; return `A`;
}
dayA++; return `A`;
};
let lines = [];
if(showD) lines.push(`D:${getStatusHuruf(‘Datang’)}`);
if(showS) lines.push(`S:${getStatusHuruf(‘Sholat’)}`);
if(showP) lines.push(`P:${getStatusHuruf(‘Pulang’)}`);
let cellContent = `
‘)}
`;
// Kalkulasi total bulanan
cH += dayH; cI += dayI; cS += dayS;
// PERBAIKAN LOGIKA ALPA: 1 HARI PENUH = 1 ALPA
if (dayA > 0 && dayH === 0 && dayI === 0 && dayS === 0) {
cA += 1;
}
tr+=`
`;
}
}
tkH += cH;
tr += `
`;
html += tr;
});
} tb.innerHTML = html;
const targetSesi = (showD?1:0)+(showS?1:0)+(showP?1:0);
const avg = (sis.length*aktif*targetSesi)>0 ? Math.round((tkH/(sis.length*aktif*targetSesi))*100) : 0;
st.innerHTML=`
Siswa
${sis.length}
Efektif per Sesi
${aktif} hr
Total Log
${abL.length}
Rata2 Kedisiplinan
${avg}%
`;
}
} catch(e) { tb.innerHTML=`
`; }
}
async function downloadPDF() {
const { jsPDF } = window.jspdf;
const orient = recapMode === ‘bulanan’ ? ‘landscape’ : ‘portrait’;
const doc = new jsPDF(orient);
const instansi = localStorage.getItem(‘app_v2_nama’) || “Sekolah / Lembaga”;
const alamat = localStorage.getItem(‘app_v2_alamat’) || “Alamat Belum Diatur”;
const pimpinan = localStorage.getItem(‘app_v2_atasan’) || “Nama Pimpinan”;
const idNomor = localStorage.getItem(‘app_v2_id_nomor’) || “NIP. -“;
const klsFilter = document.getElementById(‘recap-filter-kelas’).value;
let periodeText = “”;
if (recapMode === ‘harian’) {
const fd = document.getElementById(‘filter-date’).value;
periodeText = fd ? fd.split(‘-‘).reverse().join(‘/’) : “-“;
} else {
const fm = document.getElementById(‘filter-month’).value;
if(fm) {
const [y, m] = fm.split(‘-‘);
const monthNames = [“Januari”, “Februari”, “Maret”, “April”, “Mei”, “Juni”, “Juli”, “Agustus”, “September”, “Oktober”, “November”, “Desember”];
periodeText = `${monthNames[parseInt(m)-1]} ${y}`;
} else {
periodeText = “-“;
}
}
const pageWidth = doc.internal.pageSize.getWidth();
// Header
doc.setFontSize(16); doc.setFont(“helvetica”, “bold”);
doc.text(instansi.toUpperCase(), pageWidth/2, 15, { align: “center” });
doc.setFontSize(9); doc.setFont(“helvetica”, “normal”);
doc.text(alamat, pageWidth/2, 21, { align: “center” });
doc.setLineWidth(0.5); doc.line(14, 25, pageWidth – 14, 25);
doc.setFontSize(11); doc.setFont(“helvetica”, “bold”);
doc.text(`LAPORAN ABSENSI ${recapMode.toUpperCase()}`, pageWidth/2, 32, { align: “center” });
doc.setFontSize(10); doc.setFont(“helvetica”, “normal”);
doc.text(`Kelas: ${klsFilter}`, 14, 40);
doc.text(`${recapMode === ‘harian’ ? ‘Tanggal’ : ‘Bulan’}: ${periodeText}`, 14, 46);
// Render Table ke PDF (AutoTable API)
doc.autoTable({
html: ‘#recap-table’,
startY: 50,
theme: ‘grid’,
styles: { fontSize: recapMode === ‘bulanan’ ? 5 : 9, cellPadding: recapMode === ‘bulanan’ ? 0.5 : 2, minCellHeight: 8, valign: ‘middle’, halign: ‘center’, lineColor: [200, 200, 200] },
headStyles: { fillColor: [15, 23, 42], textColor: 255, halign: ‘center’, valign: ‘middle’ },
columnStyles: { 0: { halign: ‘center’ }, 1: { halign: ‘left’ } },
didParseCell: function(data) {
// Parsing untuk cell Hari Libur/Minggu agar tulisannya Vertikal Kebawah di PDF
if (recapMode === ‘bulanan’ && data.section === ‘body’) {
if (data.column.index >= 2 && data.column.index doc.internal.pageSize.getHeight() – 40) {
doc.addPage();
finalY = 20;
}
doc.setFontSize(8);
doc.setFont(“helvetica”, “bold”);
doc.text(“Keterangan:”, 14, finalY);
doc.setFont(“helvetica”, “normal”);
doc.text(“D : Datang”, 14, finalY + 5);
doc.text(“S : Sholat”, 14, finalY + 9);
doc.text(“P : Pulang”, 14, finalY + 13);
doc.text(“H : Hadir”, 35, finalY + 5);
doc.text(“I : Izin”, 35, finalY + 9);
doc.text(“S : Sakit”, 35, finalY + 13);
doc.text(“A : Alpa”, 35, finalY + 17);
doc.text(“- : Libur”, 35, finalY + 21);
doc.setFontSize(10);
doc.text(`Dicetak pada: ${new Date().toLocaleDateString(‘id-ID’)}`, pageWidth – 70, finalY);
doc.text(“Mengetahui,”, pageWidth – 70, finalY + 5);
doc.text(“Kepala Sekolah”, pageWidth – 70, finalY + 10);
doc.setFont(“helvetica”, “bold”);
doc.text(pimpinan, pageWidth – 70, finalY + 30);
doc.setFont(“helvetica”, “normal”);
doc.text(idNomor, pageWidth – 70, finalY + 35);
doc.save(`Rekap_${klsFilter}_${periodeText.replace(/ /g,”_”)}.pdf`);
showToast(“Laporan PDF berhasil diunduh.”, “success”);
}
async function handleLogin(e) {
e.preventDefault(); const u=document.getElementById(‘login-user’).value, p=document.getElementById(‘login-pass’).value, b=document.getElementById(‘btn-login’);
b.disabled=true; b.innerHTML=’‘;
try {
if(IS_PREVIEW){ if(u===’admin’&&p===’admin2026’){ localStorage.setItem(‘app_v2_logged’,’true’); navigate(‘admin’); } else throw new Error(“Salah!”); }
else { const r=await fetch(GAS_URL,{method:’POST’,body:JSON.stringify({action:’loginAdmin’,user:u,pass:p})}); const j=await r.json(); if(j.status===’success’){ localStorage.setItem(‘app_v2_logged’,’true’); navigate(‘admin’); } else throw new Error(j.message); }
} catch(err) { showToast(err.message, “error”); } finally { b.disabled=false; b.innerHTML=’Verifikasi Akun ‘; }
}
function logout() { if(confirm(“Keluar sesi?”)) { localStorage.removeItem(‘app_v2_logged’); navigate(‘landing’); } }
function loadLocalSettings() {
[‘nama’,’atasan’,’alamat’,’id_nomor’].forEach(f=>{ const v=localStorage.getItem(‘app_v2_’+f); if(v && document.getElementById(‘set-‘+f.replace(‘_’,’-‘))) document.getElementById(‘set-‘+f.replace(‘_’,’-‘)).value=v; if(f===’nama’&&document.getElementById(‘ui-nama-lembaga’)&&v) document.getElementById(‘ui-nama-lembaga’).innerText=v; });
const lA=JSON.parse(localStorage.getItem(‘app_v2_libur_mingguan_arr’)||”[0]”); document.querySelectorAll(‘#set-libur-mingguan input’).forEach(c=>c.checked=lA.includes(parseInt(c.value)));
if(document.getElementById(‘set-libur-nasional’)) document.getElementById(‘set-libur-nasional’).value=localStorage.getItem(‘app_v2_libur_nasional’)||””;
}
function renderKelasSettings() {
const list=document.getElementById(‘list-pengaturan-kelas’); if(!list) return;
list.innerHTML=cachedKelas.map(k=>`
`).join(”);
}
async function addNewKelas() {
const n=document.getElementById(‘set-new-kelas’).value.trim(); if(!n) return;
if(IS_PREVIEW) dummyKelas.push({id:’K-‘+Date.now(),nama:n}); else await fetch(GAS_URL,{method:’POST’,body:JSON.stringify({action:’addKelas’,nama:n})});
document.getElementById(‘set-new-kelas’).value=””; fetchSiswaData(); setTimeout(renderKelasSettings,500); showToast(“Kelas ditambah”, “success”);
}
async function deleteKelas(id) {
if(!confirm(“Hapus kelas?”)) return;
if(IS_PREVIEW) dummyKelas=dummyKelas.filter(k=>k.id!==id); else await fetch(GAS_URL,{method:’POST’,body:JSON.stringify({action:’deleteKelas’,id})});
fetchSiswaData(); setTimeout(renderKelasSettings,500);
}
async function fetchRemoteSettings() {
if(IS_PREVIEW) return;
try { const r=await fetch(GAS_URL,{method:’POST’,body:JSON.stringify({action:’getGlobalData’})}); const j=await r.json(); if(j.status===’success’){ for(const [k,v] of Object.entries(j.data.settings)){ localStorage.setItem(‘app_v2_’+k.replace(‘app_’,”),v); if(k===’app_libur_mingguan’) localStorage.setItem(‘app_v2_libur_mingguan_arr’,v); } loadLocalSettings(); } }catch(e){}
}
async function saveSettings() {
let a=[]; document.querySelectorAll(‘#set-libur-mingguan input:checked’).forEach(c=>a.push(parseInt(c.value))); const lS=JSON.stringify(a);
const d={ app_nama:document.getElementById(‘set-nama’).value, app_alamat:document.getElementById(‘set-alamat’).value, app_atasan:document.getElementById(‘set-atasan’).value, app_id_nomor:document.getElementById(‘set-id-nomor’).value, app_libur_mingguan:lS, app_libur_nasional:document.getElementById(‘set-libur-nasional’).value };
for(const [k,v] of Object.entries(d)) localStorage.setItem(‘app_v2_’+k.replace(‘app_’,”),v); localStorage.setItem(‘app_v2_libur_mingguan_arr’,lS);
if(!IS_PREVIEW) { document.getElementById(‘btn-save-settings’).innerHTML=’ Menyinkronkan…’; await fetch(GAS_URL,{method:’POST’,body:JSON.stringify({action:’saveSettings’,data:d})}); document.getElementById(‘btn-save-settings’).innerHTML=’ Simpan Pengaturan’; }
showToast(“Pengaturan tersimpan”, “success”); loadLocalSettings();
}
