(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();
})();