init backend presensi
This commit is contained in:
339
app/Views/dashboard/parent.php
Normal file
339
app/Views/dashboard/parent.php
Normal file
@@ -0,0 +1,339 @@
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold">Portal Orang Tua</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-1 text-sm">
|
||||
Lihat data absensi dan pelanggaran anak Anda.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pilih Anak -->
|
||||
<div class="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm">
|
||||
<h2 class="font-semibold text-gray-900 dark:text-gray-100 text-sm mb-3">Pilih Anak</h2>
|
||||
<select id="select-child" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm">
|
||||
<option value="">-- Pilih anak --</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Data Anak -->
|
||||
<div id="child-data-section" class="hidden space-y-6">
|
||||
<!-- Profil Anak -->
|
||||
<div class="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm">
|
||||
<h2 class="font-semibold text-gray-900 dark:text-gray-100 text-sm mb-3">Profil Anak</h2>
|
||||
<div id="child-profile" class="text-sm text-gray-600 dark:text-gray-300 space-y-2">
|
||||
<!-- Akan diisi via JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Absensi -->
|
||||
<div class="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h2 class="font-semibold text-gray-900 dark:text-gray-100 text-sm">Riwayat Absensi</h2>
|
||||
</div>
|
||||
<div class="mb-3 space-y-2">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="block mb-1 text-xs text-gray-600 dark:text-gray-300">Dari tanggal</label>
|
||||
<input type="date" id="attendance-from" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-xs">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-1 text-xs text-gray-600 dark:text-gray-300">Sampai tanggal</label>
|
||||
<input type="date" id="attendance-to" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-xs">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" id="btn-load-attendance" class="w-full inline-flex items-center justify-center gap-2 px-3 py-2 rounded-lg bg-primary text-white text-xs font-medium hover:bg-primary-hover">
|
||||
Muat Absensi
|
||||
</button>
|
||||
</div>
|
||||
<div id="attendance-content" class="text-sm">
|
||||
<p id="attendance-empty" class="text-gray-500 dark:text-gray-400 text-xs">Pilih anak dan klik "Muat Absensi" untuk melihat data.</p>
|
||||
<div id="attendance-table-wrap" class="hidden overflow-x-auto">
|
||||
<table class="w-full text-left text-xs">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-400">
|
||||
<tr>
|
||||
<th class="px-3 py-2 font-medium">Tanggal</th>
|
||||
<th class="px-3 py-2 font-medium">Mata Pelajaran</th>
|
||||
<th class="px-3 py-2 font-medium">Guru</th>
|
||||
<th class="px-3 py-2 font-medium">Waktu Masuk</th>
|
||||
<th class="px-3 py-2 font-medium">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="attendance-tbody" class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<!-- Akan diisi via JS -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pelanggaran -->
|
||||
<div class="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm">
|
||||
<h2 class="font-semibold text-gray-900 dark:text-gray-100 text-sm mb-3">Data Pelanggaran</h2>
|
||||
<div id="discipline-summary" class="mb-4 p-3 rounded-lg bg-gray-50 dark:bg-gray-700/50 text-sm">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div class="text-gray-600 dark:text-gray-400 text-xs">Total Poin</div>
|
||||
<div id="discipline-total-points" class="text-lg font-semibold text-gray-900 dark:text-gray-100">-</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-gray-600 dark:text-gray-400 text-xs">Jumlah Kasus</div>
|
||||
<div id="discipline-violation-count" class="text-lg font-semibold text-gray-900 dark:text-gray-100">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 pt-3 border-t border-gray-200 dark:border-gray-600">
|
||||
<div class="text-gray-600 dark:text-gray-400 text-xs">Level / Tindakan</div>
|
||||
<div id="discipline-level" class="text-sm font-medium text-gray-900 dark:text-gray-100 mt-1">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="discipline-content" class="text-sm">
|
||||
<p id="discipline-empty" class="text-gray-500 dark:text-gray-400 text-xs">Memuat data pelanggaran...</p>
|
||||
<div id="discipline-table-wrap" class="hidden overflow-x-auto">
|
||||
<table class="w-full text-left text-xs">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-400">
|
||||
<tr>
|
||||
<th class="px-3 py-2 font-medium">Tanggal</th>
|
||||
<th class="px-3 py-2 font-medium">Kategori</th>
|
||||
<th class="px-3 py-2 font-medium">Jenis Pelanggaran</th>
|
||||
<th class="px-3 py-2 font-medium text-right">Poin</th>
|
||||
<th class="px-3 py-2 font-medium">Catatan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="discipline-tbody" class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<!-- Akan diisi via JS -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var apiBase = '<?= base_url('api/parent') ?>';
|
||||
var selectChild = document.getElementById('select-child');
|
||||
var childDataSection = document.getElementById('child-data-section');
|
||||
var childProfile = document.getElementById('child-profile');
|
||||
var attendanceFrom = document.getElementById('attendance-from');
|
||||
var attendanceTo = document.getElementById('attendance-to');
|
||||
var btnLoadAttendance = document.getElementById('btn-load-attendance');
|
||||
var attendanceEmpty = document.getElementById('attendance-empty');
|
||||
var attendanceTableWrap = document.getElementById('attendance-table-wrap');
|
||||
var attendanceTbody = document.getElementById('attendance-tbody');
|
||||
var disciplineSummary = document.getElementById('discipline-summary');
|
||||
var disciplineTotalPoints = document.getElementById('discipline-total-points');
|
||||
var disciplineViolationCount = document.getElementById('discipline-violation-count');
|
||||
var disciplineLevel = document.getElementById('discipline-level');
|
||||
var disciplineEmpty = document.getElementById('discipline-empty');
|
||||
var disciplineTableWrap = document.getElementById('discipline-table-wrap');
|
||||
var disciplineTbody = document.getElementById('discipline-tbody');
|
||||
|
||||
var children = [];
|
||||
var currentChildId = null;
|
||||
|
||||
function fetchJson(url) {
|
||||
return fetch(url, {
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Accept': 'application/json' }
|
||||
}).then(function(r) {
|
||||
return r.json().then(function(data) {
|
||||
return { ok: r.ok, status: r.status, data: data };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showToast(msg, type) {
|
||||
type = type || 'info';
|
||||
var bg = type === 'error' ? 'bg-red-500' : type === 'success' ? 'bg-green-500' : 'bg-blue-500';
|
||||
var toast = document.createElement('div');
|
||||
toast.className = 'fixed top-4 right-4 ' + bg + ' text-white px-4 py-2 rounded-lg shadow-lg z-50 text-sm';
|
||||
toast.textContent = msg;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(function() {
|
||||
toast.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function formatDate(d) {
|
||||
if (!d) return '-';
|
||||
var date = new Date(d);
|
||||
if (isNaN(date.getTime())) return d;
|
||||
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
|
||||
return pad(date.getDate()) + '/' + pad(date.getMonth() + 1) + '/' + date.getFullYear();
|
||||
}
|
||||
|
||||
function formatDateTime(dt) {
|
||||
if (!dt) return '-';
|
||||
var date = new Date(dt);
|
||||
if (isNaN(date.getTime())) return dt;
|
||||
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
|
||||
return pad(date.getDate()) + '/' + pad(date.getMonth() + 1) + '/' + date.getFullYear() +
|
||||
' ' + pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||
}
|
||||
|
||||
function loadChildren() {
|
||||
fetchJson(apiBase + '/children').then(function(r) {
|
||||
if (!r.ok) {
|
||||
showToast('Gagal memuat daftar anak', 'error');
|
||||
return;
|
||||
}
|
||||
children = (r.data && r.data.data) ? r.data.data : [];
|
||||
selectChild.innerHTML = '<option value="">-- Pilih anak --</option>';
|
||||
children.forEach(function(child) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = child.id;
|
||||
opt.textContent = child.name + (child.class_label ? ' (' + child.class_label + ')' : '');
|
||||
selectChild.appendChild(opt);
|
||||
});
|
||||
if (children.length === 0) {
|
||||
showToast('Tidak ada data anak yang terhubung', 'info');
|
||||
}
|
||||
}).catch(function() {
|
||||
showToast('Gagal memuat daftar anak', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function loadChildProfile(childId) {
|
||||
var child = children.find(function(c) { return c.id == childId; });
|
||||
if (!child) return;
|
||||
childProfile.innerHTML =
|
||||
'<div><strong>Nama:</strong> ' + (child.name || '-') + '</div>' +
|
||||
'<div><strong>NISN:</strong> ' + (child.nisn || '-') + '</div>' +
|
||||
'<div><strong>Kelas:</strong> ' + (child.class_label || '-') + '</div>' +
|
||||
'<div><strong>Jenis Kelamin:</strong> ' + (child.gender === 'L' ? 'Laki-laki' : child.gender === 'P' ? 'Perempuan' : '-') + '</div>' +
|
||||
'<div><strong>Hubungan:</strong> ' + (child.relationship || '-') + '</div>';
|
||||
}
|
||||
|
||||
function loadAttendance() {
|
||||
if (!currentChildId) {
|
||||
showToast('Pilih anak terlebih dahulu', 'error');
|
||||
return;
|
||||
}
|
||||
var from = attendanceFrom.value || '';
|
||||
var to = attendanceTo.value || '';
|
||||
var url = apiBase + '/attendance?student_id=' + encodeURIComponent(currentChildId);
|
||||
if (from) url += '&from=' + encodeURIComponent(from);
|
||||
if (to) url += '&to=' + encodeURIComponent(to);
|
||||
|
||||
btnLoadAttendance.disabled = true;
|
||||
btnLoadAttendance.textContent = 'Memuat...';
|
||||
|
||||
fetchJson(url).then(function(r) {
|
||||
btnLoadAttendance.disabled = false;
|
||||
btnLoadAttendance.textContent = 'Muat Absensi';
|
||||
|
||||
if (!r.ok) {
|
||||
showToast('Gagal memuat data absensi', 'error');
|
||||
return;
|
||||
}
|
||||
var list = (r.data && r.data.data) ? r.data.data : [];
|
||||
if (list.length === 0) {
|
||||
attendanceEmpty.classList.remove('hidden');
|
||||
attendanceTableWrap.classList.add('hidden');
|
||||
attendanceTbody.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
attendanceEmpty.classList.add('hidden');
|
||||
attendanceTableWrap.classList.remove('hidden');
|
||||
attendanceTbody.innerHTML = '';
|
||||
list.forEach(function(item) {
|
||||
var tr = document.createElement('tr');
|
||||
var statusClass = item.status === 'PRESENT' ? 'text-green-600 dark:text-green-400' :
|
||||
item.status === 'LATE' ? 'text-yellow-600 dark:text-yellow-400' :
|
||||
'text-red-600 dark:text-red-400';
|
||||
var statusText = item.status === 'PRESENT' ? 'Hadir' :
|
||||
item.status === 'LATE' ? 'Terlambat' :
|
||||
item.status === 'OUTSIDE_ZONE' ? 'Di Luar Zona' :
|
||||
item.status === 'NO_SCHEDULE' ? 'Tidak Ada Jadwal' :
|
||||
item.status === 'INVALID_DEVICE' ? 'Device Tidak Valid' :
|
||||
item.status || '-';
|
||||
tr.innerHTML =
|
||||
'<td class="px-3 py-2">' + formatDate(item.attendance_date) + '</td>' +
|
||||
'<td class="px-3 py-2">' + (item.subject_name || '-') + '</td>' +
|
||||
'<td class="px-3 py-2">' + (item.teacher_name || '-') + '</td>' +
|
||||
'<td class="px-3 py-2">' + (item.checkin_at ? formatDateTime(item.checkin_at) : '-') + '</td>' +
|
||||
'<td class="px-3 py-2"><span class="' + statusClass + '">' + statusText + '</span></td>';
|
||||
attendanceTbody.appendChild(tr);
|
||||
});
|
||||
}).catch(function() {
|
||||
btnLoadAttendance.disabled = false;
|
||||
btnLoadAttendance.textContent = 'Muat Absensi';
|
||||
showToast('Gagal memuat data absensi', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function loadDiscipline() {
|
||||
if (!currentChildId) return;
|
||||
fetchJson(apiBase + '/discipline?student_id=' + encodeURIComponent(currentChildId)).then(function(r) {
|
||||
if (!r.ok) {
|
||||
showToast('Gagal memuat data pelanggaran', 'error');
|
||||
disciplineEmpty.classList.remove('hidden');
|
||||
disciplineTableWrap.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
var data = (r.data && r.data.data) ? r.data.data : {};
|
||||
var totalPoints = data.total_points || 0;
|
||||
var violationCount = data.violation_count || 0;
|
||||
var level = data.discipline_level || null;
|
||||
var violations = data.violations || [];
|
||||
|
||||
disciplineTotalPoints.textContent = totalPoints;
|
||||
disciplineViolationCount.textContent = violationCount;
|
||||
if (level) {
|
||||
disciplineLevel.textContent = level.title + (level.school_action ? ' — ' + level.school_action : '');
|
||||
} else {
|
||||
disciplineLevel.textContent = '-';
|
||||
}
|
||||
|
||||
if (violations.length === 0) {
|
||||
disciplineEmpty.classList.remove('hidden');
|
||||
disciplineTableWrap.classList.add('hidden');
|
||||
disciplineTbody.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
disciplineEmpty.classList.add('hidden');
|
||||
disciplineTableWrap.classList.remove('hidden');
|
||||
disciplineTbody.innerHTML = '';
|
||||
violations.forEach(function(v) {
|
||||
var tr = document.createElement('tr');
|
||||
tr.innerHTML =
|
||||
'<td class="px-3 py-2">' + formatDateTime(v.occurred_at) + '</td>' +
|
||||
'<td class="px-3 py-2">' + (v.category_name || '-') + '</td>' +
|
||||
'<td class="px-3 py-2">' + (v.violation_title || '-') + '</td>' +
|
||||
'<td class="px-3 py-2 text-right">' + v.score + '</td>' +
|
||||
'<td class="px-3 py-2">' + (v.notes || '-') + '</td>';
|
||||
disciplineTbody.appendChild(tr);
|
||||
});
|
||||
}).catch(function() {
|
||||
showToast('Gagal memuat data pelanggaran', 'error');
|
||||
disciplineEmpty.classList.remove('hidden');
|
||||
disciplineTableWrap.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
selectChild.addEventListener('change', function() {
|
||||
var childId = parseInt(this.value);
|
||||
if (!childId) {
|
||||
currentChildId = null;
|
||||
childDataSection.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
currentChildId = childId;
|
||||
childDataSection.classList.remove('hidden');
|
||||
loadChildProfile(childId);
|
||||
loadDiscipline();
|
||||
// Set default date range untuk absensi (30 hari terakhir)
|
||||
var today = new Date();
|
||||
var lastMonth = new Date(today);
|
||||
lastMonth.setDate(lastMonth.getDate() - 30);
|
||||
attendanceTo.value = today.toISOString().split('T')[0];
|
||||
attendanceFrom.value = lastMonth.toISOString().split('T')[0];
|
||||
});
|
||||
|
||||
btnLoadAttendance.addEventListener('click', loadAttendance);
|
||||
|
||||
// Load children on page load
|
||||
loadChildren();
|
||||
})();
|
||||
</script>
|
||||
Reference in New Issue
Block a user