1335 lines
55 KiB
JavaScript
1335 lines
55 KiB
JavaScript
(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;
|
|
|
|
// 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);
|
|
|
|
goToScreen('home');
|
|
showToast('Registrasi berhasil. Siap presensi.', '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 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 (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();
|
|
})();
|
|
|