Files
presensi_app/app.js

1513 lines
65 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(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 =
'<div><strong>' + (profile.name || '-') + '</strong></div>' +
'<div>NISN: ' + (profile.nisn || '-') + '</div>' +
'<div>Kelas: ' + (profile.class_label || '-') + '</div>';
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 (35 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 (35 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 (35 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 (35 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 (35 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 (35 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();
})();