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

394 lines
24 KiB
PHP
Raw Permalink 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">Guru / PTK</h1>
<p class="text-gray-600 dark:text-gray-400 mt-1">Kelola data guru dan wali kelas.</p>
</div>
<button type="button" id="btn-add-teacher" 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 Guru</span>
</button>
</div>
<div id="teachers-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="teachers-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="teachers-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">Nama</th>
<th class="px-6 py-3 font-medium">Email</th>
<th class="px-6 py-3 font-medium">Role</th>
<th class="px-6 py-3 font-medium text-right">Aksi</th>
</tr>
</thead>
<tbody id="teachers-tbody" class="divide-y divide-gray-200 dark:divide-gray-700"></tbody>
</table>
</div>
</div>
</div>
<!-- Add/Edit Modal -->
<div id="modal-teacher" class="fixed inset-0 z-50 hidden" aria-hidden="true">
<div class="fixed inset-0 bg-black/50" id="modal-teacher-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-teacher-title" class="text-lg font-semibold mb-4">Tambah Guru</h2>
<form id="form-teacher">
<input type="hidden" id="form-teacher-id" value="">
<div class="space-y-4">
<div>
<label for="form-teacher-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-teacher-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-teacher-email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email <span class="text-red-500">*</span></label>
<input type="email" id="form-teacher-email" name="email" 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 id="form-teacher-password-wrap">
<label for="form-teacher-password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Password <span id="form-teacher-password-req" class="text-red-500">*</span></label>
<input type="password" id="form-teacher-password" name="password" 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="Kosongkan jika tidak diubah">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Role <span class="text-red-500">*</span></label>
<div class="space-y-2">
<label class="inline-flex items-center gap-2">
<input type="checkbox" id="form-role-guru" class="rounded border-gray-300 text-primary focus:ring-primary">
<span class="text-sm text-gray-700 dark:text-gray-300">Guru Mata Pelajaran</span>
</label>
<label class="inline-flex items-center gap-2">
<input type="checkbox" id="form-role-wali" class="rounded border-gray-300 text-primary focus:ring-primary">
<span class="text-sm text-gray-700 dark:text-gray-300">Wali Kelas</span>
</label>
</div>
</div>
</div>
<div class="flex gap-3 mt-6">
<button type="submit" id="form-teacher-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-teacher-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>
<!-- Mapel Diajar Modal -->
<div id="modal-subjects" class="fixed inset-0 z-50 hidden" aria-hidden="true">
<div class="fixed inset-0 bg-black/50" id="modal-subjects-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-subjects-title" class="text-lg font-semibold mb-4">Mapel yang Diajar</h2>
<p id="modal-subjects-teacher" class="text-sm text-gray-600 dark:text-gray-400 mb-4"></p>
<div id="modal-subjects-body" class="max-h-72 overflow-y-auto space-y-2 text-sm text-gray-700 dark:text-gray-200">
<!-- checkbox subjects will be injected here -->
</div>
<div class="flex gap-3 mt-6">
<button type="button" id="modal-subjects-save" 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="modal-subjects-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>
<!-- Delete confirmation -->
<div id="modal-teacher-delete" class="fixed inset-0 z-50 hidden" aria-hidden="true">
<div class="fixed inset-0 bg-black/50" id="modal-teacher-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 Guru</h2>
<p id="modal-teacher-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-teacher-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-teacher-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 apiTeachers = baseUrl + '/api/academic/teachers';
var apiUsers = baseUrl + '/api/users';
var apiSubjects = baseUrl + '/api/academic/subjects';
var apiTeacherSubjects = baseUrl + '/api/academic/teacher-subjects';
var loading = document.getElementById('teachers-loading');
var errorEl = document.getElementById('teachers-error');
var content = document.getElementById('teachers-content');
var tbody = document.getElementById('teachers-tbody');
var btnAdd = document.getElementById('btn-add-teacher');
var modal = document.getElementById('modal-teacher');
var modalTitle = document.getElementById('modal-teacher-title');
var form = document.getElementById('form-teacher');
var formId = document.getElementById('form-teacher-id');
var formName = document.getElementById('form-teacher-name');
var formEmail = document.getElementById('form-teacher-email');
var formPassword = document.getElementById('form-teacher-password');
var formPasswordReq = document.getElementById('form-teacher-password-req');
var formRoleGuru = document.getElementById('form-role-guru');
var formRoleWali = document.getElementById('form-role-wali');
var formSubmit = document.getElementById('form-teacher-submit');
var formCancel = document.getElementById('form-teacher-cancel');
var modalBackdrop = document.getElementById('modal-teacher-backdrop');
var modalDelete = document.getElementById('modal-teacher-delete');
var modalDeleteMsg = document.getElementById('modal-teacher-delete-message');
var modalDeleteConfirm = document.getElementById('modal-teacher-delete-confirm');
var modalDeleteCancel = document.getElementById('modal-teacher-delete-cancel');
var modalDeleteBackdrop = document.getElementById('modal-teacher-delete-backdrop');
var toastContainer = document.getElementById('toast-container');
var deleteTargetId = null;
var modalSubjects = document.getElementById('modal-subjects');
var modalSubjectsBackdrop = document.getElementById('modal-subjects-backdrop');
var modalSubjectsTitle = document.getElementById('modal-subjects-title');
var modalSubjectsTeacher = document.getElementById('modal-subjects-teacher');
var modalSubjectsBody = document.getElementById('modal-subjects-body');
var modalSubjectsSave = document.getElementById('modal-subjects-save');
var modalSubjectsCancel = document.getElementById('modal-subjects-cancel');
var currentSubjectTeacherId = null;
var cachedSubjects = 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 roleLabel(roles) {
if (!roles || !roles.length) return '';
return roles.map(function(r) { return r.role_name || r.role_code; }).join(', ');
}
function loadTeachers() {
showLoading();
fetch(apiTeachers, 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 list = (r.data && r.data.data) ? r.data.data : (Array.isArray(r.data) ? r.data : []);
tbody.innerHTML = '';
list.forEach(function(row) {
var roleCodes = (row.roles || []).map(function(r) { return r.role_code; }).join(',');
var tr = document.createElement('tr');
tr.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/30';
tr.innerHTML =
'<td class="px-6 py-3 font-medium">' + escapeHtml(row.name) + '</td>' +
'<td class="px-6 py-3 text-gray-600 dark:text-gray-400">' + escapeHtml(row.email) + '</td>' +
'<td class="px-6 py-3 text-gray-600 dark:text-gray-400">' + escapeHtml(roleLabel(row.roles)) + '</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-email="' + escapeHtml(row.email) + '" data-roles="' + escapeHtml(roleCodes) + '">Edit</button> ' +
'<button type="button" class="btn-subjects px-3 py-1.5 rounded-lg text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/20" data-id="' + row.id + '" data-name="' + escapeHtml(row.name) + '">Mapel Diajar</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');
formName.value = btn.getAttribute('data-name') || '';
formEmail.value = btn.getAttribute('data-email') || '';
formPassword.value = '';
formPassword.placeholder = 'Kosongkan jika tidak diubah';
formPasswordReq.style.display = 'none';
var codes = (btn.getAttribute('data-roles') || '').split(',').filter(function(c) { return c; });
formRoleGuru.checked = codes.indexOf('GURU_MAPEL') !== -1;
formRoleWali.checked = codes.indexOf('WALI_KELAS') !== -1;
modalTitle.textContent = 'Edit Guru';
modal.classList.remove('hidden');
});
});
tbody.querySelectorAll('.btn-subjects').forEach(function(btn) {
btn.addEventListener('click', function() {
var tid = parseInt(btn.getAttribute('data-id'), 10);
var tname = btn.getAttribute('data-name') || '';
openSubjectsModal(tid, tname);
});
});
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');
});
});
showContent();
})
.catch(function() { showError('Gagal memuat data'); });
}
btnAdd.addEventListener('click', function() {
formId.value = ''; formName.value = ''; formEmail.value = ''; formPassword.value = '';
formPassword.placeholder = ''; formPasswordReq.style.display = 'inline';
formRoleGuru.checked = true;
formRoleWali.checked = false;
modalTitle.textContent = 'Tambah Guru';
modal.classList.remove('hidden');
});
form.addEventListener('submit', function(e) {
e.preventDefault();
var id = formId.value ? parseInt(formId.value, 10) : null;
var roles = [];
if (formRoleGuru.checked) roles.push('GURU_MAPEL');
if (formRoleWali.checked) roles.push('WALI_KELAS');
if (!roles.length) {
showToast('Minimal pilih satu role', 'error');
return;
}
var payload = {
name: formName.value.trim(),
email: formEmail.value.trim(),
roles: roles
};
if (!id) {
payload.password = formPassword.value;
if (!payload.password) { showToast('Password wajib untuk user baru', 'error'); return; }
} else if (formPassword.value) {
payload.password = formPassword.value;
}
formSubmit.disabled = true;
var url = apiUsers;
var opts = postOpts(payload);
if (id) { url = apiUsers + '/' + 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');
loadTeachers();
})
.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(apiUsers + '/' + 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;
loadTeachers();
})
.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; });
function loadSubjects(callback) {
if (cachedSubjects) {
if (callback) callback(cachedSubjects);
return;
}
fetch(apiSubjects, fetchOpts)
.then(function(res) { return res.json().then(function(j) { return { ok: res.ok, data: j }; }); })
.then(function(r) {
if (!r.ok) { showToast(r.data && r.data.message ? r.data.message : 'Gagal memuat mapel', 'error'); return; }
var list = (r.data && r.data.data) ? r.data.data : (Array.isArray(r.data) ? r.data : []);
cachedSubjects = list;
if (callback) callback(list);
})
.catch(function() { showToast('Gagal memuat mapel', 'error'); });
}
function openSubjectsModal(teacherId, teacherName) {
currentSubjectTeacherId = teacherId;
modalSubjectsTeacher.textContent = 'Guru: ' + teacherName;
modalSubjectsBody.innerHTML = 'Memuat...';
modalSubjects.classList.remove('hidden');
loadSubjects(function(subjects) {
fetch(apiTeacherSubjects + '/' + teacherId, fetchOpts)
.then(function(res) { return res.json().then(function(j) { return { ok: res.ok, data: j }; }); })
.then(function(r) {
if (!r.ok) {
modalSubjectsBody.textContent = (r.data && r.data.message) ? r.data.message : 'Gagal memuat data';
return;
}
var d = r.data && r.data.data ? r.data.data : r.data;
var selectedIds = (d && d.subject_ids) ? d.subject_ids : [];
var selectedSet = {};
selectedIds.forEach(function(id) { selectedSet[id] = true; });
var html = '';
subjects.forEach(function(s) {
var checked = selectedSet[s.id] ? ' checked' : '';
html += '<label class="flex items-center gap-2">' +
'<input type="checkbox" class="subject-check rounded border-gray-300 text-primary focus:ring-primary" value="' + s.id + '"' + checked + '>' +
'<span>' + escapeHtml(s.name) + '</span>' +
'</label>';
});
modalSubjectsBody.innerHTML = html || '<p class="text-sm text-gray-500">Belum ada mata pelajaran.</p>';
})
.catch(function() {
modalSubjectsBody.textContent = 'Gagal memuat data';
});
});
}
modalSubjectsCancel.addEventListener('click', function() {
modalSubjects.classList.add('hidden');
currentSubjectTeacherId = null;
});
modalSubjectsBackdrop.addEventListener('click', function() {
modalSubjects.classList.add('hidden');
currentSubjectTeacherId = null;
});
modalSubjectsSave.addEventListener('click', function() {
if (!currentSubjectTeacherId) return;
var checks = modalSubjectsBody.querySelectorAll('.subject-check');
var ids = [];
checks.forEach(function(ch) {
if (ch.checked) ids.push(parseInt(ch.value, 10));
});
fetch(apiTeacherSubjects + '/' + currentSubjectTeacherId, putOpts({ subject_ids: ids }))
.then(function(res) { return res.json().then(function(j) { return { ok: res.ok, data: j }; }); })
.then(function(r) {
if (!r.ok) {
showToast(r.data && r.data.message ? r.data.message : 'Gagal menyimpan mapel', 'error');
return;
}
showToast('Mapel guru diperbarui');
modalSubjects.classList.add('hidden');
currentSubjectTeacherId = null;
})
.catch(function() {
showToast('Gagal menyimpan mapel', 'error');
});
});
loadTeachers();
})();
</script>