init backend presensi
This commit is contained in:
213
app/Views/dashboard/attendance_report.php
Normal file
213
app/Views/dashboard/attendance_report.php
Normal file
@@ -0,0 +1,213 @@
|
||||
<div class="space-y-6" id="attendance-report-app">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<h1 class="text-xl font-semibold">Schedule Attendance Report</h1>
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="report-date" class="text-sm text-gray-600 dark:text-gray-400">Date</label>
|
||||
<input type="date" id="report-date" class="rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm focus:ring-2 focus:ring-primary"
|
||||
value="<?= date('Y-m-d') ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="report-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 report…
|
||||
</div>
|
||||
<div id="report-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="report-content" class="hidden space-y-6">
|
||||
<!-- SECTION 1: Schedule Info -->
|
||||
<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 bg-gray-50 dark:bg-gray-700/30">
|
||||
<h2 class="text-lg font-semibold">Schedule Info</h2>
|
||||
</div>
|
||||
<div class="p-6 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 tracking-wide">Subject</p>
|
||||
<p id="info-subject" class="mt-1 font-medium">–</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Class</p>
|
||||
<p id="info-class" class="mt-1 font-medium">–</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Teacher</p>
|
||||
<p id="info-teacher" class="mt-1 font-medium">–</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Time</p>
|
||||
<p id="info-time" class="mt-1 font-medium">–</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SECTION 2: Summary Cards -->
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div class="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-6 shadow-sm">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Expected</p>
|
||||
<p id="summary-expected" class="mt-2 text-2xl font-bold text-gray-900 dark:text-white">0</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-6 shadow-sm">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Present</p>
|
||||
<p id="summary-present" class="mt-2 text-2xl font-bold text-green-600 dark:text-green-400">0</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-6 shadow-sm">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Late</p>
|
||||
<p id="summary-late" class="mt-2 text-2xl font-bold text-yellow-600 dark:text-yellow-400">0</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-6 shadow-sm">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Absent</p>
|
||||
<p id="summary-absent" class="mt-2 text-2xl font-bold text-red-600 dark:text-red-400">0</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SECTION 3: Present List -->
|
||||
<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 bg-gray-50 dark:bg-gray-700/30">
|
||||
<h2 class="text-lg font-semibold">Present</h2>
|
||||
</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">NIS</th>
|
||||
<th class="px-6 py-3 font-medium">Name</th>
|
||||
<th class="px-6 py-3 font-medium">Status</th>
|
||||
<th class="px-6 py-3 font-medium">Check-in time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="present-tbody" class="divide-y divide-gray-200 dark:divide-gray-700"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SECTION 4: Absent List -->
|
||||
<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 bg-gray-50 dark:bg-gray-700/30">
|
||||
<h2 class="text-lg font-semibold">Absent</h2>
|
||||
</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">NIS</th>
|
||||
<th class="px-6 py-3 font-medium">Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="absent-tbody" class="divide-y divide-gray-200 dark:divide-gray-700"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var scheduleId = <?= (int) $scheduleId ?>;
|
||||
var baseUrl = '<?= base_url() ?>';
|
||||
var apiUrl = baseUrl.replace(/\/$/, '') + '/api/attendance/report/schedule/' + scheduleId;
|
||||
|
||||
var reportLoading = document.getElementById('report-loading');
|
||||
var reportError = document.getElementById('report-error');
|
||||
var reportContent = document.getElementById('report-content');
|
||||
var reportDate = document.getElementById('report-date');
|
||||
|
||||
function showLoading() {
|
||||
reportLoading.classList.remove('hidden');
|
||||
reportError.classList.add('hidden');
|
||||
reportContent.classList.add('hidden');
|
||||
}
|
||||
function showError(msg) {
|
||||
reportLoading.classList.add('hidden');
|
||||
reportContent.classList.add('hidden');
|
||||
reportError.classList.remove('hidden');
|
||||
reportError.textContent = msg;
|
||||
}
|
||||
function showContent() {
|
||||
reportLoading.classList.add('hidden');
|
||||
reportError.classList.add('hidden');
|
||||
reportContent.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (str == null) return '';
|
||||
var div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
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 statusBadgeClass(status) {
|
||||
return status === 'LATE' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400' : 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400';
|
||||
}
|
||||
|
||||
function renderReport(data) {
|
||||
var schedule = data.schedule || {};
|
||||
var summary = data.summary || {};
|
||||
var present = data.present || [];
|
||||
var absent = data.absent || [];
|
||||
|
||||
document.getElementById('info-subject').textContent = schedule.subject || '–';
|
||||
document.getElementById('info-class').textContent = schedule.class_name || ('Class #' + (schedule.class_id || '–'));
|
||||
document.getElementById('info-teacher').textContent = schedule.teacher || '–';
|
||||
document.getElementById('info-time').textContent = (schedule.start_time && schedule.end_time) ? (schedule.start_time + ' – ' + schedule.end_time) : '–';
|
||||
|
||||
document.getElementById('summary-expected').textContent = summary.expected_total != null ? summary.expected_total : 0;
|
||||
document.getElementById('summary-present').textContent = summary.present_total != null ? summary.present_total : 0;
|
||||
document.getElementById('summary-late').textContent = summary.late_total != null ? summary.late_total : 0;
|
||||
document.getElementById('summary-absent').textContent = summary.absent_total != null ? summary.absent_total : 0;
|
||||
|
||||
var presentTbody = document.getElementById('present-tbody');
|
||||
presentTbody.innerHTML = '';
|
||||
present.forEach(function(row) {
|
||||
var tr = document.createElement('tr');
|
||||
tr.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/30';
|
||||
var statusClass = statusBadgeClass(row.status);
|
||||
tr.innerHTML = '<td class="px-6 py-3 text-gray-600 dark:text-gray-400">' + escapeHtml(row.nisn) + '</td>' +
|
||||
'<td class="px-6 py-3 font-medium">' + escapeHtml(row.name) + '</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(row.status) + '</span></td>' +
|
||||
'<td class="px-6 py-3 text-gray-600 dark:text-gray-400">' + formatTime(row.checkin_at) + '</td>';
|
||||
presentTbody.appendChild(tr);
|
||||
});
|
||||
|
||||
var absentTbody = document.getElementById('absent-tbody');
|
||||
absentTbody.innerHTML = '';
|
||||
absent.forEach(function(row) {
|
||||
var tr = document.createElement('tr');
|
||||
tr.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/30';
|
||||
tr.innerHTML = '<td class="px-6 py-3 text-gray-600 dark:text-gray-400">' + escapeHtml(row.nisn) + '</td>' +
|
||||
'<td class="px-6 py-3 font-medium">' + escapeHtml(row.name) + '</td>';
|
||||
absentTbody.appendChild(tr);
|
||||
});
|
||||
|
||||
showContent();
|
||||
}
|
||||
|
||||
function loadReport() {
|
||||
showLoading();
|
||||
var date = reportDate.value || new Date().toISOString().slice(0, 10);
|
||||
var url = apiUrl + '?date=' + encodeURIComponent(date);
|
||||
|
||||
fetch(url, { 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 : 'Failed to load report');
|
||||
return;
|
||||
}
|
||||
var data = r.data && r.data.data ? r.data.data : r.data;
|
||||
if (!data) {
|
||||
showError('Invalid response');
|
||||
return;
|
||||
}
|
||||
renderReport(data);
|
||||
})
|
||||
.catch(function() {
|
||||
showError('Network error');
|
||||
});
|
||||
}
|
||||
|
||||
reportDate.addEventListener('change', loadReport);
|
||||
loadReport();
|
||||
})();
|
||||
</script>
|
||||
Reference in New Issue
Block a user