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

353 lines
18 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">Kelas</h1>
<p class="text-gray-600 dark:text-gray-400 mt-1">Kelola data kelas dan wali kelas.</p>
</div>
<button type="button" id="btn-add-class" 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 Kelas</span>
</button>
</div>
<div id="classes-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="classes-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="classes-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">Label</th>
<th class="px-6 py-3 font-medium">Wali Kelas</th>
<th class="px-6 py-3 font-medium text-right">Aksi</th>
</tr>
</thead>
<tbody id="classes-tbody" class="divide-y divide-gray-200 dark:divide-gray-700"></tbody>
</table>
</div>
</div>
</div>
<!-- Add/Edit Modal -->
<div id="modal-class" class="fixed inset-0 z-50 hidden" aria-hidden="true">
<div class="fixed inset-0 bg-black/50" id="modal-class-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-class-title" class="text-lg font-semibold mb-4">Tambah Kelas</h2>
<form id="form-class">
<input type="hidden" id="form-class-id" value="">
<div class="space-y-4">
<div>
<label for="form-class-grade" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Tingkat <span class="text-red-500">*</span></label>
<input type="text" id="form-class-grade" name="grade" 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" placeholder="Contoh: X, 10, 11, 12">
</div>
<div>
<label for="form-class-major" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Jurusan <span class="text-red-500">*</span></label>
<input type="text" id="form-class-major" name="major" 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" placeholder="Contoh: IPA, IPS, Bahasa">
</div>
<div>
<label for="form-class-name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Rombel <span class="text-red-500">*</span></label>
<input type="text" id="form-class-name" name="name" required maxlength="100" 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" placeholder="Contoh: 1, 2, 3 (muncul jadi X IPA 1, X IPA 2, ...)">
</div>
<div>
<label for="form-class-wali" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Wali Kelas</label>
<select id="form-class-wali" name="wali_user_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>
<div class="flex gap-3 mt-6">
<button type="submit" id="form-class-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-class-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 modal -->
<div id="modal-delete" class="fixed inset-0 z-50 hidden" aria-hidden="true">
<div class="fixed inset-0 bg-black/50" id="modal-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 Kelas</h2>
<p id="modal-delete-message" class="text-gray-600 dark:text-gray-400 mb-6">Yakin ingin menghapus kelas ini?</p>
<div class="flex gap-3">
<button type="button" id="modal-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-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>
<!-- Toast container -->
<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 apiClasses = baseUrl + '/api/academic/classes';
var apiUsers = baseUrl + '/api/users';
var loading = document.getElementById('classes-loading');
var errorEl = document.getElementById('classes-error');
var content = document.getElementById('classes-content');
var tbody = document.getElementById('classes-tbody');
var btnAdd = document.getElementById('btn-add-class');
var modalClass = document.getElementById('modal-class');
var modalClassTitle = document.getElementById('modal-class-title');
var formClass = document.getElementById('form-class');
var formClassId = document.getElementById('form-class-id');
var formClassName = document.getElementById('form-class-name');
var formClassGrade = document.getElementById('form-class-grade');
var formClassMajor = document.getElementById('form-class-major');
var formClassWali = document.getElementById('form-class-wali');
var formClassSubmit = document.getElementById('form-class-submit');
var formClassCancel = document.getElementById('form-class-cancel');
var modalClassBackdrop = document.getElementById('modal-class-backdrop');
var modalDelete = document.getElementById('modal-delete');
var modalDeleteMessage = document.getElementById('modal-delete-message');
var modalDeleteConfirm = document.getElementById('modal-delete-confirm');
var modalDeleteCancel = document.getElementById('modal-delete-cancel');
var modalDeleteBackdrop = document.getElementById('modal-delete-backdrop');
var toastContainer = document.getElementById('toast-container');
var waliUsers = [];
var deleteTargetId = 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 div = document.createElement('div');
div.textContent = str;
return div.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);
}
function fetchOptions() {
return { method: 'GET', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } };
}
function postOptions(body) {
return { method: 'POST', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(body) };
}
function putOptions(body) {
return { method: 'PUT', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(body) };
}
function deleteOptions() {
return { method: 'DELETE', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } };
}
function loadWaliUsers(callback) {
if (waliUsers.length) {
if (callback) callback();
return;
}
Promise.all([
fetch(apiUsers + '?role=WALI_KELAS', fetchOptions()).then(function(r) { return r.json(); }),
fetch(apiUsers + '?role=GURU_MAPEL', fetchOptions()).then(function(r) { return r.json(); })
]).then(function(results) {
var byId = {};
results.forEach(function(res) {
var list = (res && res.data) ? res.data : (res && Array.isArray(res) ? res : []);
list.forEach(function(u) {
byId[u.id] = { id: u.id, name: u.name || u.email };
});
});
waliUsers = Object.values(byId).sort(function(a, b) { return (a.name || '').localeCompare(b.name || ''); });
if (callback) callback();
}).catch(function() {
waliUsers = [];
if (callback) callback();
});
}
function fillWaliDropdown(selectedId) {
var id = selectedId ? (typeof selectedId === 'number' ? selectedId : parseInt(selectedId, 10)) : null;
formClassWali.innerHTML = '<option value="">— Tidak ada —</option>';
waliUsers.forEach(function(u) {
var opt = document.createElement('option');
opt.value = u.id;
opt.textContent = u.name;
if (id && u.id === id) opt.selected = true;
formClassWali.appendChild(opt);
});
}
function loadClasses() {
showLoading();
fetch(apiClasses, fetchOptions())
.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 kelas');
return;
}
var list = (r.data && r.data.data) ? r.data.data : (Array.isArray(r.data) ? r.data : []);
tbody.innerHTML = '';
list.forEach(function(row) {
var tr = document.createElement('tr');
tr.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/30';
var label = row.full_label ? escapeHtml(row.full_label) : (escapeHtml(row.grade) + ' ' + escapeHtml(row.major) + ' ' + escapeHtml(row.name)).trim() || '';
var waliName = (row.wali_name || row.wali_user_name) ? escapeHtml(row.wali_name || row.wali_user_name) : '';
tr.innerHTML =
'<td class="px-6 py-3 font-medium">' + label + '</td>' +
'<td class="px-6 py-3 text-gray-600 dark:text-gray-400">' + waliName + '</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-name="' + escapeHtml(row.name) + '" data-grade="' + escapeHtml(row.grade) + '" data-major="' + escapeHtml(row.major || '') + '" data-wali-id="' + (row.wali_user_id || '') + '">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.full_label || (row.grade + ' ' + (row.major || '') + ' ' + row.name).trim()) + '">Hapus</button>' +
'</td>';
tbody.appendChild(tr);
});
tbody.querySelectorAll('.btn-edit').forEach(function(btn) {
btn.addEventListener('click', function() { openEdit(parseInt(btn.getAttribute('data-id'), 10)); });
});
tbody.querySelectorAll('.btn-delete').forEach(function(btn) {
btn.addEventListener('click', function() { openDelete(parseInt(btn.getAttribute('data-id'), 10), btn.getAttribute('data-name')); });
});
showContent();
})
.catch(function() { showError('Gagal memuat data'); });
}
function openAdd() {
formClassId.value = '';
formClassName.value = '';
formClassGrade.value = '';
formClassMajor.value = '';
modalClassTitle.textContent = 'Tambah Kelas';
fillWaliDropdown(null);
modalClass.classList.remove('hidden');
}
function openEdit(id) {
var btn = tbody.querySelector('.btn-edit[data-id="' + id + '"]');
if (!btn) return;
formClassId.value = id;
formClassName.value = btn.getAttribute('data-name') || '';
formClassGrade.value = btn.getAttribute('data-grade') || '';
formClassMajor.value = btn.getAttribute('data-major') || '';
var waliId = btn.getAttribute('data-wali-id');
waliId = waliId ? parseInt(waliId, 10) : null;
modalClassTitle.textContent = 'Edit Kelas';
fillWaliDropdown(waliId);
modalClass.classList.remove('hidden');
}
function openDelete(id, name) {
deleteTargetId = id;
modalDeleteMessage.textContent = 'Yakin ingin menghapus kelas "' + (name || '') + '"?';
modalDelete.classList.remove('hidden');
}
function closeModalClass() {
modalClass.classList.add('hidden');
}
function closeModalDelete() {
modalDelete.classList.add('hidden');
deleteTargetId = null;
}
formClass.addEventListener('submit', function(e) {
e.preventDefault();
var id = formClassId.value ? parseInt(formClassId.value, 10) : null;
var payload = {
name: formClassName.value.trim(),
grade: formClassGrade.value.trim(),
major: formClassMajor.value.trim(),
wali_user_id: formClassWali.value ? parseInt(formClassWali.value, 10) : null
};
formClassSubmit.disabled = true;
var url = apiClasses;
var opts = postOptions(payload);
if (id) {
url = apiClasses + '/' + id;
opts = putOptions(payload);
}
fetch(url, opts)
.then(function(res) { return res.json().then(function(j) { return { ok: res.ok, data: j }; }); })
.then(function(r) {
formClassSubmit.disabled = false;
if (!r.ok) {
showToast(r.data && r.data.message ? r.data.message : 'Gagal menyimpan', 'error');
return;
}
showToast(id ? 'Kelas berhasil diubah' : 'Kelas berhasil ditambahkan');
closeModalClass();
loadClasses();
})
.catch(function() {
formClassSubmit.disabled = false;
showToast('Gagal menyimpan', 'error');
});
});
formClassCancel.addEventListener('click', closeModalClass);
modalClassBackdrop.addEventListener('click', closeModalClass);
modalDeleteConfirm.addEventListener('click', function() {
if (!deleteTargetId) return;
var id = deleteTargetId;
modalDeleteConfirm.disabled = true;
fetch(apiClasses + '/' + id, deleteOptions())
.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('Kelas berhasil dihapus');
closeModalDelete();
loadClasses();
})
.catch(function() {
modalDeleteConfirm.disabled = false;
showToast('Gagal menghapus', 'error');
});
});
modalDeleteCancel.addEventListener('click', closeModalDelete);
modalDeleteBackdrop.addEventListener('click', closeModalDelete);
btnAdd.addEventListener('click', function() {
loadWaliUsers(function() {
openAdd();
});
});
loadWaliUsers(function() {
loadClasses();
});
})();
</script>