(function () { // Screens var screenWelcome = document.getElementById('screen-welcome'); var screenLogin = document.getElementById('screen-login'); var screenRegisterNisn = document.getElementById('screen-register-nisn'); var screenRegisterPin = document.getElementById('screen-register-pin'); var screenForgotPin = document.getElementById('screen-forgot-pin'); var screenHome = document.getElementById('screen-home'); // Header & settings var btnOpenSettings = document.getElementById('btn-open-settings'); var settingsModal = document.getElementById('settings-modal'); var btnCloseSettings = document.getElementById('btn-close-settings'); var backendUrlInput = document.getElementById('backend-url'); var btnSaveConfig = document.getElementById('btn-save-config'); var configStatus = document.getElementById('config-status'); // Registration step 1 var regNisnInput = document.getElementById('reg-nisn'); var btnCheckNisn = document.getElementById('btn-check-nisn'); var regNisnStatus = document.getElementById('reg-nisn-status'); // Registration step 2 var regStudentSummary = document.getElementById('reg-student-summary'); var regClassSelect = document.getElementById('reg-class'); var regPinInput = document.getElementById('reg-pin'); var regPin2Input = document.getElementById('reg-pin2'); var btnCompleteRegister = document.getElementById('btn-complete-register'); var regPinStatus = document.getElementById('reg-pin-status'); // Login var loginNisnInput = document.getElementById('login-nisn'); var loginPinInput = document.getElementById('login-pin'); var btnLogin = document.getElementById('btn-login'); var loginStatus = document.getElementById('login-status'); var forgotNisnInput = document.getElementById('forgot-nisn'); var forgotNewPinInput = document.getElementById('forgot-new-pin'); var forgotNewPin2Input = document.getElementById('forgot-new-pin2'); var forgotPinStatus = document.getElementById('forgot-pin-status'); var btnResetPin = document.getElementById('btn-reset-pin'); // Home screen var homeStudentSummary = document.getElementById('home-student-summary'); var btnAbsenMasuk = document.getElementById('btn-absen-masuk'); var btnAbsenPulang = document.getElementById('btn-absen-pulang'); var btnRefreshLocation = document.getElementById('btn-refresh-location'); var locationStatus = document.getElementById('location-status'); var resultBox = document.getElementById('result-box'); var resultStatus = document.getElementById('result-status'); var resultMessage = document.getElementById('result-message'); var resultIcon = document.getElementById('result-icon'); var resultCloseBtn = document.getElementById('result-close-btn'); var resultMeta = document.getElementById('result-meta'); var masukStatusEl = document.getElementById('masuk-status'); var pulangStatusEl = document.getElementById('pulang-status'); var homeDateLabel = document.getElementById('home-date-label'); var currentCheckinType = null; var pendingMasukPulangType = null; var toastContainer = document.getElementById('toast-container'); var currentLat = null; var currentLng = null; var currentStudentProfile = null; var cameraModal = document.getElementById('camera-modal'); var cameraVideo = document.getElementById('camera-video'); var cameraError = document.getElementById('camera-error'); var cameraFaceStatus = document.getElementById('camera-face-status'); var cameraBtnCancel = document.getElementById('camera-modal-close'); var cameraBtnCancel2 = document.getElementById('camera-btn-cancel'); var cameraStream = null; var faceDetectIntervalId = null; var faceDetectConsecutiveCount = 0; var faceDetectSubmitting = false; var scanQrModal = document.getElementById('scan-qr-modal'); var scanQrClose = document.getElementById('scan-qr-close'); var scanQrStatus = document.getElementById('scan-qr-status'); var qrReader = document.getElementById('qr-reader'); var qrPinModal = document.getElementById('qr-pin-modal'); var qrPinClose = document.getElementById('qr-pin-close'); var qrPinBackdrop = document.getElementById('qr-pin-backdrop'); var qrPinInput = document.getElementById('qr-pin-input'); var qrPinCancel = document.getElementById('qr-pin-cancel'); var qrPinSubmit = document.getElementById('qr-pin-submit'); var pendingQrToken = null; var html5QrCodeInstance = null; // Enroll wajah saat daftar var enrollFaceModal = document.getElementById('enroll-face-modal'); var enrollFaceVideo = document.getElementById('enroll-face-video'); var enrollFaceStatus = document.getElementById('enroll-face-status'); var enrollFaceError = document.getElementById('enroll-face-error'); var enrollFaceStart = document.getElementById('enroll-face-start'); var enrollFaceBackdrop = document.getElementById('enroll-face-backdrop'); var enrollStream = null; var enrollIntervalId = null; var enrollCollectedImages = []; var enrollTargetCount = 5; var enrollMinCount = 3; var enrollSubmitting = false; // Device credentials — bisa diisi di Pengaturan, default untuk development var DEFAULT_DEVICE_CODE = 'MOBILE_APP'; var DEFAULT_API_KEY = 'MOBILE_APP_SECRET'; function showToast(message, type) { if (!toastContainer) return; type = type || 'info'; var el = document.createElement('div'); el.className = 'toast ' + (type === 'error' ? 'toast-error' : type === 'success' ? 'toast-success' : 'toast-info'); el.textContent = message; toastContainer.appendChild(el); setTimeout(function () { el.remove(); }, 3000); } function loadConfig() { try { var raw = localStorage.getItem('sman1_device_config'); if (!raw) return; var cfg = JSON.parse(raw); if (cfg.backendUrl) backendUrlInput.value = cfg.backendUrl; } catch (e) { // ignore } } function saveConfig() { var backendUrl = backendUrlInput.value.trim(); if (!backendUrl) { configStatus.textContent = 'Backend URL wajib diisi.'; configStatus.style.color = '#f97316'; showToast('Backend URL belum diisi', 'error'); return; } var cfg = { backendUrl: backendUrl }; try { localStorage.setItem('sman1_device_config', JSON.stringify(cfg)); configStatus.textContent = 'Konfigurasi disimpan.'; configStatus.style.color = '#22c55e'; showToast('Konfigurasi disimpan', 'success'); testConnection(); } catch (e) { configStatus.textContent = 'Gagal menyimpan konfigurasi di browser.'; configStatus.style.color = '#f97316'; } } function testConnection() { var url = backendBase() + '/api/mobile/ping'; configStatus.textContent = 'Mengecek koneksi…'; configStatus.style.color = '#64748b'; fetch(url, { method: 'GET', headers: { 'Accept': 'application/json' } }) .then(function (res) { return res.json(); }) .then(function (data) { configStatus.textContent = 'Server OK — ' + (data.message || 'Terhubung'); configStatus.style.color = '#22c55e'; showToast('Server terhubung', 'success'); }) .catch(function (err) { var msg = err && err.message ? err.message : 'Tidak bisa terhubung'; configStatus.textContent = 'Gagal: ' + msg; configStatus.style.color = '#dc2626'; showToast('Gagal: ' + msg, 'error'); }); } function openSettingsModal() { settingsModal.classList.remove('hidden'); } function closeSettingsModal() { settingsModal.classList.add('hidden'); } function updateLocationStatus(text, isError) { locationStatus.textContent = 'Lokasi: ' + text; locationStatus.style.borderColor = isError ? 'rgba(220,38,38,0.5)' : 'rgba(148,163,184,0.4)'; locationStatus.style.color = isError ? '#fecaca' : '#9ca3af'; } function getLocation() { if (!navigator.geolocation) { updateLocationStatus('Geolocation tidak didukung di perangkat ini', true); showToast('Geolocation tidak didukung', 'error'); return; } updateLocationStatus('Mengambil lokasi…', false); navigator.geolocation.getCurrentPosition( function (pos) { currentLat = pos.coords.latitude; currentLng = pos.coords.longitude; updateLocationStatus(currentLat.toFixed(6) + ', ' + currentLng.toFixed(6), false); }, function (err) { currentLat = null; currentLng = null; updateLocationStatus('Gagal mengambil lokasi: ' + err.message, true); showToast('Gagal mengambil lokasi', 'error'); }, { enableHighAccuracy: true, timeout: 8000, maximumAge: 0 } ); } function setResult(statusCode, message, metaText) { resultBox.classList.remove('hidden'); resultBox.classList.remove('success', 'warning', 'danger'); var type = 'danger'; var upper = (statusCode || '').toUpperCase(); if (upper === 'PRESENT') type = 'success'; else if (upper === 'LATE') type = 'warning'; resultBox.classList.add(type); resultStatus.className = 'result-popup-status ' + type; resultStatus.textContent = upper || '-'; resultMessage.textContent = message || ''; if (resultIcon) { resultIcon.className = 'result-popup-icon'; resultIcon.textContent = type === 'success' ? '✓' : type === 'warning' ? '⚠' : '✕'; } if (resultMeta) resultMeta.textContent = metaText || ''; } function backendBase() { var backendUrl = backendUrlInput.value.trim(); return backendUrl.replace(/\/+$/, ''); } function ensureConfig() { var backendUrl = backendUrlInput.value.trim(); if (!backendUrl) { showToast('Isi URL server SMAN 1 Garut terlebih dahulu.', 'error'); openSettingsModal(); return false; } return true; } function renderStudentSummary(container, profile) { if (!profile) { container.textContent = ''; return; } container.innerHTML = '
' + (profile.name || '-') + '
' + '
NISN: ' + (profile.nisn || '-') + '
' + '
Kelas: ' + (profile.class_label || '-') + '
'; if (container === homeStudentSummary) { var greetingEl = document.getElementById('home-greeting-text'); if (greetingEl && profile.name) { var firstName = profile.name.split(/\s+/)[0] || profile.name; greetingEl.textContent = 'Halo, ' + firstName; } } } function goToScreen(name) { screenWelcome.classList.add('hidden'); screenLogin.classList.add('hidden'); screenRegisterNisn.classList.add('hidden'); screenRegisterPin.classList.add('hidden'); if (screenForgotPin) screenForgotPin.classList.add('hidden'); screenHome.classList.add('hidden'); if (name === 'welcome') { screenWelcome.classList.remove('hidden'); } else if (name === 'login') { screenLogin.classList.remove('hidden'); } else if (name === 'register_nisn') { screenRegisterNisn.classList.remove('hidden'); } else if (name === 'register_pin') { screenRegisterPin.classList.remove('hidden'); } else if (name === 'forgot_pin') { if (screenForgotPin) { screenForgotPin.classList.remove('hidden'); if (forgotNisnInput && loginNisnInput) forgotNisnInput.value = loginNisnInput.value.trim(); } } else if (name === 'home') { screenHome.classList.remove('hidden'); setHomeDateLabel(); fetchTodayStatus(); } } function saveStudentProfile(profile) { currentStudentProfile = profile; try { localStorage.setItem('sman1_student_profile', JSON.stringify(profile)); } catch (e) { // ignore } } function loadStudentProfile() { try { var raw = localStorage.getItem('sman1_student_profile'); if (!raw) return null; return JSON.parse(raw); } catch (e) { return null; } } function handleCheckNisn() { if (!ensureConfig()) return; var nisn = regNisnInput.value.trim(); if (nisn === '') { regNisnStatus.textContent = 'NISN wajib diisi.'; regNisnStatus.style.color = '#f97316'; return; } var url = backendBase() + '/api/mobile/register/nisn'; btnCheckNisn.disabled = true; btnCheckNisn.textContent = 'Memeriksa…'; regNisnStatus.textContent = ''; fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ nisn: nisn }) }).then(function (res) { return res.json().then(function (data) { return { ok: res.ok, status: res.status, data: data }; }); }).then(function (r) { btnCheckNisn.disabled = false; btnCheckNisn.textContent = 'Lanjutkan'; if (!r.ok) { var msg = (r.data && r.data.message) ? r.data.message : 'NISN tidak valid'; regNisnStatus.textContent = msg; regNisnStatus.style.color = '#f97316'; showToast(msg, 'error'); if (r.status === 409 || (msg && msg.indexOf('sudah terdaftar') !== -1)) { if (loginNisnInput) loginNisnInput.value = nisn; goToScreen('login'); } return; } var d = r.data && r.data.data ? r.data.data : r.data; if (!d || !d.student_id) { regNisnStatus.textContent = 'Response tidak valid dari server.'; regNisnStatus.style.color = '#f97316'; return; } // Isi summary + dropdown kelas var summaryProfile = { student_id: d.student_id, name: d.name, nisn: d.nisn, class_id: d.current_class_id, class_label: d.current_class_label }; renderStudentSummary(regStudentSummary, summaryProfile); // Isi dropdown kelas var list = Array.isArray(d.available_classes) ? d.available_classes : []; var first = regClassSelect.options[0]; regClassSelect.innerHTML = ''; if (first) regClassSelect.appendChild(first); list.forEach(function (c) { var opt = document.createElement('option'); opt.value = c.id; opt.textContent = c.label; if (d.current_class_id && c.id === d.current_class_id) { opt.selected = true; summaryProfile.class_label = c.label; } regClassSelect.appendChild(opt); }); currentStudentProfile = summaryProfile; regPinInput.value = ''; regPin2Input.value = ''; regPinStatus.textContent = ''; goToScreen('register_pin'); showToast('NISN ditemukan. Lengkapi data.', 'success'); }).catch(function (err) { btnCheckNisn.disabled = false; btnCheckNisn.textContent = 'Lanjutkan'; regNisnStatus.textContent = 'Gagal menghubungi server.'; regNisnStatus.style.color = '#f97316'; var msg = (err && err.message) ? err.message : 'Gagal menghubungi server'; showToast(msg, 'error'); }); } function handleLogin() { if (!ensureConfig()) return; var nisn = loginNisnInput.value.trim(); var pin = loginPinInput.value.trim(); if (nisn === '' || pin === '') { loginStatus.textContent = 'NISN dan PIN wajib diisi.'; loginStatus.style.color = '#f97316'; return; } var url = backendBase() + '/api/mobile/login'; btnLogin.disabled = true; btnLogin.textContent = 'Memproses…'; loginStatus.textContent = ''; fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ nisn: nisn, pin: pin }) }).then(function (res) { return res.json().then(function (data) { return { ok: res.ok, status: res.status, data: data }; }); }).then(function (r) { btnLogin.disabled = false; btnLogin.textContent = 'Masuk'; if (!r.ok) { var msg = (r.data && r.data.message) ? r.data.message : 'Login gagal'; loginStatus.textContent = msg; loginStatus.style.color = '#f97316'; showToast(msg, 'error'); return; } var d = r.data && r.data.data ? r.data.data : r.data; if (!d || !d.student_id) { loginStatus.textContent = 'Response tidak valid dari server.'; loginStatus.style.color = '#f97316'; return; } var profile = { student_id: d.student_id, name: d.name, nisn: d.nisn, class_id: d.class_id, class_label: d.class_label }; saveStudentProfile(profile); renderStudentSummary(homeStudentSummary, profile); showToast('Login berhasil', 'success'); goToScreen('home'); }).catch(function (err) { btnLogin.disabled = false; btnLogin.textContent = 'Masuk'; loginStatus.textContent = 'Gagal menghubungi server.'; loginStatus.style.color = '#f97316'; var msg = (err && err.message) ? err.message : 'Gagal menghubungi server'; showToast(msg, 'error'); }); } function handleCompleteRegister() { if (!ensureConfig()) return; if (!currentStudentProfile || !currentStudentProfile.student_id) { showToast('Mulai dari input NISN dulu.', 'error'); goToScreen('register_nisn'); return; } var classId = parseInt(regClassSelect.value, 10); var pin = regPinInput.value.trim(); var pin2 = regPin2Input.value.trim(); if (!classId || classId <= 0) { regPinStatus.textContent = 'Pilih kelas terlebih dahulu.'; regPinStatus.style.color = '#f97316'; return; } if (pin.length < 4) { regPinStatus.textContent = 'PIN minimal 4 digit.'; regPinStatus.style.color = '#f97316'; return; } if (pin !== pin2) { regPinStatus.textContent = 'PIN dan ulangi PIN tidak sama.'; regPinStatus.style.color = '#f97316'; return; } var url = backendBase() + '/api/mobile/register/complete'; btnCompleteRegister.disabled = true; btnCompleteRegister.textContent = 'Menyimpan…'; regPinStatus.textContent = ''; fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ student_id: currentStudentProfile.student_id, class_id: classId, pin: pin }) }).then(function (res) { return res.json().then(function (data) { return { ok: res.ok, status: res.status, data: data }; }); }).then(function (r) { btnCompleteRegister.disabled = false; btnCompleteRegister.textContent = 'Simpan & Mulai'; if (!r.ok) { var msg = (r.data && r.data.message) ? r.data.message : 'Gagal menyimpan registrasi'; regPinStatus.textContent = msg; regPinStatus.style.color = '#f97316'; showToast(msg, 'error'); return; } var d = r.data && r.data.data ? r.data.data : r.data; if (!d || !d.student_id) { regPinStatus.textContent = 'Response tidak valid dari server.'; regPinStatus.style.color = '#f97316'; return; } var profile = { student_id: d.student_id, name: d.name, nisn: d.nisn, class_id: d.class_id, class_label: d.class_label }; saveStudentProfile(profile); renderStudentSummary(homeStudentSummary, profile); openEnrollFaceModal(); showToast('Registrasi berhasil. Rekam wajah untuk verifikasi absen.', 'success'); }).catch(function (err) { btnCompleteRegister.disabled = false; btnCompleteRegister.textContent = 'Simpan & Mulai'; regPinStatus.textContent = 'Gagal menghubungi server.'; regPinStatus.style.color = '#f97316'; var msg = (err && err.message) ? err.message : 'Gagal menghubungi server'; showToast(msg, 'error'); }); } function handleResetPin() { if (!ensureConfig()) return; var nisn = forgotNisnInput ? forgotNisnInput.value.trim() : ''; var newPin = forgotNewPinInput ? forgotNewPinInput.value.trim() : ''; var newPin2 = forgotNewPin2Input ? forgotNewPin2Input.value.trim() : ''; if (nisn === '') { if (forgotPinStatus) { forgotPinStatus.textContent = 'NISN wajib diisi.'; forgotPinStatus.style.color = '#f97316'; } showToast('NISN wajib diisi', 'error'); return; } if (newPin.length < 4) { if (forgotPinStatus) { forgotPinStatus.textContent = 'PIN baru minimal 4 digit.'; forgotPinStatus.style.color = '#f97316'; } showToast('PIN baru minimal 4 digit', 'error'); return; } if (newPin !== newPin2) { if (forgotPinStatus) { forgotPinStatus.textContent = 'PIN baru dan ulangi tidak sama.'; forgotPinStatus.style.color = '#f97316'; } showToast('PIN baru dan ulangi tidak sama', 'error'); return; } if (forgotPinStatus) forgotPinStatus.textContent = ''; var url = backendBase() + '/api/mobile/forgot-pin'; if (btnResetPin) { btnResetPin.disabled = true; btnResetPin.textContent = 'Memproses…'; } fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ nisn: nisn, new_pin: newPin, new_pin_confirm: newPin2 }) }).then(function (res) { return res.json().then(function (data) { return { ok: res.ok, status: res.status, data: data }; }); }).then(function (r) { if (btnResetPin) { btnResetPin.disabled = false; btnResetPin.textContent = 'Reset PIN'; } if (!r.ok) { var msg = (r.data && r.data.message) ? r.data.message : 'Gagal reset PIN'; if (forgotPinStatus) { forgotPinStatus.textContent = msg; forgotPinStatus.style.color = '#f97316'; } showToast(msg, 'error'); return; } showToast('PIN berhasil direset. Silakan masuk dengan PIN baru.', 'success'); if (loginNisnInput) loginNisnInput.value = nisn; if (loginPinInput) loginPinInput.value = ''; goToScreen('login'); }).catch(function (err) { if (btnResetPin) { btnResetPin.disabled = false; btnResetPin.textContent = 'Reset PIN'; } var msg = (err && err.message) ? err.message : 'Gagal menghubungi server'; if (forgotPinStatus) { forgotPinStatus.textContent = msg; forgotPinStatus.style.color = '#f97316'; } showToast(msg, 'error'); }); } function doCheckin() { if (!ensureConfig()) return; if (!currentStudentProfile || !currentStudentProfile.student_id) { showToast('Lengkapi registrasi siswa dulu.', 'error'); goToScreen('register_nisn'); return; } if (currentLat == null || currentLng == null) { showToast('Lokasi belum diambil. Tekan tombol refresh lokasi dulu.', 'error'); return; } openCameraModal(); } function getActiveCheckinButton() { if (currentCheckinType === 'masuk' && btnAbsenMasuk) return btnAbsenMasuk; if (currentCheckinType === 'pulang' && btnAbsenPulang) return btnAbsenPulang; return null; } function fetchTodayStatus() { if (!currentStudentProfile || !currentStudentProfile.student_id) return; var url = backendBase() + '/api/mobile/attendance/today?student_id=' + encodeURIComponent(currentStudentProfile.student_id); fetch(url, { method: 'GET', headers: { 'Accept': 'application/json' } }) .then(function (res) { return res.json(); }) .then(function (res) { if (res && res.success && res.data) { updatePresenceUI(res.data); } }) .catch(function () {}); } function updatePresenceUI(data) { var hasMasuk = data.has_masuk === true; var hasPulang = data.has_pulang === true; if (masukStatusEl && btnAbsenMasuk) { if (hasMasuk) { masukStatusEl.classList.remove('hidden'); btnAbsenMasuk.classList.add('hidden'); } else { masukStatusEl.classList.add('hidden'); btnAbsenMasuk.classList.remove('hidden'); } } if (pulangStatusEl && btnAbsenPulang) { if (hasPulang) { pulangStatusEl.classList.remove('hidden'); btnAbsenPulang.classList.add('hidden'); } else { pulangStatusEl.classList.add('hidden'); btnAbsenPulang.classList.remove('hidden'); } } } function setHomeDateLabel() { if (!homeDateLabel) return; var d = new Date(); var days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu']; var dayName = days[d.getDay()]; var dateStr = d.getDate() + ' ' + ['Jan','Feb','Mar','Apr','Mei','Jun','Jul','Agu','Sep','Okt','Nov','Des'][d.getMonth()] + ' ' + d.getFullYear(); homeDateLabel.textContent = dayName + ', ' + dateStr; } function stopFaceDetection() { if (faceDetectIntervalId) { clearInterval(faceDetectIntervalId); faceDetectIntervalId = null; } faceDetectConsecutiveCount = 0; if (cameraFaceStatus) { cameraFaceStatus.classList.add('hidden'); cameraFaceStatus.textContent = ''; } } function openCameraModal() { if (!cameraModal || !cameraVideo) return; faceDetectSubmitting = false; stopFaceDetection(); cameraModal.classList.remove('hidden'); if (cameraError) cameraError.classList.add('hidden'); if (cameraFaceStatus) cameraFaceStatus.classList.add('hidden'); cameraVideo.style.display = 'block'; var constraints = { video: { facingMode: 'user', width: { ideal: 640 }, height: { ideal: 480 } }, audio: false }; navigator.mediaDevices.getUserMedia(constraints).then(function (stream) { cameraStream = stream; cameraVideo.srcObject = stream; cameraVideo.onloadedmetadata = function () { cameraVideo.play().then(function () { startFaceDetection(); }).catch(function () {}); }; }).catch(function (err) { if (cameraError) { cameraError.textContent = 'Kamera tidak tersedia atau izin ditolak.'; cameraError.classList.remove('hidden'); } cameraVideo.style.display = 'none'; showToast('Aktifkan izin kamera untuk verifikasi wajah.', 'error'); }); } function captureCurrentFrameAsBase64() { if (!cameraVideo || cameraVideo.readyState < 2) return null; var w = cameraVideo.videoWidth || 640; var h = cameraVideo.videoHeight || 480; if (w === 0 || h === 0) return null; var canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; var ctx = canvas.getContext('2d'); ctx.drawImage(cameraVideo, 0, 0, w, h); return canvas.toDataURL('image/jpeg', 0.9); } function verifyFaceOnServer(imageBase64) { if (!ensureConfig()) return Promise.reject(new Error('Config backend belum diisi')); if (!currentStudentProfile || !currentStudentProfile.student_id) { return Promise.reject(new Error('Profil siswa belum tersedia')); } var url = backendBase() + '/api/attendance/verify-face'; return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ student_id: currentStudentProfile.student_id, image: imageBase64 }) }).then(function (res) { return res.json().then(function (data) { return { ok: res.ok, status: res.status, data: data }; }); }).then(function (r) { if (!r.ok || !r.data) { var msg = (r.data && r.data.message) ? r.data.message : 'Verifikasi wajah gagal'; throw new Error(msg); } var d = r.data.data || r.data; if (!d) throw new Error('Response verifikasi tidak valid'); return d; // { student_id, similarity, threshold, matched_source, status } }); } function startFaceDetection() { if (faceDetectSubmitting || !cameraVideo || !cameraStream) return; if (typeof faceapi === 'undefined') return; var isMasukPulang = currentCheckinType === 'masuk' || currentCheckinType === 'pulang'; var weightsUrl = 'https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/weights/'; function runDetection() { if (faceDetectIntervalId || !cameraStream) return; if (cameraFaceStatus) { cameraFaceStatus.classList.remove('hidden'); cameraFaceStatus.classList.add('detecting'); cameraFaceStatus.textContent = 'Mendeteksi wajah…'; } var requiredConsecutive = 5; faceDetectIntervalId = setInterval(function () { if (faceDetectSubmitting || !cameraStream || !cameraVideo.srcObject) return; if (cameraVideo.readyState < 2) return; var detect = faceapi.detectSingleFace(cameraVideo, new faceapi.TinyFaceDetectorOptions({ inputSize: 224, scoreThreshold: 0.5 })); detect.then(function (detection) { if (faceDetectSubmitting) return; if (detection) { faceDetectConsecutiveCount++; if (cameraFaceStatus) { cameraFaceStatus.textContent = 'Wajah terdeteksi… ' + (faceDetectConsecutiveCount >= requiredConsecutive ? 'Memverifikasi…' : ''); } if (faceDetectConsecutiveCount >= requiredConsecutive) { faceDetectSubmitting = true; stopFaceDetection(); if (cameraFaceStatus) { cameraFaceStatus.classList.remove('detecting'); cameraFaceStatus.classList.add('sending'); cameraFaceStatus.textContent = 'Memverifikasi wajah…'; } if (isMasukPulang) { var frameBase64 = captureCurrentFrameAsBase64(); if (!frameBase64) { faceDetectSubmitting = false; faceDetectConsecutiveCount = 0; if (cameraFaceStatus) { cameraFaceStatus.textContent = 'Gagal menangkap frame. Coba lagi.'; } return; } verifyFaceOnServer(frameBase64).then(function (result) { if (result.status !== 'match') { faceDetectSubmitting = false; faceDetectConsecutiveCount = 0; if (cameraFaceStatus) { cameraFaceStatus.textContent = 'Wajah tidak dikenali server. Coba lagi.'; } showToast('Verifikasi wajah gagal. Coba lagi.', 'error'); return; } pendingMasukPulangType = currentCheckinType; closeCameraModal(); var titleEl = qrPinModal && qrPinModal.querySelector('.modal-panel-title'); if (titleEl) titleEl.textContent = currentCheckinType === 'masuk' ? 'Verifikasi wajah berhasil - Masukkan PIN' : 'Verifikasi wajah berhasil - Masukkan PIN'; var hintEl = qrPinModal && qrPinModal.querySelector('.modal-panel-hint'); if (hintEl) hintEl.textContent = 'Wajah terverifikasi server. Masukkan PIN untuk menyelesaikan absen ' + (currentCheckinType === 'masuk' ? 'masuk' : 'pulang') + '.'; if (qrPinInput) { qrPinInput.value = ''; qrPinInput.focus(); } if (qrPinModal) qrPinModal.classList.remove('hidden'); }).catch(function (err) { faceDetectSubmitting = false; faceDetectConsecutiveCount = 0; if (cameraFaceStatus) { cameraFaceStatus.textContent = 'Gagal verifikasi wajah di server.'; } var msg = (err && err.message) ? err.message : 'Gagal verifikasi wajah di server'; showToast(msg, 'error'); }); } else { doCheckinSubmit(); } } } else { faceDetectConsecutiveCount = 0; if (cameraFaceStatus && !faceDetectSubmitting) { cameraFaceStatus.textContent = 'Arahkan wajah ke kamera…'; } } }) .catch(function () { faceDetectConsecutiveCount = 0; }); }, 300); } faceapi.nets.tinyFaceDetector.loadFromUri(weightsUrl).then(runDetection).catch(function () { if (cameraFaceStatus) cameraFaceStatus.classList.add('hidden'); }); } function closeCameraModal() { stopFaceDetection(); stopCamera(); if (cameraModal) cameraModal.classList.add('hidden'); } function captureEnrollFrameAsBase64() { if (!enrollFaceVideo || enrollFaceVideo.readyState < 2) return null; var w = enrollFaceVideo.videoWidth || 640; var h = enrollFaceVideo.videoHeight || 480; if (w === 0 || h === 0) return null; var canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; var ctx = canvas.getContext('2d'); ctx.drawImage(enrollFaceVideo, 0, 0, w, h); return canvas.toDataURL('image/jpeg', 0.9); } function closeEnrollFaceModal() { if (enrollIntervalId) { clearInterval(enrollIntervalId); enrollIntervalId = null; } enrollCollectedImages = []; if (enrollStream) { enrollStream.getTracks().forEach(function (t) { t.stop(); }); enrollStream = null; } if (enrollFaceVideo) enrollFaceVideo.srcObject = null; if (enrollFaceModal) enrollFaceModal.classList.add('hidden'); if (enrollFaceStatus) { enrollFaceStatus.classList.add('hidden'); enrollFaceStatus.textContent = ''; } } function openEnrollFaceModal() { if (!ensureConfig()) { goToScreen('home'); return; } if (!enrollFaceModal || !enrollFaceVideo || !currentStudentProfile || !currentStudentProfile.student_id) { goToScreen('home'); return; } enrollCollectedImages = []; enrollSubmitting = false; if (enrollFaceError) enrollFaceError.classList.add('hidden'); if (enrollFaceStatus) { enrollFaceStatus.textContent = 'Menyiapkan kamera…'; enrollFaceStatus.classList.remove('hidden'); } enrollFaceModal.classList.remove('hidden'); enrollFaceVideo.style.display = 'block'; var constraints = { video: { facingMode: 'user', width: { ideal: 640 }, height: { ideal: 480 } }, audio: false }; navigator.mediaDevices.getUserMedia(constraints).then(function (stream) { enrollStream = stream; enrollFaceVideo.srcObject = stream; enrollFaceVideo.onloadedmetadata = function () { enrollFaceVideo.play().then(function () { if (enrollFaceStatus) enrollFaceStatus.textContent = 'Arahkan wajah ke kamera lalu tekan Rekam.'; }).catch(function () {}); }; }).catch(function () { if (enrollFaceError) { enrollFaceError.textContent = 'Kamera tidak tersedia atau izin ditolak. Bisa dilewati.'; enrollFaceError.classList.remove('hidden'); } if (enrollFaceStatus) enrollFaceStatus.textContent = 'Kamera gagal. Tekan Lewati untuk lanjut.'; }); } function startEnrollCapture() { if (enrollSubmitting || !enrollStream || !enrollFaceVideo || typeof faceapi === 'undefined') return; if (!currentStudentProfile || !currentStudentProfile.student_id) return; enrollSubmitting = true; enrollCollectedImages = []; if (enrollFaceStart) { enrollFaceStart.disabled = true; enrollFaceStart.textContent = 'Merekam…'; } var weightsUrl = 'https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/weights/'; faceapi.nets.tinyFaceDetector.loadFromUri(weightsUrl).then(function () { var tick = 0; var lastFaceCount = 0; enrollIntervalId = setInterval(function () { if (!enrollStream || !enrollFaceVideo.srcObject || enrollFaceVideo.readyState < 2) return; var detect = faceapi.detectSingleFace(enrollFaceVideo, new faceapi.TinyFaceDetectorOptions({ inputSize: 224, scoreThreshold: 0.5 })); detect.then(function (detection) { if (detection && enrollCollectedImages.length < enrollTargetCount) { var b64 = captureEnrollFrameAsBase64(); if (b64) { enrollCollectedImages.push(b64); if (enrollFaceStatus) enrollFaceStatus.textContent = 'Rekam ' + enrollCollectedImages.length + '/' + enrollTargetCount + '…'; } } if (enrollCollectedImages.length >= enrollMinCount) { clearInterval(enrollIntervalId); enrollIntervalId = null; var imgs = enrollCollectedImages.slice(0, enrollTargetCount); var url = backendBase() + '/api/face/enroll-live'; if (enrollFaceStatus) enrollFaceStatus.textContent = 'Mengirim ke server…'; fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ student_id: currentStudentProfile.student_id, images: imgs }) }).then(function (r) { return r.json().then(function (d) { return { ok: r.ok, data: d }; }); }) .then(function (r) { enrollSubmitting = false; if (enrollFaceStart) { enrollFaceStart.disabled = false; enrollFaceStart.textContent = 'Rekam (3–5 foto)'; } if (r.ok && r.data && (r.data.saved || 0) > 0) { showToast('Wajah berhasil direkam. Siap untuk verifikasi absen.', 'success'); } else { showToast('Rekam wajah gagal. Bisa dicoba nanti dari pengaturan.', 'info'); } closeEnrollFaceModal(); goToScreen('home'); }).catch(function () { enrollSubmitting = false; if (enrollFaceStart) { enrollFaceStart.disabled = false; enrollFaceStart.textContent = 'Rekam (3–5 foto)'; } if (enrollFaceStatus) enrollFaceStatus.textContent = 'Gagal mengirim. Coba lagi atau Lewati.'; showToast('Gagal mengirim rekaman wajah.', 'error'); }); } }).catch(function () {}); tick++; if (tick > 80) { clearInterval(enrollIntervalId); enrollIntervalId = null; enrollSubmitting = false; if (enrollFaceStart) { enrollFaceStart.disabled = false; enrollFaceStart.textContent = 'Rekam (3–5 foto)'; } if (enrollCollectedImages.length >= 1) { var url = backendBase() + '/api/face/enroll-live'; if (enrollFaceStatus) enrollFaceStatus.textContent = 'Mengirim ' + enrollCollectedImages.length + ' foto…'; fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ student_id: currentStudentProfile.student_id, images: enrollCollectedImages }) }).then(function (r) { return r.json().then(function (d) { return { ok: r.ok, data: d }; }); }) .then(function (r) { if (r.ok && (r.data && r.data.saved > 0)) showToast('Wajah berhasil direkam.', 'success'); else showToast('Rekam wajah gagal. Bisa dicoba nanti.', 'info'); closeEnrollFaceModal(); goToScreen('home'); }).catch(function () { enrollSubmitting = false; if (enrollFaceStart) { enrollFaceStart.disabled = false; enrollFaceStart.textContent = 'Rekam (3–5 foto)'; } if (enrollFaceStatus) enrollFaceStatus.textContent = 'Gagal mengirim. Coba lagi atau Lewati.'; showToast('Gagal mengirim rekaman wajah.', 'error'); }); } else { enrollSubmitting = false; if (enrollFaceStart) { enrollFaceStart.disabled = false; enrollFaceStart.textContent = 'Rekam (3–5 foto)'; } if (enrollFaceStatus) enrollFaceStatus.textContent = 'Wajah tidak terdeteksi. Arahkan wajah ke kamera lalu Rekam lagi.'; } } }, 500); }).catch(function () { enrollSubmitting = false; if (enrollFaceStart) { enrollFaceStart.disabled = false; enrollFaceStart.textContent = 'Rekam (3–5 foto)'; } if (enrollFaceStatus) enrollFaceStatus.textContent = 'Gagal memuat deteksi wajah. Lewati atau coba lagi.'; }); } function stopCamera() { if (cameraStream) { cameraStream.getTracks().forEach(function (t) { t.stop(); }); cameraStream = null; } if (cameraVideo) cameraVideo.srcObject = null; } function doCheckinSubmit() { var url = backendBase() + '/api/attendance/checkin'; var deviceCode = DEFAULT_DEVICE_CODE; var apiKey = DEFAULT_API_KEY; var payload = { device_code: deviceCode, api_key: apiKey, student_id: currentStudentProfile.student_id, datetime: new Date().toISOString().slice(0, 19).replace('T', ' '), lat: currentLat, lng: currentLng, confidence: 1.0 }; var activeBtn = getActiveCheckinButton(); if (activeBtn) { activeBtn.disabled = true; activeBtn.textContent = 'Memproses…'; } fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(payload) }) .then(function (res) { return res.json().then(function (data) { return { ok: res.ok, status: res.status, data: data }; }); }) .then(function (r) { if (activeBtn) { activeBtn.disabled = false; activeBtn.textContent = currentCheckinType === 'masuk' ? 'Absen Masuk' : 'Absen Pulang'; } if (!r.ok) { faceDetectSubmitting = false; var msg = (r.data && r.data.message) ? r.data.message : 'Gagal check-in'; showToast(msg, 'error'); setResult('ERROR', msg, ''); return; } var body = r.data || {}; var status = body.data && body.data.status ? body.data.status : 'UNKNOWN'; var meta = ''; if (body.data && body.data.checkin_at) { meta = 'Waktu server: ' + body.data.checkin_at; } if (status === 'OUTSIDE_ZONE') { updateLocationStatus('Anda di luar jangkauan yang diizinkan', true); } var friendly; switch (status) { case 'PRESENT': friendly = 'Absensi tercatat sebagai HADIR.'; break; case 'LATE': friendly = 'Absensi tercatat, namun TERLAMBAT.'; break; case 'OUTSIDE_ZONE': friendly = 'Anda di luar jangkauan yang diizinkan.'; break; case 'NO_SCHEDULE': friendly = 'Tidak ada jadwal aktif untuk siswa ini.'; break; case 'INVALID_DEVICE': friendly = 'Device code / API key tidak valid.'; break; case 'ALREADY_CHECKED_IN': friendly = 'Siswa ini sudah check-in untuk jadwal hari ini.'; break; case 'ABSENCE_WINDOW_CLOSED': friendly = 'Check-in di luar window waktu absensi.'; break; case 'SESSION_CLOSED': friendly = 'Sesi pelajaran sudah ditutup.'; break; default: friendly = body.message || 'Status: ' + status; break; } setResult(status, friendly, meta); showToast('Status: ' + status, (status === 'PRESENT' || status === 'LATE') ? 'success' : 'info'); currentCheckinType = null; fetchTodayStatus(); closeCameraModal(); }) .catch(function (err) { if (activeBtn) { activeBtn.disabled = false; activeBtn.textContent = currentCheckinType === 'masuk' ? 'Absen Masuk' : 'Absen Pulang'; } faceDetectSubmitting = false; currentCheckinType = null; var msg = (err && err.message) ? err.message : 'Tidak dapat terhubung ke backend.'; showToast(msg, 'error'); setResult('ERROR', msg, ''); }); } function openScanQrModal() { if (!ensureConfig()) return; if (!currentStudentProfile || !currentStudentProfile.student_id) { showToast('Anda harus masuk dulu.', 'error'); return; } pendingQrToken = null; if (scanQrStatus) scanQrStatus.textContent = ''; if (qrReader) qrReader.innerHTML = ''; if (scanQrModal) scanQrModal.classList.remove('hidden'); function startScanner() { if (typeof Html5Qrcode !== 'undefined') { if (html5QrCodeInstance) { try { html5QrCodeInstance.stop(); } catch (e) {} html5QrCodeInstance = null; } html5QrCodeInstance = new Html5Qrcode('qr-reader'); var opts = { fps: 8, qrbox: { width: 220, height: 220 } }; html5QrCodeInstance.start( { facingMode: 'environment' }, opts, function (decodedText) { var token = (decodedText || '').trim(); if (!token) return; try { html5QrCodeInstance.stop(); } catch (e) {} html5QrCodeInstance = null; pendingQrToken = token; if (scanQrModal) scanQrModal.classList.add('hidden'); var titleEl = qrPinModal && qrPinModal.querySelector('.modal-panel-title'); if (titleEl) titleEl.textContent = 'Konfirmasi PIN'; var hintEl = qrPinModal && qrPinModal.querySelector('.modal-panel-hint'); if (hintEl) hintEl.textContent = 'Masukkan PIN untuk mengirim absen mapel.'; if (qrPinInput) { qrPinInput.value = ''; qrPinInput.focus(); } if (qrPinModal) qrPinModal.classList.remove('hidden'); }, function () {} ).catch(function (err) { if (scanQrStatus) scanQrStatus.textContent = 'Kamera tidak tersedia atau izin ditolak.'; showToast('Aktifkan izin kamera untuk scan QR.', 'error'); }); } else { if (scanQrStatus) scanQrStatus.textContent = 'Library scan QR tidak terbaca. Refresh halaman.'; } } if (typeof Html5Qrcode !== 'undefined') { startScanner(); } else { if (scanQrStatus) scanQrStatus.textContent = 'Memuat pemindai QR…'; var s = document.createElement('script'); s.src = 'https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js'; s.onload = startScanner; s.onerror = function () { if (scanQrStatus) scanQrStatus.textContent = 'Gagal memuat library. Cek koneksi.'; }; document.head.appendChild(s); } } function closeScanQrModal() { if (html5QrCodeInstance) { try { html5QrCodeInstance.stop(); } catch (e) {} html5QrCodeInstance = null; } if (scanQrModal) scanQrModal.classList.add('hidden'); } function submitCheckinMasukPulang(pin) { if (!currentStudentProfile || !currentStudentProfile.nisn || !pendingMasukPulangType) return; var url = backendBase() + '/api/mobile/checkin-masuk-pulang'; var payload = { nisn: currentStudentProfile.nisn, pin: pin, type: pendingMasukPulangType, lat: currentLat, lng: currentLng }; if (qrPinSubmit) { qrPinSubmit.disabled = true; qrPinSubmit.textContent = 'Mengirim…'; } fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(payload) }) .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, data: d }; }); }) .then(function (r) { if (qrPinSubmit) { qrPinSubmit.disabled = false; qrPinSubmit.textContent = 'Kirim Absen'; } closeQrPinModal(); pendingMasukPulangType = null; currentCheckinType = null; if (!r.ok) { var msg = (r.data && r.data.message) ? r.data.message : 'Gagal mengirim absen'; showToast(msg, 'error'); setResult('ERROR', msg, ''); return; } var body = r.data || {}; var status = (body.data && body.data.status) ? body.data.status : 'UNKNOWN'; var message = body.message || status; var meta = (body.data && body.data.checkin_at) ? ('Waktu: ' + body.data.checkin_at) : ''; var friendly = message; if (status === 'PRESENT') friendly = pendingMasukPulangType === 'masuk' ? 'Absen masuk berhasil.' : 'Absen pulang berhasil.'; else if (status === 'OUTSIDE_ZONE') { friendly = 'Anda di luar jangkauan sekolah.'; updateLocationStatus('Anda di luar jangkauan sekolah', true); } else if (status === 'INVALID_DEVICE') friendly = 'Device aplikasi belum dikonfigurasi. Hubungi admin.'; else if (status === 'ALREADY_CHECKED_IN') friendly = pendingMasukPulangType === 'masuk' ? 'Sudah absen masuk hari ini.' : 'Sudah absen pulang hari ini.'; else if (status === 'ABSENCE_WINDOW_CLOSED') friendly = 'Di luar jam absen. Cek jam masuk/pulang di pengaturan sekolah.'; setResult(status, friendly, meta); showToast(friendly, (status === 'PRESENT') ? 'success' : 'info'); fetchTodayStatus(); }) .catch(function () { if (qrPinSubmit) { qrPinSubmit.disabled = false; qrPinSubmit.textContent = 'Kirim Absen'; } pendingMasukPulangType = null; currentCheckinType = null; showToast('Tidak dapat terhubung ke server.', 'error'); setResult('ERROR', 'Tidak dapat terhubung ke server.', ''); }); } function closeQrPinModal() { pendingQrToken = null; pendingMasukPulangType = null; if (qrPinModal) qrPinModal.classList.add('hidden'); if (qrPinInput) qrPinInput.value = ''; var titleEl = qrPinModal && qrPinModal.querySelector('.modal-panel-title'); if (titleEl) titleEl.textContent = 'Konfirmasi PIN'; var hintEl = qrPinModal && qrPinModal.querySelector('.modal-panel-hint'); if (hintEl) hintEl.textContent = 'Masukkan PIN untuk mengirim absen mapel.'; } function submitCheckinQr() { var pin = (qrPinInput && qrPinInput.value) ? qrPinInput.value.trim() : ''; if (!pin) { showToast('Masukkan PIN.', 'error'); return; } if (pendingMasukPulangType === 'masuk' || pendingMasukPulangType === 'pulang') { submitCheckinMasukPulang(pin); return; } if (!pendingQrToken || !currentStudentProfile || !currentStudentProfile.nisn) { showToast('Data tidak lengkap.', 'error'); return; } var url = backendBase() + '/api/mobile/checkin-qr'; var payload = { nisn: currentStudentProfile.nisn, pin: pin, qr_token: pendingQrToken }; if (currentLat != null && currentLng != null) { payload.lat = currentLat; payload.lng = currentLng; } if (qrPinSubmit) { qrPinSubmit.disabled = true; qrPinSubmit.textContent = 'Mengirim…'; } fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(payload) }) .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, data: d }; }); }) .then(function (r) { if (qrPinSubmit) { qrPinSubmit.disabled = false; qrPinSubmit.textContent = 'Kirim Absen'; } closeQrPinModal(); if (!r.ok) { var msg = (r.data && r.data.message) ? r.data.message : 'Gagal mengirim absen'; showToast(msg, 'error'); setResult('ERROR', msg, ''); return; } var body = r.data || {}; var status = (body.data && body.data.status) ? body.data.status : 'UNKNOWN'; var message = body.message || status; var meta = (body.data && body.data.checkin_at) ? ('Waktu: ' + body.data.checkin_at) : ''; var friendly = message; if (status === 'PRESENT') friendly = 'Absensi berhasil (Hadir).'; else if (status === 'LATE') friendly = 'Absensi berhasil (Terlambat).'; else if (status === 'OUTSIDE_ZONE') { friendly = 'Anda di luar jangkauan yang diizinkan.'; updateLocationStatus('Anda di luar jangkauan yang diizinkan', true); } else if (status === 'INVALID_QR_TOKEN') friendly = 'QR tidak valid atau sudah kadaluarsa. Minta guru menampilkan QR lagi.'; else if (status === 'STUDENT_NOT_IN_CLASS') friendly = 'Anda tidak termasuk kelas untuk mapel ini.'; else if (status === 'ALREADY_CHECKED_IN') friendly = 'Sudah absen untuk mapel ini hari ini.'; setResult(status, friendly, meta); showToast(friendly, (status === 'PRESENT' || status === 'LATE') ? 'success' : 'info'); }) .catch(function () { if (qrPinSubmit) { qrPinSubmit.disabled = false; qrPinSubmit.textContent = 'Kirim Absen'; } showToast('Tidak dapat terhubung ke server.', 'error'); setResult('ERROR', 'Tidak dapat terhubung ke server.', ''); }); } // Event binding btnOpenSettings.addEventListener('click', openSettingsModal); btnCloseSettings.addEventListener('click', closeSettingsModal); btnSaveConfig.addEventListener('click', function () { saveConfig(); }); var btnTestConnection = document.getElementById('btn-test-connection'); if (btnTestConnection) { btnTestConnection.addEventListener('click', function () { if (!backendUrlInput.value.trim()) { configStatus.textContent = 'Isi URL backend dulu.'; configStatus.style.color = '#f97316'; return; } testConnection(); }); } btnRefreshLocation.addEventListener('click', getLocation); if (btnAbsenMasuk) { btnAbsenMasuk.addEventListener('click', function () { currentCheckinType = 'masuk'; doCheckin(); }); } if (btnAbsenPulang) { btnAbsenPulang.addEventListener('click', function () { currentCheckinType = 'pulang'; doCheckin(); }); } var btnScanQr = document.getElementById('btn-scan-qr'); if (btnScanQr) btnScanQr.addEventListener('click', openScanQrModal); btnCheckNisn.addEventListener('click', handleCheckNisn); btnCompleteRegister.addEventListener('click', handleCompleteRegister); if (cameraBtnCancel) { cameraBtnCancel.addEventListener('click', closeCameraModal); } if (cameraBtnCancel2) { cameraBtnCancel2.addEventListener('click', closeCameraModal); } var cameraBackdrop = document.getElementById('camera-backdrop'); if (cameraBackdrop) { cameraBackdrop.addEventListener('click', closeCameraModal); } if (enrollFaceBackdrop) { enrollFaceBackdrop.addEventListener('click', function () { showToast('Rekam wajah wajib untuk melanjutkan.', 'info'); }); } if (enrollFaceStart) { enrollFaceStart.addEventListener('click', startEnrollCapture); } if (resultCloseBtn) { resultCloseBtn.addEventListener('click', function () { resultBox.classList.add('hidden'); }); } if (scanQrClose) scanQrClose.addEventListener('click', closeScanQrModal); var scanQrBackdrop = document.getElementById('scan-qr-backdrop'); if (scanQrBackdrop) scanQrBackdrop.addEventListener('click', closeScanQrModal); if (qrPinClose) qrPinClose.addEventListener('click', closeQrPinModal); if (qrPinBackdrop) qrPinBackdrop.addEventListener('click', closeQrPinModal); if (qrPinCancel) qrPinCancel.addEventListener('click', closeQrPinModal); if (qrPinSubmit) qrPinSubmit.addEventListener('click', submitCheckinQr); var btnGoLogin = document.getElementById('btn-go-login'); var btnGoRegister = document.getElementById('btn-go-register'); var linkGoRegister = document.getElementById('link-go-register'); if (btnGoLogin) { btnGoLogin.addEventListener('click', function () { goToScreen('login'); }); } if (btnGoRegister) { btnGoRegister.addEventListener('click', function () { goToScreen('register_nisn'); }); } if (linkGoRegister) { linkGoRegister.addEventListener('click', function () { goToScreen('register_nisn'); }); } var linkBackLogin1 = document.getElementById('link-back-login-1'); if (linkBackLogin1) { linkBackLogin1.addEventListener('click', function (e) { e.preventDefault(); goToScreen('login'); }); } var btnRegisterPinBatal = document.getElementById('btn-register-pin-batal'); if (btnRegisterPinBatal) { btnRegisterPinBatal.addEventListener('click', function (e) { e.preventDefault(); goToScreen('register_nisn'); }); } var btnLogout = document.getElementById('btn-logout'); if (btnLogout) { btnLogout.addEventListener('click', function (e) { e.preventDefault(); currentStudentProfile = null; try { localStorage.removeItem('sman1_student_profile'); } catch (err) {} showToast('Anda sudah keluar', 'info'); goToScreen('welcome'); }); } if (btnLogin) { btnLogin.addEventListener('click', handleLogin); } var linkForgotPin = document.getElementById('link-forgot-pin'); if (linkForgotPin) { linkForgotPin.addEventListener('click', function (e) { e.preventDefault(); goToScreen('forgot_pin'); }); } var linkBackToLogin = document.getElementById('link-back-to-login'); if (linkBackToLogin) { linkBackToLogin.addEventListener('click', function (e) { e.preventDefault(); goToScreen('login'); }); } if (btnResetPin) { btnResetPin.addEventListener('click', handleResetPin); } // Init loadConfig(); currentStudentProfile = loadStudentProfile(); if (currentStudentProfile && currentStudentProfile.student_id) { renderStudentSummary(homeStudentSummary, currentStudentProfile); goToScreen('home'); } else { goToScreen('welcome'); } getLocation(); })();