353 lines
18 KiB
PHP
353 lines
18 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">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>
|