Update graduation workflow, promotions, and admin module improvements

This commit is contained in:
2026-05-02 13:09:26 +07:00
parent 700c24209d
commit 96d6e364a6
16 changed files with 1593 additions and 32 deletions
+237
View File
@@ -445,6 +445,243 @@ document.addEventListener('DOMContentLoaded', () => {
renderBulkUploadFiles();
}
const bulkUploadOverlay = document.querySelector('[data-bulk-upload-overlay]');
document.querySelectorAll('[data-bulk-upload-form]').forEach((form) => {
const submitButton = form.querySelector('[data-bulk-upload-submit-button]');
const submitLabel = form.querySelector('[data-bulk-upload-submit-label]');
const submitIcon = form.querySelector('[data-bulk-upload-submit-icon]');
const mode = form.dataset.bulkUploadMode || 'preview';
const useAjaxPreview = form.dataset.bulkUploadAjaxPreview === 'true';
const previewUrl = form.dataset.bulkUploadPreviewUrl || form.getAttribute('action') || '';
const showOverlay = (config = {}) => {
if (!bulkUploadOverlay) {
return;
}
const title = bulkUploadOverlay.querySelector('[data-bulk-upload-overlay-title]');
const message = bulkUploadOverlay.querySelector('[data-bulk-upload-overlay-message]');
const fileCountLabel = bulkUploadOverlay.querySelector('[data-bulk-upload-overlay-file-count]');
const stageUpload = bulkUploadOverlay.querySelector('[data-bulk-upload-stage-upload]');
const stageMatch = bulkUploadOverlay.querySelector('[data-bulk-upload-stage-match]');
const stageSave = bulkUploadOverlay.querySelector('[data-bulk-upload-stage-save]');
const progressLabel = bulkUploadOverlay.querySelector('[data-bulk-upload-overlay-progress-label]');
const progressPercent = bulkUploadOverlay.querySelector('[data-bulk-upload-overlay-progress-percent]');
const progressBar = bulkUploadOverlay.querySelector('[data-bulk-upload-overlay-progress-bar]');
const fileCountSource = form.dataset.bulkUploadFileCountSource || 'input';
const inputFileCount = bulkUploadInput?.files?.length || 0;
const previewFileCount = Number(form.dataset.bulkUploadPreviewCount || 0);
const activeFileCount = fileCountSource === 'preview' ? previewFileCount : inputFileCount;
const percent = Math.max(0, Math.min(100, Number(config.percent ?? 0)));
if (title) {
title.textContent = config.title || (mode === 'process'
? 'Sedang memproses upload permanen...'
: 'Sedang menyiapkan preview dokumen...');
}
if (message) {
message.textContent = config.message || (mode === 'process'
? 'Mohon tunggu. Sistem sedang menyimpan dokumen ke arsip kelulusan dan mencocokkannya ke data siswa. Jangan tutup tab ini agar proses tidak terputus.'
: 'Mohon tunggu. File sedang diunggah dan dipetakan oleh sistem untuk menyiapkan preview. Jangan tutup tab ini agar proses tidak terputus.');
}
if (fileCountLabel) {
fileCountLabel.textContent = activeFileCount > 0
? `${activeFileCount} file sedang diproses`
: 'Sedang menghitung jumlah file...';
}
if (stageUpload) {
stageUpload.textContent = config.stageUpload || (mode === 'process' ? 'Validasi file upload' : 'Upload file');
stageUpload.parentElement?.classList.add('ring-1', 'ring-indigo-300/50', 'bg-indigo-500/10');
}
if (stageMatch) {
stageMatch.textContent = config.stageMatch || (mode === 'process' ? 'Cocokkan ke siswa lulus' : 'Susun preview & matching');
stageMatch.parentElement?.classList.add('ring-1', 'ring-sky-300/40', 'bg-sky-500/10');
}
if (stageSave) {
stageSave.textContent = config.stageSave || (mode === 'process' ? 'Simpan ke arsip kelulusan' : 'Tunggu konfirmasi operator');
stageSave.parentElement?.classList.add('ring-1', 'ring-emerald-300/40', 'bg-emerald-500/10');
}
if (progressLabel) {
progressLabel.textContent = config.progressLabel || 'Menunggu proses dimulai...';
}
if (progressPercent) {
progressPercent.textContent = `${Math.round(percent)}%`;
}
if (progressBar) {
progressBar.style.width = `${percent}%`;
progressBar.classList.toggle('animate-pulse', !!config.indeterminate);
}
bulkUploadOverlay.classList.remove('hidden');
bulkUploadOverlay.classList.add('flex');
document.body.classList.add('overflow-hidden');
};
form.addEventListener('submit', async (event) => {
if (submitButton) {
submitButton.disabled = true;
}
if (submitIcon) {
submitIcon.classList.remove('fa-magnifying-glass', 'fa-upload', 'fa-cloud-arrow-up');
submitIcon.classList.add('fa-spinner', 'fa-spin');
}
if (submitLabel) {
submitLabel.textContent = mode === 'process'
? 'Sedang memproses upload...'
: 'Sedang menyiapkan preview...';
}
if (mode === 'preview' && useAjaxPreview) {
event.preventDefault();
const formData = new FormData(form);
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
showOverlay({
title: 'Sedang mengunggah file preview...',
message: 'Mohon tunggu. Browser sedang mengirim file ke server untuk disiapkan preview dan pencocokan data.',
progressLabel: 'Mengunggah file ke server...',
percent: 0,
stageUpload: 'Upload file',
stageMatch: 'Menunggu upload selesai',
stageSave: 'Siapkan halaman preview',
indeterminate: false,
});
const xhr = new XMLHttpRequest();
xhr.open('POST', previewUrl, true);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader('Accept', 'application/json');
if (csrfToken) {
xhr.setRequestHeader('X-CSRF-TOKEN', csrfToken);
}
xhr.upload.addEventListener('progress', (progressEvent) => {
if (!progressEvent.lengthComputable) {
return;
}
const percent = (progressEvent.loaded / progressEvent.total) * 100;
showOverlay({
title: 'Sedang mengunggah file preview...',
message: 'Mohon tunggu. Browser sedang mengirim file ke server untuk disiapkan preview dan pencocokan data.',
progressLabel: `Upload file ${Math.round(percent)}%`,
percent,
stageUpload: 'Upload file',
stageMatch: 'Menunggu upload selesai',
stageSave: 'Siapkan halaman preview',
indeterminate: false,
});
});
xhr.onreadystatechange = () => {
if (xhr.readyState !== XMLHttpRequest.DONE) {
return;
}
if (xhr.status >= 200 && xhr.status < 300) {
let response = {};
try {
response = JSON.parse(xhr.responseText || '{}');
} catch (error) {
response = {};
}
showOverlay({
title: 'Upload selesai, sistem sedang menyusun preview...',
message: 'File sudah terkirim. Sekarang sistem sedang mencocokkan dokumen dan menyiapkan halaman preview.',
progressLabel: 'Upload selesai, menyiapkan preview...',
percent: 100,
stageUpload: 'Upload selesai',
stageMatch: 'Mencocokkan ke siswa lulus',
stageSave: 'Membuka halaman preview',
indeterminate: true,
});
window.location.href = response.redirect_url || window.location.href;
return;
}
document.body.classList.remove('overflow-hidden');
bulkUploadOverlay?.classList.add('hidden');
bulkUploadOverlay?.classList.remove('flex');
if (submitButton) {
submitButton.disabled = false;
}
if (submitIcon) {
submitIcon.classList.remove('fa-spinner', 'fa-spin');
submitIcon.classList.add('fa-magnifying-glass');
}
if (submitLabel) {
submitLabel.textContent = 'Preview Mapping Dokumen';
}
let errorMessage = 'Bulk upload gagal diproses. Silakan cek file lalu coba lagi.';
try {
const response = JSON.parse(xhr.responseText || '{}');
if (response.message) {
errorMessage = response.message;
} else if (response.errors) {
const firstError = Object.values(response.errors).flat()[0];
if (firstError) {
errorMessage = firstError;
}
}
} catch (error) {
// noop
}
Swal.fire({
icon: 'error',
title: 'Bulk upload gagal',
text: errorMessage,
confirmButtonColor: '#dc2626',
});
};
xhr.send(formData);
return;
}
showOverlay({
progressLabel: mode === 'process' ? 'Menyimpan dokumen ke arsip...' : 'Menyiapkan proses...',
percent: mode === 'process' ? 100 : 15,
indeterminate: mode === 'process',
});
});
});
const graduationFilterForm = document.querySelector('[data-graduation-filter-form]');
const graduationAcademicYearFilter = document.querySelector('[data-graduation-academic-year-filter]');
if (graduationFilterForm && graduationAcademicYearFilter) {
graduationAcademicYearFilter.addEventListener('change', () => {
const classFilter = graduationFilterForm.querySelector('[name="class_id"]');
if (classFilter) {
classFilter.value = '';
}
graduationFilterForm.submit();
});
}
const promotionForm = document.querySelector('[data-student-promotion-form]');
if (promotionForm) {