340 lines
17 KiB
PHP
340 lines
17 KiB
PHP
<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>
|