Files
presensi/app/Views/dashboard/index.php
mwpn 461c5c7882 chore: matikan realtime stream di dashboard
Ganti SSE /api/dashboard/stream dengan fetch sekali ke /api/dashboard/realtime supaya halaman dashboard lebih ringan dan cepat dibuka.
2026-03-06 16:55:03 +07:00

199 lines
10 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

<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">
<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>
</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>
<tbody id="attendance-tbody" class="divide-y divide-gray-200 dark:divide-gray-700"></tbody>
</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');
var realtimeApiUrl = baseUrl + '/api/dashboard/realtime';
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>';
tbody.appendChild(tr);
}
function escapeHtml(str) {
if (str == null) return '';
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
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);
});
}
loadRealtimeOnce();
if (typeof window.addEventListener === 'function') {
window.addEventListener('beforeunload', function() {
if (progressInterval) clearInterval(progressInterval);
});
}
})();
</script>