init backend presensi

This commit is contained in:
mwpn
2026-03-05 14:37:36 +07:00
commit b4fda6b9c9
319 changed files with 27261 additions and 0 deletions

View File

@@ -0,0 +1,279 @@
<div class="space-y-6">
<h1 class="text-xl font-semibold">Jadwal Hari Ini</h1>
<!-- Banner: saat ini guru harus di kelas mana -->
<div id="current-schedule-banner" class="hidden rounded-2xl border border-blue-200 dark:border-blue-800 bg-blue-50 dark:bg-blue-900/20 p-4">
<p class="text-sm font-medium text-blue-800 dark:text-blue-200 mb-1">📍 Anda seharusnya di:</p>
<p id="current-schedule-text" class="text-base font-semibold text-blue-900 dark:text-blue-100"></p>
<p id="current-schedule-hint" class="text-xs text-blue-600 dark:text-blue-300 mt-1"></p>
</div>
<div id="current-schedule-empty" class="hidden rounded-2xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 p-4 text-gray-600 dark:text-gray-400 text-sm">
Tidak ada jadwal mengajar Anda hari ini, atau tidak ada jam aktif saat ini.
</div>
<div id="schedule-loading" class="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-12 text-center text-gray-500 dark:text-gray-400">
Loading schedules…
</div>
<div id="schedule-error" class="hidden rounded-2xl border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20 p-6 text-red-700 dark:text-red-300"></div>
<div id="schedule-empty" class="hidden rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-12 text-center text-gray-500 dark:text-gray-400">
Tidak ada jadwal untuk hari ini.
</div>
<div id="schedule-content" class="hidden rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-sm overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-left">
<thead class="bg-gray-50 dark:bg-gray-700/50 text-sm text-gray-600 dark:text-gray-400">
<tr>
<th class="px-6 py-3 font-medium">Mapel</th>
<th class="px-6 py-3 font-medium">Kelas</th>
<th class="px-6 py-3 font-medium">Guru</th>
<th class="px-6 py-3 font-medium">Jam</th>
<th class="px-6 py-3 font-medium w-52">Aksi</th>
</tr>
</thead>
<tbody id="schedule-tbody" class="divide-y divide-gray-200 dark:divide-gray-700"></tbody>
</table>
</div>
</div>
<!-- Modal QR Absen Mapel -->
<div id="qr-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/50" id="qr-modal-backdrop"></div>
<div class="relative bg-white dark:bg-gray-800 rounded-2xl shadow-xl max-w-sm w-full mx-4 p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">QR Absen Mapel</h2>
<button type="button" id="qr-modal-close" class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-400">&times;</button>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1"><strong id="qr-subject">-</strong> <span id="qr-class">-</span></p>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-4" id="qr-expires"></p>
<div id="qr-code-container" class="flex justify-center my-4"></div>
<p class="text-xs text-gray-500 dark:text-gray-400 text-center">Siswa scan QR ini dengan aplikasi presensi untuk absen mapel.</p>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<script>
(function() {
var baseUrl = '<?= base_url() ?>'.replace(/\/$/, '');
var reportBaseUrl = baseUrl + '/dashboard/attendance/report/';
var apiUrl = baseUrl + '/api/dashboard/schedules/today';
var currentApiUrl = baseUrl + '/api/dashboard/schedules/current';
var currentBanner = document.getElementById('current-schedule-banner');
var currentText = document.getElementById('current-schedule-text');
var currentHint = document.getElementById('current-schedule-hint');
var currentEmpty = document.getElementById('current-schedule-empty');
var loading = document.getElementById('schedule-loading');
var errorEl = document.getElementById('schedule-error');
var emptyEl = document.getElementById('schedule-empty');
var content = document.getElementById('schedule-content');
var tbody = document.getElementById('schedule-tbody');
function fetchCurrentAndHighlight() {
fetch(currentApiUrl, { method: 'GET', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(function(res) { return res.json().then(function(j) { return { ok: res.ok, data: j }; }); })
.then(function(r) {
if (!r.ok || !r.data) {
if (currentEmpty) currentEmpty.classList.remove('hidden');
return;
}
var d = r.data.data || r.data;
var scheduleIdToHighlight = null;
var isActiveNow = d.is_active_now === true;
if (isActiveNow && d.schedule_id) {
scheduleIdToHighlight = d.schedule_id;
if (currentBanner) {
currentBanner.classList.remove('hidden');
currentBanner.classList.remove('border-amber-200', 'dark:border-amber-800', 'bg-amber-50', 'dark:bg-amber-900/20');
currentBanner.classList.add('border-blue-200', 'dark:border-blue-800', 'bg-blue-50', 'dark:bg-blue-900/20');
}
if (currentText) currentText.textContent = (d.subject_name || '-') + ' — Kelas ' + (d.class_name || '-');
if (currentHint) currentHint.textContent = 'Jam ' + (d.start_time || '') + ' ' + (d.end_time || '') + '. Tampilkan QR untuk jadwal ini di bawah.';
if (currentEmpty) currentEmpty.classList.add('hidden');
} else if (d.next_schedule && d.next_schedule.schedule_id) {
var next = d.next_schedule;
scheduleIdToHighlight = next.schedule_id;
if (currentBanner) {
currentBanner.classList.remove('hidden');
currentBanner.classList.remove('border-blue-200', 'dark:border-blue-800', 'bg-blue-50', 'dark:bg-blue-900/20');
currentBanner.classList.add('border-amber-200', 'dark:border-amber-800', 'bg-amber-50', 'dark:bg-amber-900/20');
}
if (currentText) currentText.textContent = 'Berikutnya: ' + (next.subject_name || '-') + ' — Kelas ' + (next.class_name || '-') + ' (mulai ' + (next.start_time || '') + ')';
if (currentHint) currentHint.textContent = 'Belum jam mengajar. Tampilkan QR saat jam tersebut tiba.';
if (currentEmpty) currentEmpty.classList.add('hidden');
} else {
if (currentBanner) currentBanner.classList.add('hidden');
if (currentEmpty) currentEmpty.classList.remove('hidden');
}
if (scheduleIdToHighlight && tbody) {
var rows = tbody.querySelectorAll('tr[data-schedule-id]');
rows.forEach(function(tr) {
var id = tr.getAttribute('data-schedule-id');
if (id && parseInt(id, 10) === scheduleIdToHighlight) {
tr.classList.add('bg-blue-50', 'dark:bg-blue-900/20', 'ring-2', 'ring-inset', 'ring-blue-400', 'dark:ring-blue-500');
var firstCell = tr.querySelector('td');
if (firstCell && !firstCell.querySelector('.badge-current')) {
var badge = document.createElement('span');
badge.className = 'badge-current inline-block ml-1 px-2 py-0.5 text-xs font-medium rounded-full ' + (isActiveNow ? 'bg-blue-600 text-white' : 'bg-amber-500 text-white');
badge.textContent = isActiveNow ? 'Saat ini' : 'Berikutnya';
firstCell.appendChild(badge);
}
}
});
}
})
.catch(function() {
if (currentEmpty) currentEmpty.classList.remove('hidden');
});
}
function showLoading() {
var errorEl = document.getElementById('schedule-error');
var emptyEl = document.getElementById('schedule-empty');
var content = document.getElementById('schedule-content');
var tbody = document.getElementById('schedule-tbody');
function showLoading() {
loading.classList.remove('hidden');
errorEl.classList.add('hidden');
emptyEl.classList.add('hidden');
content.classList.add('hidden');
}
function showError(msg) {
loading.classList.add('hidden');
emptyEl.classList.add('hidden');
content.classList.add('hidden');
errorEl.classList.remove('hidden');
errorEl.textContent = msg;
}
function showEmpty() {
loading.classList.add('hidden');
errorEl.classList.add('hidden');
content.classList.add('hidden');
emptyEl.classList.remove('hidden');
}
function showContent() {
loading.classList.add('hidden');
errorEl.classList.add('hidden');
emptyEl.classList.add('hidden');
content.classList.remove('hidden');
}
function escapeHtml(str) {
if (str == null) return '';
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
fetch(apiUrl, { method: 'GET', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(function(res) { return res.json().then(function(j) { return { ok: res.ok, data: j }; }); })
.then(function(r) {
if (!r.ok) {
showError(r.data && r.data.message ? r.data.message : 'Gagal memuat jadwal');
return;
}
var list = r.data && r.data.data ? r.data.data : r.data;
if (!Array.isArray(list)) {
showError('Data jadwal tidak valid');
return;
}
if (list.length === 0) {
showEmpty();
return;
}
tbody.innerHTML = '';
list.forEach(function(row) {
var tr = document.createElement('tr');
tr.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/30';
tr.setAttribute('data-schedule-id', row.schedule_id);
var timeStr = (row.start_time || '') + ' ' + (row.end_time || '');
var reportUrl = reportBaseUrl + row.schedule_id;
tr.innerHTML =
'<td class="px-6 py-3 font-medium">' + escapeHtml(row.subject_name) + '</td>' +
'<td class="px-6 py-3 text-gray-600 dark:text-gray-400">' + escapeHtml(row.class_name) + '</td>' +
'<td class="px-6 py-3 text-gray-600 dark:text-gray-400">' + escapeHtml(row.teacher_name) + '</td>' +
'<td class="px-6 py-3 text-gray-600 dark:text-gray-400">' + escapeHtml(timeStr) + '</td>' +
'<td class="px-6 py-3 flex flex-wrap gap-2">' +
'<a href="' + escapeHtml(reportUrl) + '" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white bg-primary rounded-lg hover:opacity-90">Laporan</a>' +
'<button type="button" class="btn-qr inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 bg-gray-100 dark:bg-gray-600 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-500" data-schedule-id="' + row.schedule_id + '" data-subject="' + escapeHtml(row.subject_name) + '" data-class="' + escapeHtml(row.class_name) + '">Tampilkan QR</button>' +
'</td>';
tbody.appendChild(tr);
});
showContent();
fetchCurrentAndHighlight();
})
.catch(function() {
showError('Koneksi gagal');
});
var qrModal = document.getElementById('qr-modal');
var qrContainer = document.getElementById('qr-code-container');
var qrSubject = document.getElementById('qr-subject');
var qrClass = document.getElementById('qr-class');
var qrExpires = document.getElementById('qr-expires');
var qrCloseBtn = document.getElementById('qr-modal-close');
var qrApiUrl = baseUrl + '/api/dashboard/qr-attendance/generate';
function closeQrModal() {
if (qrModal) qrModal.classList.add('hidden');
if (qrContainer) {
qrContainer.innerHTML = '';
}
}
tbody.addEventListener('click', function(e) {
var btn = e.target.closest('.btn-qr');
if (!btn) return;
var scheduleId = btn.getAttribute('data-schedule-id');
var subject = btn.getAttribute('data-subject') || '-';
var classNm = btn.getAttribute('data-class') || '-';
if (!scheduleId) return;
if (qrSubject) qrSubject.textContent = subject;
if (qrClass) qrClass.textContent = classNm;
if (qrExpires) qrExpires.textContent = 'Memuat…';
if (qrContainer) qrContainer.innerHTML = '<p class="text-gray-500">Memuat…</p>';
fetch(qrApiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
credentials: 'same-origin',
body: JSON.stringify({ schedule_id: parseInt(scheduleId, 10) })
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (!res.success || !res.data || !res.data.token) {
if (qrContainer) qrContainer.innerHTML = '<p class="text-red-500">' + (res.message || 'Gagal generate QR') + '</p>';
if (qrExpires) qrExpires.textContent = '';
return;
}
var token = res.data.token;
var expiresAt = res.data.expires_at || '';
if (qrExpires) qrExpires.textContent = 'Berlaku sampai: ' + expiresAt;
qrContainer.innerHTML = '';
var qrDiv = document.createElement('div');
qrDiv.id = 'qrcode';
qrDiv.className = 'inline-block p-2 bg-white rounded-lg';
qrContainer.appendChild(qrDiv);
if (typeof QRCode !== 'undefined') {
new QRCode(qrDiv, { text: token, width: 220, height: 220 });
} else {
qrDiv.innerHTML = '<p class="text-gray-600">Token: ' + token.substring(0, 16) + '…<br><small>Pasang library QRCode untuk tampil QR.</small></p>';
}
if (qrModal) qrModal.classList.remove('hidden');
})
.catch(function() {
if (qrContainer) qrContainer.innerHTML = '<p class="text-red-500">Gagal memuat (jaringan).</p>';
if (qrExpires) qrExpires.textContent = '';
});
});
if (qrCloseBtn) qrCloseBtn.addEventListener('click', closeQrModal);
})();
</script>