2026-03-05 14:37:36 +07:00
|
|
|
|
<div id="current-lesson-card" class="hidden rounded-2xl border-2 border-primary bg-primary/5 dark:bg-primary/10 border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden mb-6">
|
|
|
|
|
|
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-primary/10 dark:bg-primary/20">
|
|
|
|
|
|
<h2 class="text-lg font-semibold text-primary dark:text-primary-400">CURRENT LESSON</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="p-6 flex flex-wrap items-center justify-between gap-4">
|
|
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Subject</p>
|
|
|
|
|
|
<p id="current-subject" class="font-medium">–</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Class</p>
|
|
|
|
|
|
<p id="current-class" class="font-medium">–</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Teacher</p>
|
|
|
|
|
|
<p id="current-teacher" class="font-medium">–</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Time</p>
|
|
|
|
|
|
<p id="current-time" class="font-medium">–</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a id="current-open-attendance" href="#" class="inline-flex items-center gap-2 px-4 py-2.5 text-sm font-medium text-white bg-primary rounded-lg hover:opacity-90">Open Attendance</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="current-progress-wrap" class="hidden border-t border-gray-200 dark:border-gray-700 px-6 py-4 bg-gray-50 dark:bg-gray-800/50">
|
|
|
|
|
|
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase mb-2">Live Attendance</p>
|
|
|
|
|
|
<div class="flex items-center gap-4 flex-wrap">
|
|
|
|
|
|
<div class="flex-1 min-w-[200px]">
|
|
|
|
|
|
<div class="h-3 rounded-full bg-gray-200 dark:bg-gray-700 overflow-hidden">
|
|
|
|
|
|
<div id="current-progress-bar" class="h-full rounded-full bg-green-500 dark:bg-green-600 transition-all duration-300" style="width: 0%"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="flex gap-6 text-sm">
|
|
|
|
|
|
<span><strong id="current-present">0</strong> hadir</span>
|
|
|
|
|
|
<span><strong id="current-late">0</strong> terlambat</span>
|
|
|
|
|
|
<span><strong id="current-absent">0</strong> tidak hadir</span>
|
|
|
|
|
|
<span class="text-gray-500 dark:text-gray-400"><span id="current-expected">0</span> siswa</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-sm overflow-hidden">
|
|
|
|
|
|
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
2026-03-06 16:55:03 +07:00
|
|
|
|
<h2 class="text-lg font-semibold">Riwayat Absensi Terbaru</h2>
|
|
|
|
|
|
<p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">Daftar absen terakhir (tanpa live stream), paling baru di atas.</p>
|
2026-03-05 14:37:36 +07:00
|
|
|
|
</div>
|
|
|
|
|
|
<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">Time</th>
|
|
|
|
|
|
<th class="px-6 py-3 font-medium">Student</th>
|
|
|
|
|
|
<th class="px-6 py-3 font-medium">Class</th>
|
|
|
|
|
|
<th class="px-6 py-3 font-medium">Subject</th>
|
|
|
|
|
|
<th class="px-6 py-3 font-medium">Status</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
2026-03-06 16:55:03 +07:00
|
|
|
|
<tbody id="attendance-tbody" class="divide-y divide-gray-200 dark:divide-gray-700"></tbody>
|
2026-03-05 14:37:36 +07:00
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
(function() {
|
|
|
|
|
|
var baseUrl = '<?= base_url() ?>'.replace(/\/$/, '');
|
|
|
|
|
|
var currentApiUrl = baseUrl + '/api/dashboard/schedules/current';
|
|
|
|
|
|
var progressApiUrl = baseUrl + '/api/dashboard/attendance/progress/current';
|
|
|
|
|
|
|
|
|
|
|
|
var currentCard = document.getElementById('current-lesson-card');
|
|
|
|
|
|
var currentSubject = document.getElementById('current-subject');
|
|
|
|
|
|
var currentClass = document.getElementById('current-class');
|
|
|
|
|
|
var currentTeacher = document.getElementById('current-teacher');
|
|
|
|
|
|
var currentTime = document.getElementById('current-time');
|
|
|
|
|
|
var currentOpenBtn = document.getElementById('current-open-attendance');
|
|
|
|
|
|
var progressWrap = document.getElementById('current-progress-wrap');
|
|
|
|
|
|
var progressBar = document.getElementById('current-progress-bar');
|
|
|
|
|
|
var elPresent = document.getElementById('current-present');
|
|
|
|
|
|
var elLate = document.getElementById('current-late');
|
|
|
|
|
|
var elAbsent = document.getElementById('current-absent');
|
|
|
|
|
|
var elExpected = document.getElementById('current-expected');
|
|
|
|
|
|
|
|
|
|
|
|
var progressInterval = null;
|
|
|
|
|
|
|
|
|
|
|
|
function updateProgress() {
|
|
|
|
|
|
fetch(progressApiUrl, { 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 || !r.data.data) return;
|
|
|
|
|
|
var d = r.data.data;
|
|
|
|
|
|
if (!d.active) {
|
|
|
|
|
|
if (progressWrap) progressWrap.classList.add('hidden');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (progressWrap) progressWrap.classList.remove('hidden');
|
|
|
|
|
|
var expected = d.expected_total || 0;
|
|
|
|
|
|
var present = d.present_total || 0;
|
|
|
|
|
|
var late = d.late_total || 0;
|
|
|
|
|
|
var absent = d.absent_total || 0;
|
|
|
|
|
|
var pct = expected > 0 ? Math.round((present / expected) * 100) : 0;
|
|
|
|
|
|
if (progressBar) progressBar.style.width = pct + '%';
|
|
|
|
|
|
if (elPresent) elPresent.textContent = present;
|
|
|
|
|
|
if (elLate) elLate.textContent = late;
|
|
|
|
|
|
if (elAbsent) elAbsent.textContent = absent;
|
|
|
|
|
|
if (elExpected) elExpected.textContent = expected;
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(function() {});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 || !r.data.data) return;
|
|
|
|
|
|
var d = r.data.data;
|
|
|
|
|
|
if (d.is_active_now && d.schedule_id) {
|
|
|
|
|
|
currentSubject.textContent = d.subject_name || '–';
|
|
|
|
|
|
currentClass.textContent = d.class_name || '–';
|
|
|
|
|
|
currentTeacher.textContent = d.teacher_name || '–';
|
|
|
|
|
|
currentTime.textContent = (d.start_time && d.end_time) ? (d.start_time + ' – ' + d.end_time) : '–';
|
|
|
|
|
|
currentOpenBtn.href = baseUrl + '/dashboard/attendance/report/' + d.schedule_id;
|
|
|
|
|
|
currentCard.classList.remove('hidden');
|
|
|
|
|
|
updateProgress();
|
|
|
|
|
|
progressInterval = setInterval(updateProgress, 5000);
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(function() {});
|
|
|
|
|
|
|
|
|
|
|
|
var tbody = document.getElementById('attendance-tbody');
|
2026-03-06 16:55:03 +07:00
|
|
|
|
var realtimeApiUrl = baseUrl + '/api/dashboard/realtime';
|
2026-03-05 14:37:36 +07:00
|
|
|
|
|
|
|
|
|
|
function badgeClass(status) {
|
|
|
|
|
|
var s = (status || '').toUpperCase();
|
|
|
|
|
|
if (s === 'PRESENT') return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400';
|
|
|
|
|
|
if (s === 'LATE') return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400';
|
|
|
|
|
|
if (s === 'OUTSIDE_ZONE') return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400';
|
|
|
|
|
|
if (s === 'NO_SCHEDULE' || s === 'INVALID_DEVICE') return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-400';
|
|
|
|
|
|
return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-400';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatTime(iso) {
|
|
|
|
|
|
if (!iso) return '–';
|
|
|
|
|
|
var d = new Date(iso);
|
|
|
|
|
|
return d.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function addRow(data) {
|
|
|
|
|
|
var tr = document.createElement('tr');
|
|
|
|
|
|
tr.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/30';
|
|
|
|
|
|
tr.setAttribute('data-id', data.id);
|
|
|
|
|
|
var statusClass = badgeClass(data.status);
|
|
|
|
|
|
tr.innerHTML =
|
|
|
|
|
|
'<td class="px-6 py-3 text-sm text-gray-600 dark:text-gray-400">' + formatTime(data.checkin_at) + '</td>' +
|
|
|
|
|
|
'<td class="px-6 py-3 font-medium">' + escapeHtml(data.student_name) + '</td>' +
|
|
|
|
|
|
'<td class="px-6 py-3 text-gray-600 dark:text-gray-400">' + escapeHtml(data.class_name) + '</td>' +
|
|
|
|
|
|
'<td class="px-6 py-3 text-gray-600 dark:text-gray-400">' + escapeHtml(data.subject) + '</td>' +
|
|
|
|
|
|
'<td class="px-6 py-3"><span class="inline-flex px-2.5 py-0.5 rounded-full text-xs font-medium ' + statusClass + '">' + escapeHtml(data.status) + '</span></td>';
|
2026-03-06 16:55:03 +07:00
|
|
|
|
tbody.appendChild(tr);
|
2026-03-05 14:37:36 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function escapeHtml(str) {
|
|
|
|
|
|
if (str == null) return '';
|
|
|
|
|
|
var div = document.createElement('div');
|
|
|
|
|
|
div.textContent = str;
|
|
|
|
|
|
return div.innerHTML;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-06 16:55:03 +07:00
|
|
|
|
function loadRealtimeOnce() {
|
|
|
|
|
|
fetch(realtimeApiUrl, { method: 'GET', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } })
|
|
|
|
|
|
.then(function(res) { return res.json().then(function(j) { return { ok: res.ok, data: j }; }); })
|
|
|
|
|
|
.then(function(r) {
|
|
|
|
|
|
if (!r.ok) return;
|
|
|
|
|
|
var list = (r.data && r.data.data) ? r.data.data : (Array.isArray(r.data) ? r.data : []);
|
|
|
|
|
|
tbody.innerHTML = '';
|
|
|
|
|
|
if (!list.length) {
|
|
|
|
|
|
var tr = document.createElement('tr');
|
|
|
|
|
|
tr.innerHTML = '<td colspan="5" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400">Belum ada data absensi hari ini.</td>';
|
|
|
|
|
|
tbody.appendChild(tr);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
list.forEach(function(row) { addRow(row); });
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(function() {
|
|
|
|
|
|
var tr = document.createElement('tr');
|
|
|
|
|
|
tr.innerHTML = '<td colspan="5" class="px-6 py-8 text-center text-red-500 dark:text-red-400">Gagal memuat data absensi.</td>';
|
|
|
|
|
|
tbody.appendChild(tr);
|
|
|
|
|
|
});
|
2026-03-05 14:37:36 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-06 16:55:03 +07:00
|
|
|
|
loadRealtimeOnce();
|
2026-03-05 14:37:36 +07:00
|
|
|
|
|
|
|
|
|
|
if (typeof window.addEventListener === 'function') {
|
|
|
|
|
|
window.addEventListener('beforeunload', function() {
|
|
|
|
|
|
if (progressInterval) clearInterval(progressInterval);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|
|
|
|
|
|
</script>
|