import './bootstrap'; import Alpine from 'alpinejs'; import Swal from 'sweetalert2'; window.Alpine = Alpine; window.Swal = Swal; Alpine.start(); document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.delete-form').forEach((form) => { form.addEventListener('submit', (event) => { event.preventDefault(); const entity = form.dataset.entity || 'data ini'; const title = form.dataset.confirmTitle || 'Hapus data?'; const text = form.dataset.confirmText || `Yakin ingin menghapus ${entity}? Tindakan ini tidak bisa dibatalkan.`; const confirmButtonColor = form.dataset.confirmButtonColor || '#e11d48'; const confirmButtonText = form.dataset.confirmButtonText || 'Ya, hapus'; Swal.fire({ title, text, icon: 'warning', showCancelButton: true, confirmButtonColor, cancelButtonColor: '#475569', confirmButtonText, cancelButtonText: 'Batal', reverseButtons: true, }).then((result) => { if (result.isConfirmed) { form.submit(); } }); }); }); 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 = `
Preview Gambar
Preview
`; 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'; passwordInput.setAttribute('type', isPassword ? 'text' : 'password'); passwordToggle.setAttribute('aria-label', isPassword ? 'Sembunyikan password' : 'Tampilkan password'); }); } const documentTypeSelect = document.querySelector('[data-document-type-select]'); const maxFileSizeInfo = document.querySelector('[data-max-file-size-info]'); const allowedExtensionsInfo = document.querySelector('[data-allowed-extensions-info]'); if (documentTypeSelect && maxFileSizeInfo && allowedExtensionsInfo) { const updateDocumentTypeInfo = () => { const selectedOption = documentTypeSelect.options[documentTypeSelect.selectedIndex]; const maxSizeKb = Number(selectedOption?.dataset?.maxSizeKb || 5120); const maxSizeMb = (maxSizeKb / 1024).toFixed(2); const extensions = (selectedOption?.dataset?.extensions || 'jpg,jpeg,png,pdf,webp') .split(',') .map((item) => item.trim().toUpperCase()) .join(', '); maxFileSizeInfo.textContent = `Maksimal ukuran file untuk jenis dokumen ini: ${maxSizeMb} MB`; allowedExtensionsInfo.textContent = `Format diizinkan: ${extensions}.`; }; documentTypeSelect.addEventListener('change', updateDocumentTypeInfo); updateDocumentTypeInfo(); } const studentHoverTriggers = document.querySelectorAll('.student-hover-trigger'); if (studentHoverTriggers.length > 0) { const hoverCard = document.createElement('div'); hoverCard.className = 'pointer-events-none fixed z-[9999] hidden w-64 overflow-hidden rounded-[20px] border border-slate-200 bg-white p-2.5 shadow-[0_20px_45px_-20px_rgba(15,23,42,0.35)] transition-opacity duration-150'; document.body.appendChild(hoverCard); const renderHoverCard = (trigger) => { const photoUrl = trigger.dataset.photoUrl; const studentName = trigger.dataset.studentName || '-'; const studentNis = trigger.dataset.studentNis || '-'; const studentClass = trigger.dataset.studentClass || '-'; const studentDepartment = trigger.dataset.studentDepartment || '-'; const studentInitial = studentName.charAt(0).toUpperCase(); hoverCard.innerHTML = `
${photoUrl ? `
${studentName}
` : `
${studentInitial}
No Photo
`}
${studentName}
NIS ยท ${studentNis}
${studentClass}
${studentDepartment}
`; }; const positionHoverCard = (trigger) => { const rect = trigger.getBoundingClientRect(); const cardWidth = 256; const cardHeight = 120; let left = rect.left; let top = rect.bottom + 12; if (left + cardWidth > window.innerWidth - 16) { left = window.innerWidth - cardWidth - 16; } if (top + cardHeight > window.innerHeight - 16) { top = rect.top - cardHeight - 12; } if (top < 16) { top = 16; } hoverCard.style.left = `${Math.max(left, 16)}px`; hoverCard.style.top = `${top}px`; }; const showHoverCard = (trigger) => { renderHoverCard(trigger); positionHoverCard(trigger); hoverCard.classList.remove('hidden'); hoverCard.style.opacity = '1'; }; const hideHoverCard = () => { hoverCard.style.opacity = '0'; hoverCard.classList.add('hidden'); }; studentHoverTriggers.forEach((trigger) => { trigger.addEventListener('mouseenter', () => showHoverCard(trigger)); trigger.addEventListener('mouseleave', hideHoverCard); trigger.addEventListener('focus', () => showHoverCard(trigger)); trigger.addEventListener('blur', hideHoverCard); }); window.addEventListener('scroll', hideHoverCard, true); window.addEventListener('resize', hideHoverCard); } const setupBulkDelete = ({ formSelector, selectAllSelector, rowCheckboxSelector, buttonSelector, title, textBuilder, confirmText }) => { const form = document.querySelector(formSelector); 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; bulkDeleteButton.classList.toggle('opacity-50', checkedCount === 0); bulkDeleteButton.classList.toggle('cursor-not-allowed', checkedCount === 0); } if (selectAllCheckbox) { selectAllCheckbox.checked = checkedCount > 0 && enabledCheckboxes.length > 0 && checkedCount === enabledCheckboxes.length; selectAllCheckbox.indeterminate = checkedCount > 0 && checkedCount < enabledCheckboxes.length; } }; selectAllCheckbox?.addEventListener('change', () => { rowCheckboxes.forEach((checkbox) => { if (!checkbox.disabled) { checkbox.checked = selectAllCheckbox.checked; } }); syncBulkDeleteState(); }); rowCheckboxes.forEach((checkbox) => { checkbox.addEventListener('change', syncBulkDeleteState); }); form.addEventListener('submit', (event) => { const checkedCount = Array.from(rowCheckboxes).filter((checkbox) => checkbox.checked).length; if (checkedCount === 0) { event.preventDefault(); return; } event.preventDefault(); Swal.fire({ title, text: textBuilder(checkedCount), icon: 'warning', showCancelButton: true, confirmButtonColor: '#e11d48', cancelButtonColor: '#475569', confirmButtonText: confirmText, cancelButtonText: 'Batal', reverseButtons: true, }).then((result) => { if (result.isConfirmed) { 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 bulkUploadInput = document.querySelector('[data-bulk-upload-input]'); const bulkUploadDropzone = document.querySelector('[data-bulk-upload-dropzone]'); const bulkUploadDropzoneInner = document.querySelector('[data-bulk-upload-dropzone-inner]'); const bulkUploadSummary = document.querySelector('[data-bulk-upload-summary]'); const bulkUploadFileList = document.querySelector('[data-bulk-upload-file-list]'); if (bulkUploadInput && bulkUploadDropzone && bulkUploadDropzoneInner) { const renderBulkUploadFiles = () => { const files = Array.from(bulkUploadInput.files || []); if (!bulkUploadSummary || !bulkUploadFileList) { return; } if (files.length === 0) { bulkUploadSummary.classList.add('hidden'); bulkUploadFileList.classList.add('hidden'); bulkUploadFileList.innerHTML = ''; return; } const totalBytes = files.reduce((sum, file) => sum + (file.size || 0), 0); const totalMb = (totalBytes / (1024 * 1024)).toFixed(2); bulkUploadSummary.classList.remove('hidden'); bulkUploadSummary.innerHTML = `
${files.length} file siap dipreview
Total ukuran sementara ${totalMb} MB. Batas upload bulk saat ini maksimal 4 MB per file.
`; bulkUploadFileList.classList.remove('hidden'); bulkUploadFileList.innerHTML = `
File terpilih
Menampilkan ${Math.min(files.length, 8)} dari ${files.length} file
${files.slice(0, 8).map((file) => `
${file.name}
${(file.size / 1024 / 1024).toFixed(2)} MB
${(file.name.split('.').pop() || 'FILE').toUpperCase()}
`).join('')} ${files.length > 8 ? `
+${files.length - 8} file lainnya
` : ''}
`; }; ['dragenter', 'dragover'].forEach((eventName) => { bulkUploadDropzone.addEventListener(eventName, (event) => { event.preventDefault(); bulkUploadDropzone.classList.add('border-indigo-400', 'bg-indigo-50/40'); bulkUploadDropzoneInner.classList.add('border-indigo-300', 'bg-indigo-50/50'); }); }); ['dragleave', 'drop'].forEach((eventName) => { bulkUploadDropzone.addEventListener(eventName, (event) => { event.preventDefault(); bulkUploadDropzone.classList.remove('border-indigo-400', 'bg-indigo-50/40'); bulkUploadDropzoneInner.classList.remove('border-indigo-300', 'bg-indigo-50/50'); }); }); bulkUploadDropzone.addEventListener('drop', (event) => { const files = event.dataTransfer?.files; if (files && files.length > 0) { bulkUploadInput.files = files; renderBulkUploadFiles(); } }); bulkUploadInput.addEventListener('change', renderBulkUploadFiles); 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) { const rows = promotionForm.querySelectorAll('tbody tr'); const counterElements = { promote: document.querySelector('[data-promotion-counter="promote"]'), repeat: document.querySelector('[data-promotion-counter="repeat"]'), graduate: document.querySelector('[data-promotion-counter="graduate"]'), skip: document.querySelector('[data-promotion-counter="skip"]'), }; const liveSummary = document.querySelector('[data-promotion-live-summary]'); const capacitySummary = document.querySelector('[data-promotion-capacity-summary]'); const finalPreview = document.querySelector('[data-promotion-final-preview]'); const submitButton = document.querySelector('[data-promotion-submit-button]'); const toolbar = document.querySelector('[data-promotion-toolbar]'); const getOptionsForAction = (targetSelect, actionValue) => { const optionKey = actionValue === 'repeat' ? 'optionsRepeat' : 'optionsPromote'; return JSON.parse(targetSelect?.dataset?.[optionKey] || '[]'); }; const calculateCounts = () => { const counts = { promote: 0, repeat: 0, graduate: 0, skip: 0 }; rows.forEach((row) => { const actionSelect = row.querySelector('.promotion-action-select'); const actionValue = actionSelect?.value || 'skip'; if (counts[actionValue] !== undefined) { counts[actionValue] += 1; } }); return counts; }; const updateCounters = () => { const counts = calculateCounts(); Object.entries(counterElements).forEach(([key, element]) => { if (element) { element.textContent = counts[key]; } }); if (liveSummary) { liveSummary.innerHTML = `Ringkasan live: ${counts.promote} naik, ${counts.repeat} tinggal kelas, ${counts.graduate} lulus, dan ${counts.skip} dilewati.`; } }; const updateFinalPreview = () => { if (!finalPreview) { return; } const counts = calculateCounts(); const totalProcessed = counts.promote + counts.repeat + counts.graduate; finalPreview.classList.remove('hidden'); finalPreview.innerHTML = `
Preview final sebelum submit
Batch ini akan memproses ${totalProcessed} siswa.
`; }; const updateCapacitySummary = () => { if (!capacitySummary) { return; } const plannedByClass = new Map(); rows.forEach((row) => { const actionSelect = row.querySelector('.promotion-action-select'); const targetSelect = row.querySelector('.promotion-target-select'); const actionValue = actionSelect?.value || 'skip'; if (!targetSelect || !['promote', 'repeat'].includes(actionValue) || !targetSelect.value) { return; } const options = getOptionsForAction(targetSelect, actionValue); const selectedOption = options.find((option) => String(option.id) === String(targetSelect.value)); if (!selectedOption) { return; } const entry = plannedByClass.get(String(selectedOption.id)) || { name: selectedOption.name, planned: 0, remaining: selectedOption.remaining_capacity, capacityLabel: selectedOption.capacity_label, }; entry.planned += 1; plannedByClass.set(String(selectedOption.id), entry); }); const conflicts = Array.from(plannedByClass.values()).filter((entry) => entry.remaining !== null && entry.planned > entry.remaining); const warnings = Array.from(plannedByClass.values()).filter((entry) => entry.remaining !== null && entry.planned <= entry.remaining && entry.remaining - entry.planned <= 1); capacitySummary.classList.remove('hidden', 'border-rose-200', 'bg-rose-50', 'text-rose-700', 'border-amber-200', 'bg-amber-50', 'text-amber-700', 'border-emerald-200', 'bg-emerald-50', 'text-emerald-700'); if (conflicts.length > 0) { capacitySummary.classList.add('border-rose-200', 'bg-rose-50', 'text-rose-700'); capacitySummary.innerHTML = `
Konflik kapasitas terdeteksi sebelum submit.
`; if (submitButton) { submitButton.disabled = true; submitButton.classList.add('opacity-50', 'cursor-not-allowed'); } return; } if (warnings.length > 0) { capacitySummary.classList.add('border-amber-200', 'bg-amber-50', 'text-amber-700'); capacitySummary.innerHTML = `
Perhatian: alokasi mendekati batas kuota.
`; if (submitButton) { submitButton.disabled = false; submitButton.classList.remove('opacity-50', 'cursor-not-allowed'); } return; } capacitySummary.classList.add('border-emerald-200', 'bg-emerald-50', 'text-emerald-700'); capacitySummary.innerHTML = '
Cek kapasitas aman.
Belum ada konflik kuota pada alokasi kelas tujuan saat ini.
'; if (submitButton) { submitButton.disabled = false; submitButton.classList.remove('opacity-50', 'cursor-not-allowed'); } }; const updateCapacityWarning = (row, actionValue) => { const targetSelect = row.querySelector('.promotion-target-select'); const warningBox = row.querySelector('[data-promotion-capacity-warning]'); if (!targetSelect || !warningBox || (actionValue !== 'promote' && actionValue !== 'repeat')) { warningBox?.classList.add('hidden'); warningBox?.classList.remove('border-rose-200', 'bg-rose-50', 'text-rose-700', 'border-amber-200', 'bg-amber-50', 'text-amber-700', 'border-emerald-200', 'bg-emerald-50', 'text-emerald-700'); return; } const options = getOptionsForAction(targetSelect, actionValue); const selectedOption = options.find((option) => String(option.id) === String(targetSelect.value)); if (!selectedOption) { warningBox.classList.add('hidden'); warningBox.classList.remove('border-rose-200', 'bg-rose-50', 'text-rose-700', 'border-amber-200', 'bg-amber-50', 'text-amber-700', 'border-emerald-200', 'bg-emerald-50', 'text-emerald-700'); return; } warningBox.classList.remove('hidden', 'border-rose-200', 'bg-rose-50', 'text-rose-700', 'border-amber-200', 'bg-amber-50', 'text-amber-700', 'border-emerald-200', 'bg-emerald-50', 'text-emerald-700'); if (selectedOption.warning_level === 'full') { warningBox.classList.add('border-rose-200', 'bg-rose-50', 'text-rose-700'); warningBox.textContent = `Peringatan: ${selectedOption.name} sudah penuh. ${selectedOption.capacity_label}.`; } else if (selectedOption.warning_level === 'low') { warningBox.classList.add('border-amber-200', 'bg-amber-50', 'text-amber-700'); warningBox.textContent = `Perhatian: kuota ${selectedOption.name} menipis. ${selectedOption.capacity_label}.`; } else { warningBox.classList.add('border-emerald-200', 'bg-emerald-50', 'text-emerald-700'); warningBox.textContent = `Kelas tujuan tersedia. ${selectedOption.capacity_label}.`; } }; const renderTargetOptions = (targetSelect, actionValue) => { if (!targetSelect) { return 0; } const selectedKey = actionValue === 'repeat' ? 'selectedRepeat' : 'selectedPromote'; const options = getOptionsForAction(targetSelect, actionValue); const selectedValue = targetSelect.dataset[selectedKey] || ''; targetSelect.innerHTML = ''; options.forEach((option) => { const optionElement = document.createElement('option'); optionElement.value = option.id; optionElement.textContent = option.option_label || option.name; if (String(option.id) === String(selectedValue)) { optionElement.selected = true; } targetSelect.appendChild(optionElement); }); return options.length; }; const applyRowActionState = (row, actionValue) => { const targetSelect = row.querySelector('.promotion-target-select'); const targetHelp = row.querySelector('.promotion-target-help'); const graduatePlaceholder = row.querySelector('.promotion-graduate-placeholder'); const hiddenTarget = row.querySelector('[data-promotion-target-hidden]'); if (targetSelect) { const requiresTarget = actionValue === 'promote' || actionValue === 'repeat'; const optionCount = requiresTarget ? renderTargetOptions(targetSelect, actionValue) : 0; targetSelect.disabled = !requiresTarget; targetSelect.classList.toggle('bg-slate-100', !requiresTarget); targetSelect.classList.toggle('text-slate-400', !requiresTarget); targetSelect.classList.toggle('hidden', !requiresTarget); if (!requiresTarget) { targetSelect.value = ''; } if (targetHelp) { if (actionValue === 'skip') { targetHelp.textContent = 'Siswa ini akan dilewati saat proses promosi.'; } else if (actionValue === 'graduate') { targetHelp.textContent = 'Aksi lulus tidak memerlukan kelas tujuan.'; } else if (optionCount === 0) { targetHelp.textContent = actionValue === 'repeat' ? 'Belum ada kelas aktif dengan level yang sama pada tahun ajaran target.' : 'Belum ada kelas aktif level berikutnya pada tahun ajaran target.'; } else { targetHelp.textContent = actionValue === 'repeat' ? 'Pilih kelas tujuan dengan level yang sama untuk skenario tinggal kelas.' : 'Pilih kelas tujuan level berikutnya untuk skenario naik kelas.'; } } } if (graduatePlaceholder) { graduatePlaceholder.classList.toggle('hidden', actionValue !== 'graduate'); graduatePlaceholder.classList.toggle('opacity-60', actionValue === 'skip'); } if (hiddenTarget && (actionValue === 'graduate' || actionValue === 'skip')) { hiddenTarget.value = ''; } row.classList.toggle('opacity-60', actionValue === 'skip'); updateCapacityWarning(row, actionValue); updateCounters(); updateCapacitySummary(); updateFinalPreview(); }; rows.forEach((row) => { const actionSelect = row.querySelector('.promotion-action-select'); const targetSelect = row.querySelector('.promotion-target-select'); if (!actionSelect) { return; } applyRowActionState(row, actionSelect.value); actionSelect.addEventListener('change', () => applyRowActionState(row, actionSelect.value)); targetSelect?.addEventListener('change', () => { updateCapacityWarning(row, actionSelect.value); updateCapacitySummary(); }); }); promotionForm.querySelectorAll('[data-promotion-bulk-action]').forEach((button) => { button.addEventListener('click', () => { const actionValue = button.dataset.promotionBulkAction; rows.forEach((row) => { const actionSelect = row.querySelector('.promotion-action-select'); if (!actionSelect) { return; } if (actionValue === 'promote' && actionSelect.querySelector('option[value="promote"]')) { actionSelect.value = 'promote'; } else if (actionValue === 'graduate' && actionSelect.querySelector('option[value="graduate"]')) { actionSelect.value = 'graduate'; } else if (actionValue === 'repeat') { actionSelect.value = 'repeat'; } else if (actionValue === 'skip') { actionSelect.value = 'skip'; } applyRowActionState(row, actionSelect.value); }); }); }); if (toolbar?.dataset?.classLevel === 'XII') { const graduateButton = toolbar.querySelector('[data-promotion-bulk-action="graduate"]'); graduateButton?.classList.add('ring-2', 'ring-sky-200'); } updateCounters(); updateCapacitySummary(); updateFinalPreview(); } });