Add student achievements module and improve student admin workflows
This commit is contained in:
+166
-14
@@ -36,6 +36,131 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const passwordInput = document.querySelector('[data-password-input]');
|
||||
const passwordToggle = document.querySelector('[data-password-toggle]');
|
||||
|
||||
document.querySelectorAll('[data-searchable-student-select]').forEach((wrapper) => {
|
||||
const input = wrapper.querySelector('[data-student-search-input]');
|
||||
const hiddenInput = wrapper.querySelector('[data-student-search-value]');
|
||||
const toggle = wrapper.querySelector('[data-student-search-toggle]');
|
||||
const dropdown = wrapper.querySelector('[data-student-search-dropdown]');
|
||||
const options = Array.from(wrapper.querySelectorAll('[data-student-option]'));
|
||||
const emptyState = wrapper.querySelector('[data-student-empty-state]');
|
||||
|
||||
if (!input || !hiddenInput || !toggle || !dropdown || options.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const closeDropdown = () => {
|
||||
dropdown.classList.add('hidden');
|
||||
};
|
||||
|
||||
const openDropdown = () => {
|
||||
dropdown.classList.remove('hidden');
|
||||
};
|
||||
|
||||
const filterOptions = () => {
|
||||
const keyword = input.value.trim().toLowerCase();
|
||||
let visibleCount = 0;
|
||||
|
||||
options.forEach((option) => {
|
||||
const haystack = option.dataset.search || '';
|
||||
const match = keyword === '' || haystack.includes(keyword);
|
||||
option.classList.toggle('hidden', !match);
|
||||
if (match) visibleCount += 1;
|
||||
});
|
||||
|
||||
if (emptyState) {
|
||||
emptyState.classList.toggle('hidden', visibleCount > 0);
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('focus', () => {
|
||||
filterOptions();
|
||||
openDropdown();
|
||||
});
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
hiddenInput.value = '';
|
||||
filterOptions();
|
||||
openDropdown();
|
||||
});
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
filterOptions();
|
||||
dropdown.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
options.forEach((option) => {
|
||||
option.addEventListener('click', () => {
|
||||
hiddenInput.value = option.dataset.value || '';
|
||||
input.value = option.dataset.label || '';
|
||||
closeDropdown();
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
if (!wrapper.contains(event.target)) {
|
||||
closeDropdown();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const lightboxTriggers = document.querySelectorAll('[data-lightbox-trigger]');
|
||||
|
||||
if (lightboxTriggers.length > 0) {
|
||||
const lightbox = document.createElement('div');
|
||||
lightbox.className = 'fixed inset-0 z-[10000] hidden items-center justify-center bg-slate-950/85 px-4 py-6';
|
||||
lightbox.innerHTML = `
|
||||
<div class="absolute inset-0" data-lightbox-close></div>
|
||||
<div class="relative z-10 flex max-h-full w-full max-w-5xl flex-col items-center justify-center gap-3">
|
||||
<div class="flex w-full items-center justify-between gap-4 rounded-2xl bg-slate-900/80 px-4 py-3 text-white shadow-2xl backdrop-blur">
|
||||
<div class="truncate text-sm font-semibold" data-lightbox-title>Preview Gambar</div>
|
||||
<button type="button" class="rounded-xl border border-white/15 px-3 py-2 text-xs font-semibold text-white transition hover:bg-white/10" data-lightbox-close>Tutup</button>
|
||||
</div>
|
||||
<div class="flex max-h-[calc(100vh-110px)] w-full items-center justify-center overflow-auto rounded-2xl bg-slate-900/60 p-3 shadow-2xl backdrop-blur">
|
||||
<img src="" alt="Preview" class="max-h-[calc(100vh-140px)] w-auto max-w-full rounded-xl object-contain shadow-2xl" data-lightbox-image>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(lightbox);
|
||||
|
||||
const lightboxImage = lightbox.querySelector('[data-lightbox-image]');
|
||||
const lightboxTitle = lightbox.querySelector('[data-lightbox-title]');
|
||||
const closeLightbox = () => {
|
||||
lightbox.classList.add('hidden');
|
||||
lightbox.classList.remove('flex');
|
||||
document.body.classList.remove('overflow-hidden');
|
||||
if (lightboxImage) {
|
||||
lightboxImage.setAttribute('src', '');
|
||||
}
|
||||
};
|
||||
|
||||
lightbox.querySelectorAll('[data-lightbox-close]').forEach((element) => {
|
||||
element.addEventListener('click', closeLightbox);
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeLightbox();
|
||||
}
|
||||
});
|
||||
|
||||
lightboxTriggers.forEach((trigger) => {
|
||||
trigger.addEventListener('click', () => {
|
||||
const src = trigger.dataset.lightboxSrc || '';
|
||||
const title = trigger.dataset.lightboxTitle || 'Preview Gambar';
|
||||
|
||||
if (!src || !lightboxImage || !lightboxTitle) {
|
||||
return;
|
||||
}
|
||||
|
||||
lightboxImage.setAttribute('src', src);
|
||||
lightboxTitle.textContent = title;
|
||||
lightbox.classList.remove('hidden');
|
||||
lightbox.classList.add('flex');
|
||||
document.body.classList.add('overflow-hidden');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (passwordInput && passwordToggle) {
|
||||
passwordToggle.addEventListener('click', () => {
|
||||
const isPassword = passwordInput.getAttribute('type') === 'password';
|
||||
@@ -150,15 +275,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
window.addEventListener('resize', hideHoverCard);
|
||||
}
|
||||
|
||||
const bulkDeleteStudentsForm = document.querySelector('#bulk-delete-students-form');
|
||||
const setupBulkDelete = ({ formSelector, selectAllSelector, rowCheckboxSelector, buttonSelector, title, textBuilder, confirmText }) => {
|
||||
const form = document.querySelector(formSelector);
|
||||
|
||||
if (bulkDeleteStudentsForm) {
|
||||
const selectAllCheckbox = document.querySelector('#select-all-students');
|
||||
const rowCheckboxes = bulkDeleteStudentsForm.querySelectorAll('.student-row-checkbox');
|
||||
const bulkDeleteButton = document.querySelector('#bulk-delete-students-button');
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectAllCheckbox = document.querySelector(selectAllSelector);
|
||||
const rowCheckboxes = form.querySelectorAll(rowCheckboxSelector);
|
||||
const bulkDeleteButton = document.querySelector(buttonSelector);
|
||||
|
||||
const syncBulkDeleteState = () => {
|
||||
const checkedCount = Array.from(rowCheckboxes).filter((checkbox) => checkbox.checked).length;
|
||||
const enabledCheckboxes = Array.from(rowCheckboxes).filter((checkbox) => !checkbox.disabled);
|
||||
|
||||
if (bulkDeleteButton) {
|
||||
bulkDeleteButton.disabled = checkedCount === 0;
|
||||
@@ -167,14 +297,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
if (selectAllCheckbox) {
|
||||
selectAllCheckbox.checked = checkedCount > 0 && checkedCount === rowCheckboxes.length;
|
||||
selectAllCheckbox.indeterminate = checkedCount > 0 && checkedCount < rowCheckboxes.length;
|
||||
selectAllCheckbox.checked = checkedCount > 0 && enabledCheckboxes.length > 0 && checkedCount === enabledCheckboxes.length;
|
||||
selectAllCheckbox.indeterminate = checkedCount > 0 && checkedCount < enabledCheckboxes.length;
|
||||
}
|
||||
};
|
||||
|
||||
selectAllCheckbox?.addEventListener('change', () => {
|
||||
rowCheckboxes.forEach((checkbox) => {
|
||||
checkbox.checked = selectAllCheckbox.checked;
|
||||
if (!checkbox.disabled) {
|
||||
checkbox.checked = selectAllCheckbox.checked;
|
||||
}
|
||||
});
|
||||
syncBulkDeleteState();
|
||||
});
|
||||
@@ -183,7 +315,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
checkbox.addEventListener('change', syncBulkDeleteState);
|
||||
});
|
||||
|
||||
bulkDeleteStudentsForm.addEventListener('submit', (event) => {
|
||||
form.addEventListener('submit', (event) => {
|
||||
const checkedCount = Array.from(rowCheckboxes).filter((checkbox) => checkbox.checked).length;
|
||||
|
||||
if (checkedCount === 0) {
|
||||
@@ -194,24 +326,44 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
event.preventDefault();
|
||||
|
||||
Swal.fire({
|
||||
title: 'Hapus banyak data siswa?',
|
||||
text: `${checkedCount} data siswa yang dipilih akan dihapus. Tindakan ini tidak bisa dibatalkan.`,
|
||||
title,
|
||||
text: textBuilder(checkedCount),
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#e11d48',
|
||||
cancelButtonColor: '#475569',
|
||||
confirmButtonText: 'Ya, hapus semua',
|
||||
confirmButtonText: confirmText,
|
||||
cancelButtonText: 'Batal',
|
||||
reverseButtons: true,
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
bulkDeleteStudentsForm.submit();
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
syncBulkDeleteState();
|
||||
}
|
||||
};
|
||||
|
||||
setupBulkDelete({
|
||||
formSelector: '#bulk-delete-students-form',
|
||||
selectAllSelector: '#select-all-students',
|
||||
rowCheckboxSelector: '.student-row-checkbox',
|
||||
buttonSelector: '#bulk-delete-students-button',
|
||||
title: 'Hapus banyak data siswa?',
|
||||
textBuilder: (checkedCount) => `${checkedCount} data siswa yang dipilih akan dihapus. Tindakan ini tidak bisa dibatalkan.`,
|
||||
confirmText: 'Ya, hapus semua',
|
||||
});
|
||||
|
||||
setupBulkDelete({
|
||||
formSelector: '#bulk-delete-classes-form',
|
||||
selectAllSelector: '#select-all-classes',
|
||||
rowCheckboxSelector: '.class-row-checkbox',
|
||||
buttonSelector: '#bulk-delete-classes-button',
|
||||
title: 'Hapus banyak kelas?',
|
||||
textBuilder: (checkedCount) => `${checkedCount} kelas yang dipilih akan dihapus. Tindakan ini tidak bisa dibatalkan.`,
|
||||
confirmText: 'Ya, hapus semua',
|
||||
});
|
||||
|
||||
const promotionForm = document.querySelector('[data-student-promotion-form]');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user