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

380 lines
24 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 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">Siswa</h1>
<p class="text-gray-600 dark:text-gray-400 mt-1">Kelola data siswa.</p>
</div>
<button type="button" id="btn-add-student" class="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg bg-primary text-white font-medium hover:bg-primary-hover transition-colors">
<i class="bx bx-plus text-xl"></i>
<span>Tambah Siswa</span>
</button>
</div>
<div class="flex flex-col sm:flex-row gap-4">
<div class="flex-1">
<label for="filter-search" class="sr-only">Cari</label>
<input type="text" id="filter-search" placeholder="Cari nama atau NISN..." class="w-full px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
<div class="w-full sm:w-48">
<label for="filter-class" class="sr-only">Kelas</label>
<select id="filter-class" class="w-full px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
<option value="">Semua kelas</option>
</select>
</div>
<label class="flex items-center gap-2 cursor-pointer self-center whitespace-nowrap">
<input type="checkbox" id="filter-unmapped" class="rounded border-gray-300 text-primary focus:ring-primary">
<span class="text-sm text-gray-700 dark:text-gray-300">Hanya unmapped</span>
</label>
<div class="flex items-center gap-2 self-center">
<label for="filter-per-page" class="text-sm text-gray-600 dark:text-gray-400">Per halaman</label>
<select id="filter-per-page" class="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm">
<option value="20" selected>20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</div>
<div id="students-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">Memuat…</div>
<div id="students-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="students-content" class="hidden rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-sm overflow-hidden">
<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">NISN</th>
<th class="px-6 py-3 font-medium">Nama</th>
<th class="px-6 py-3 font-medium">JK</th>
<th class="px-6 py-3 font-medium">Kelas</th>
<th class="px-6 py-3 font-medium">Status</th>
<th class="px-6 py-3 font-medium text-right">Aksi</th>
</tr>
</thead>
<tbody id="students-tbody" class="divide-y divide-gray-200 dark:divide-gray-700"></tbody>
</table>
</div>
<div id="students-pagination" class="flex flex-wrap items-center justify-between gap-4 px-6 py-4 border-t border-gray-200 dark:border-gray-700">
<p id="pagination-info" class="text-sm text-gray-600 dark:text-gray-400"></p>
<div id="pagination-buttons" class="flex items-center gap-2"></div>
</div>
</div>
</div>
<!-- Add/Edit Modal -->
<div id="modal-student" class="fixed inset-0 z-50 hidden" aria-hidden="true">
<div class="fixed inset-0 bg-black/50" id="modal-student-backdrop"></div>
<div class="fixed inset-0 flex items-center justify-center p-4">
<div class="relative w-full max-w-md rounded-2xl bg-white dark:bg-gray-800 shadow-xl border border-gray-200 dark:border-gray-700">
<div class="p-6">
<h2 id="modal-student-title" class="text-lg font-semibold mb-4">Tambah Siswa</h2>
<form id="form-student">
<input type="hidden" id="form-student-id" value="">
<div class="space-y-4">
<div>
<label for="form-student-nisn" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">NISN <span class="text-red-500">*</span></label>
<input type="text" id="form-student-nisn" name="nisn" required maxlength="50" class="w-full px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
<div>
<label for="form-student-name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Nama <span class="text-red-500">*</span></label>
<input type="text" id="form-student-name" name="name" required maxlength="255" class="w-full px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
</div>
<div>
<label for="form-student-gender" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Jenis Kelamin</label>
<select id="form-student-gender" name="gender" class="w-full px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
<option value=""> Pilih </option>
<option value="L">Laki-laki</option>
<option value="P">Perempuan</option>
</select>
</div>
<div>
<label for="form-student-class" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Kelas</label>
<select id="form-student-class" name="class_id" class="w-full px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary focus:border-transparent">
<option value=""> Tidak ada </option>
</select>
</div>
<div>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="form-student-active" name="is_active" value="1" checked class="rounded border-gray-300 text-primary focus:ring-primary">
<span class="text-sm text-gray-700 dark:text-gray-300">Aktif</span>
</label>
</div>
</div>
<div class="flex gap-3 mt-6">
<button type="submit" id="form-student-submit" class="flex-1 px-4 py-2.5 rounded-lg bg-primary text-white font-medium hover:bg-primary-hover">Simpan</button>
<button type="button" id="form-student-cancel" class="px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">Batal</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Delete confirmation -->
<div id="modal-student-delete" class="fixed inset-0 z-50 hidden" aria-hidden="true">
<div class="fixed inset-0 bg-black/50" id="modal-student-delete-backdrop"></div>
<div class="fixed inset-0 flex items-center justify-center p-4">
<div class="relative w-full max-w-sm rounded-2xl bg-white dark:bg-gray-800 shadow-xl border border-gray-200 dark:border-gray-700 p-6">
<h2 class="text-lg font-semibold mb-2">Hapus Siswa</h2>
<p id="modal-student-delete-message" class="text-gray-600 dark:text-gray-400 mb-6">Yakin ingin menghapus?</p>
<div class="flex gap-3">
<button type="button" id="modal-student-delete-confirm" class="flex-1 px-4 py-2.5 rounded-lg bg-red-600 text-white font-medium hover:bg-red-700">Hapus</button>
<button type="button" id="modal-student-delete-cancel" class="px-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">Batal</button>
</div>
</div>
</div>
</div>
<div id="toast-container" class="fixed bottom-6 right-6 z-[60] flex flex-col gap-2 pointer-events-none"></div>
<script>
(function() {
var baseUrl = '<?= base_url() ?>'.replace(/\/$/, '');
var apiStudents = baseUrl + '/api/academic/students';
var apiClasses = baseUrl + '/api/academic/classes';
var loading = document.getElementById('students-loading');
var errorEl = document.getElementById('students-error');
var content = document.getElementById('students-content');
var tbody = document.getElementById('students-tbody');
var filterSearch = document.getElementById('filter-search');
var filterClass = document.getElementById('filter-class');
var filterUnmapped = document.getElementById('filter-unmapped');
var filterPerPage = document.getElementById('filter-per-page');
var btnAdd = document.getElementById('btn-add-student');
var paginationInfo = document.getElementById('pagination-info');
var paginationButtons = document.getElementById('pagination-buttons');
var currentPage = 1;
var modal = document.getElementById('modal-student');
var modalTitle = document.getElementById('modal-student-title');
var form = document.getElementById('form-student');
var formId = document.getElementById('form-student-id');
var formNisn = document.getElementById('form-student-nisn');
var formName = document.getElementById('form-student-name');
var formGender = document.getElementById('form-student-gender');
var formClass = document.getElementById('form-student-class');
var formActive = document.getElementById('form-student-active');
var formSubmit = document.getElementById('form-student-submit');
var formCancel = document.getElementById('form-student-cancel');
var modalBackdrop = document.getElementById('modal-student-backdrop');
var modalDelete = document.getElementById('modal-student-delete');
var modalDeleteMsg = document.getElementById('modal-student-delete-message');
var modalDeleteConfirm = document.getElementById('modal-student-delete-confirm');
var modalDeleteCancel = document.getElementById('modal-student-delete-cancel');
var modalDeleteBackdrop = document.getElementById('modal-student-delete-backdrop');
var toastContainer = document.getElementById('toast-container');
var deleteTargetId = null;
var classesList = [];
var searchTimeout = null;
function showLoading() { loading.classList.remove('hidden'); errorEl.classList.add('hidden'); content.classList.add('hidden'); }
function showError(msg) { loading.classList.add('hidden'); content.classList.add('hidden'); errorEl.classList.remove('hidden'); errorEl.textContent = msg; }
function showContent() { loading.classList.add('hidden'); errorEl.classList.add('hidden'); content.classList.remove('hidden'); }
function escapeHtml(str) { if (str == null) return ''; var d = document.createElement('div'); d.textContent = str; return d.innerHTML; }
function showToast(message, type) {
type = type || 'success';
var el = document.createElement('div');
el.className = 'pointer-events-auto px-4 py-3 rounded-lg shadow-lg text-sm font-medium ' + (type === 'success' ? 'bg-green-600 text-white' : 'bg-red-600 text-white');
el.textContent = message;
toastContainer.appendChild(el);
setTimeout(function() { if (el.parentNode) el.parentNode.removeChild(el); }, 3000);
}
var fetchOpts = { method: 'GET', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } };
function postOpts(body) { return { method: 'POST', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(body) }; }
function putOpts(body) { return { method: 'PUT', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(body) }; }
function deleteOpts() { return { method: 'DELETE', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } }; }
function loadClasses(callback) {
fetch(apiClasses, fetchOpts)
.then(function(res) { return res.json(); })
.then(function(r) {
var list = (r && r.data && r.data.data) ? r.data.data : (Array.isArray(r && r.data) ? r.data : []);
classesList = list;
filterClass.innerHTML = '<option value="">Semua kelas</option>';
formClass.innerHTML = '<option value="">— Tidak ada —</option>';
list.forEach(function(c) {
var label = c.full_label || (c.grade + ' ' + (c.major || '') + ' ' + c.name).trim();
var opt1 = document.createElement('option');
opt1.value = c.id;
opt1.textContent = label;
filterClass.appendChild(opt1);
var opt2 = document.createElement('option');
opt2.value = c.id;
opt2.textContent = label;
formClass.appendChild(opt2);
});
if (callback) callback();
})
.catch(function() { if (callback) callback(); });
}
function buildStudentsUrl(page) {
var p = page != null ? page : currentPage;
var url = apiStudents;
var params = [];
if (filterUnmapped && filterUnmapped.checked) params.push('unmapped_only=1');
else if (filterClass.value) params.push('class_id=' + encodeURIComponent(filterClass.value));
if (filterSearch.value.trim()) params.push('search=' + encodeURIComponent(filterSearch.value.trim()));
params.push('page=' + p);
params.push('per_page=' + (filterPerPage ? filterPerPage.value : 20));
url += '?' + params.join('&');
return url;
}
function renderPagination(meta) {
if (!paginationInfo || !paginationButtons || !meta) return;
var total = meta.total || 0;
var page = meta.page || 1;
var perPage = meta.per_page || 20;
var totalPages = meta.total_pages || 0;
var from = total === 0 ? 0 : (page - 1) * perPage + 1;
var to = Math.min(page * perPage, total);
paginationInfo.textContent = 'Menampilkan ' + from + '' + to + ' dari ' + total + ' siswa';
paginationButtons.innerHTML = '';
if (totalPages <= 1) return;
var prevBtn = document.createElement('button');
prevBtn.type = 'button';
prevBtn.className = 'px-3 py-1.5 rounded-lg border border-gray-300 dark:border-gray-600 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed';
prevBtn.textContent = 'Sebelumnya';
prevBtn.disabled = page <= 1;
prevBtn.addEventListener('click', function() { if (page > 1) { currentPage = page - 1; loadStudents(); } });
paginationButtons.appendChild(prevBtn);
var nextBtn = document.createElement('button');
nextBtn.type = 'button';
nextBtn.className = 'px-3 py-1.5 rounded-lg border border-gray-300 dark:border-gray-600 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed';
nextBtn.textContent = 'Berikutnya';
nextBtn.disabled = page >= totalPages;
nextBtn.addEventListener('click', function() { if (page < totalPages) { currentPage = page + 1; loadStudents(); } });
paginationButtons.appendChild(nextBtn);
var pageInfo = document.createElement('span');
pageInfo.className = 'text-sm text-gray-600 dark:text-gray-400 ml-2';
pageInfo.textContent = 'Halaman ' + page + ' dari ' + totalPages;
paginationButtons.appendChild(pageInfo);
}
function loadStudents(page) {
if (page != null) currentPage = page;
showLoading();
fetch(buildStudentsUrl(), fetchOpts)
.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 : 'Gagal memuat'); return; }
var response = r.data;
var list = (response && response.data) ? response.data : (Array.isArray(response) ? response : []);
var meta = (response && response.meta) ? response.meta : null;
tbody.innerHTML = '';
list.forEach(function(row) {
var tr = document.createElement('tr');
tr.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/30';
var genderText = row.gender === 'L' ? 'L' : row.gender === 'P' ? 'P' : '';
var classCell = row.class_id == null
? '<span class="inline-flex px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300">UNMAPPED</span>'
: (row.class_label ? escapeHtml(row.class_label) : '');
var status = row.is_active ? 'Aktif' : 'Nonaktif';
var statusClass = row.is_active ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-400';
tr.innerHTML =
'<td class="px-6 py-3 font-medium">' + escapeHtml(row.nisn) + '</td>' +
'<td class="px-6 py-3">' + escapeHtml(row.name) + '</td>' +
'<td class="px-6 py-3 text-gray-600 dark:text-gray-400">' + genderText + '</td>' +
'<td class="px-6 py-3 text-gray-600 dark:text-gray-400">' + classCell + '</td>' +
'<td class="px-6 py-3"><span class="inline-flex px-2.5 py-0.5 rounded-full text-xs font-medium ' + statusClass + '">' + status + '</span></td>' +
'<td class="px-6 py-3 text-right">' +
'<button type="button" class="btn-edit px-3 py-1.5 rounded-lg text-primary hover:bg-primary/10" data-id="' + row.id + '" data-nisn="' + escapeHtml(row.nisn) + '" data-name="' + escapeHtml(row.name) + '" data-gender="' + (row.gender || '') + '" data-class-id="' + (row.class_id || '') + '" data-active="' + (row.is_active ? '1' : '0') + '">Edit</button> ' +
'<button type="button" class="btn-delete px-3 py-1.5 rounded-lg text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20" data-id="' + row.id + '" data-name="' + escapeHtml(row.name) + '">Hapus</button>' +
'</td>';
tbody.appendChild(tr);
});
tbody.querySelectorAll('.btn-edit').forEach(function(btn) {
btn.addEventListener('click', function() {
formId.value = btn.getAttribute('data-id');
formNisn.value = btn.getAttribute('data-nisn') || '';
formName.value = btn.getAttribute('data-name') || '';
formGender.value = btn.getAttribute('data-gender') || '';
formClass.value = btn.getAttribute('data-class-id') || '';
formActive.checked = btn.getAttribute('data-active') === '1';
modalTitle.textContent = 'Edit Siswa';
modal.classList.remove('hidden');
});
});
tbody.querySelectorAll('.btn-delete').forEach(function(btn) {
btn.addEventListener('click', function() {
deleteTargetId = parseInt(btn.getAttribute('data-id'), 10);
modalDeleteMsg.textContent = 'Yakin ingin menghapus "' + (btn.getAttribute('data-name') || '') + '"?';
modalDelete.classList.remove('hidden');
});
});
renderPagination(meta);
showContent();
})
.catch(function() { showError('Gagal memuat data'); });
}
filterClass.addEventListener('change', function() { currentPage = 1; loadStudents(); });
if (filterUnmapped) filterUnmapped.addEventListener('change', function() { currentPage = 1; loadStudents(); });
if (filterPerPage) filterPerPage.addEventListener('change', function() { currentPage = 1; loadStudents(); });
filterSearch.addEventListener('input', function() {
if (searchTimeout) clearTimeout(searchTimeout);
searchTimeout = setTimeout(function() { currentPage = 1; loadStudents(); }, 300);
});
btnAdd.addEventListener('click', function() {
formId.value = ''; formNisn.value = ''; formName.value = ''; formGender.value = ''; formClass.value = '';
formActive.checked = true;
modalTitle.textContent = 'Tambah Siswa';
modal.classList.remove('hidden');
});
form.addEventListener('submit', function(e) {
e.preventDefault();
var id = formId.value ? parseInt(formId.value, 10) : null;
var payload = {
nisn: formNisn.value.trim(),
name: formName.value.trim(),
gender: formGender.value || null,
class_id: formClass.value ? parseInt(formClass.value, 10) : null,
is_active: formActive.checked ? 1 : 0
};
formSubmit.disabled = true;
var url = apiStudents;
var opts = postOpts(payload);
if (id) { url = apiStudents + '/' + id; opts = putOpts(payload); }
fetch(url, opts)
.then(function(res) { return res.json().then(function(j) { return { ok: res.ok, data: j }; }); })
.then(function(r) {
formSubmit.disabled = false;
if (!r.ok) { showToast(r.data && r.data.message ? r.data.message : 'Gagal menyimpan', 'error'); return; }
showToast(id ? 'Berhasil diubah' : 'Berhasil ditambahkan');
modal.classList.add('hidden');
loadStudents();
})
.catch(function() { formSubmit.disabled = false; showToast('Gagal menyimpan', 'error'); });
});
formCancel.addEventListener('click', function() { modal.classList.add('hidden'); });
modalBackdrop.addEventListener('click', function() { modal.classList.add('hidden'); });
modalDeleteConfirm.addEventListener('click', function() {
if (!deleteTargetId) return;
var id = deleteTargetId;
modalDeleteConfirm.disabled = true;
fetch(apiStudents + '/' + id, deleteOpts())
.then(function(res) { return res.json().then(function(j) { return { ok: res.ok, data: j }; }); })
.then(function(r) {
modalDeleteConfirm.disabled = false;
if (!r.ok) { showToast(r.data && r.data.message ? r.data.message : 'Gagal menghapus', 'error'); return; }
showToast('Berhasil dihapus');
modalDelete.classList.add('hidden');
deleteTargetId = null;
loadStudents();
})
.catch(function() { modalDeleteConfirm.disabled = false; showToast('Gagal menghapus', 'error'); });
});
modalDeleteCancel.addEventListener('click', function() { modalDelete.classList.add('hidden'); deleteTargetId = null; });
modalDeleteBackdrop.addEventListener('click', function() { modalDelete.classList.add('hidden'); deleteTargetId = null; });
loadClasses(function() { loadStudents(); });
})();
</script>