Files
presensi/app/Views/dashboard/index.php
2026-03-05 14:37:36 +07:00

225 lines
11 KiB
PHP
Raw 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">Realtime Attendance</h2>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">Live stream via Server-Sent Events. Newest at top.</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">
<tr>
<td colspan="5" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400">Connecting to stream…</td>
</tr>
</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 streamUrl = baseUrl + '/api/dashboard/stream';
var afterId = 0;
var placeholderRow = null;
var connected = false;
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) {
if (placeholderRow && placeholderRow.parentNode) {
placeholderRow.remove();
placeholderRow = null;
}
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.insertBefore(tr, tbody.firstChild);
if (data.id > afterId) afterId = data.id;
}
function escapeHtml(str) {
if (str == null) return '';
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function setPlaceholder(msg) {
if (placeholderRow && placeholderRow.parentNode) placeholderRow.remove();
placeholderRow = document.createElement('tr');
placeholderRow.innerHTML = '<td colspan="5" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400">' + escapeHtml(msg) + '</td>';
tbody.appendChild(placeholderRow);
}
function connect() {
var url = streamUrl + (afterId ? '?after_id=' + afterId : '');
var es = new EventSource(url);
es.addEventListener('attendance', function(e) {
try {
var data = JSON.parse(e.data);
if (data && data.id) addRow(data);
} catch (err) {}
});
es.addEventListener('heartbeat', function() {
if (!connected) {
connected = true;
setPlaceholder('No attendance records yet. Waiting for new check-ins…');
}
});
es.addEventListener('timeout', function() {
es.close();
connected = false;
setPlaceholder('Stream ended. Reconnecting…');
setTimeout(connect, 3000);
});
es.onerror = function() {
es.close();
connected = false;
setPlaceholder('Connection lost. Reconnecting…');
setTimeout(connect, 3000);
};
}
connect();
if (typeof window.addEventListener === 'function') {
window.addEventListener('beforeunload', function() {
if (progressInterval) clearInterval(progressInterval);
});
}
})();
</script>