init backend presensi

This commit is contained in:
mwpn
2026-03-05 14:37:36 +07:00
commit b4fda6b9c9
319 changed files with 27261 additions and 0 deletions

View 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>