326 lines
15 KiB
JavaScript
326 lines
15 KiB
JavaScript
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';
|
|
|
|
Swal.fire({
|
|
title: 'Hapus data?',
|
|
text: `Yakin ingin menghapus ${entity}? Tindakan ini tidak bisa dibatalkan.`,
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonColor: '#e11d48',
|
|
cancelButtonColor: '#475569',
|
|
confirmButtonText: 'Ya, hapus',
|
|
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]');
|
|
|
|
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 = `
|
|
<div class="absolute inset-x-0 top-0 h-12 rounded-t-[20px] bg-gradient-to-r from-indigo-100 via-sky-50 to-white"></div>
|
|
<div class="relative flex items-start gap-3">
|
|
${photoUrl
|
|
? `<div class="relative shrink-0">
|
|
<div class="absolute inset-0 rounded-2xl bg-gradient-to-br from-white/70 to-transparent"></div>
|
|
<img src="${photoUrl}" alt="${studentName}" class="h-20 w-20 rounded-2xl object-cover shadow-[0_10px_24px_-14px_rgba(15,23,42,0.6)] ring-1 ring-slate-200 bg-slate-100">
|
|
</div>`
|
|
: `<div class="flex h-20 w-20 shrink-0 flex-col items-center justify-center rounded-2xl bg-gradient-to-br from-slate-100 via-white to-slate-200 text-center text-slate-500 shadow-[0_10px_24px_-14px_rgba(15,23,42,0.35)] ring-1 ring-slate-200">
|
|
<div class="flex h-9 w-9 items-center justify-center rounded-full bg-white text-sm font-bold text-slate-700 shadow-sm">${studentInitial}</div>
|
|
<div class="mt-1 text-[9px] font-semibold uppercase tracking-[0.08em] text-slate-400">No Photo</div>
|
|
</div>`}
|
|
<div class="min-w-0 flex-1 pt-0.5">
|
|
<div class="text-sm font-semibold leading-tight text-slate-800 line-clamp-2">${studentName}</div>
|
|
<div class="mt-1 inline-flex rounded-full bg-slate-100/80 px-2 py-0.5 text-[9px] font-semibold uppercase tracking-[0.14em] text-slate-500">NIS · ${studentNis}</div>
|
|
<div class="mt-2 h-px w-full bg-gradient-to-r from-slate-200 via-slate-100 to-transparent"></div>
|
|
<div class="mt-2 text-[11px] font-medium leading-snug text-slate-600">${studentClass}</div>
|
|
<div class="mt-0.5 text-[11px] leading-snug text-slate-400">${studentDepartment}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
};
|
|
|
|
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 bulkDeleteStudentsForm = document.querySelector('#bulk-delete-students-form');
|
|
|
|
if (bulkDeleteStudentsForm) {
|
|
const selectAllCheckbox = document.querySelector('#select-all-students');
|
|
const rowCheckboxes = bulkDeleteStudentsForm.querySelectorAll('.student-row-checkbox');
|
|
const bulkDeleteButton = document.querySelector('#bulk-delete-students-button');
|
|
|
|
const syncBulkDeleteState = () => {
|
|
const checkedCount = Array.from(rowCheckboxes).filter((checkbox) => checkbox.checked).length;
|
|
|
|
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 && checkedCount === rowCheckboxes.length;
|
|
selectAllCheckbox.indeterminate = checkedCount > 0 && checkedCount < rowCheckboxes.length;
|
|
}
|
|
};
|
|
|
|
selectAllCheckbox?.addEventListener('change', () => {
|
|
rowCheckboxes.forEach((checkbox) => {
|
|
checkbox.checked = selectAllCheckbox.checked;
|
|
});
|
|
syncBulkDeleteState();
|
|
});
|
|
|
|
rowCheckboxes.forEach((checkbox) => {
|
|
checkbox.addEventListener('change', syncBulkDeleteState);
|
|
});
|
|
|
|
bulkDeleteStudentsForm.addEventListener('submit', (event) => {
|
|
const checkedCount = Array.from(rowCheckboxes).filter((checkbox) => checkbox.checked).length;
|
|
|
|
if (checkedCount === 0) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
Swal.fire({
|
|
title: 'Hapus banyak data siswa?',
|
|
text: `${checkedCount} data siswa yang dipilih akan dihapus. Tindakan ini tidak bisa dibatalkan.`,
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonColor: '#e11d48',
|
|
cancelButtonColor: '#475569',
|
|
confirmButtonText: 'Ya, hapus semua',
|
|
cancelButtonText: 'Batal',
|
|
reverseButtons: true,
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
bulkDeleteStudentsForm.submit();
|
|
}
|
|
});
|
|
});
|
|
|
|
syncBulkDeleteState();
|
|
}
|
|
|
|
const promotionForm = document.querySelector('[data-student-promotion-form]');
|
|
|
|
if (promotionForm) {
|
|
const rows = promotionForm.querySelectorAll('tbody tr');
|
|
const renderTargetOptions = (targetSelect, actionValue) => {
|
|
if (!targetSelect) {
|
|
return 0;
|
|
}
|
|
|
|
const optionKey = actionValue === 'repeat' ? 'optionsRepeat' : 'optionsPromote';
|
|
const selectedKey = actionValue === 'repeat' ? 'selectedRepeat' : 'selectedPromote';
|
|
const options = JSON.parse(targetSelect.dataset[optionKey] || '[]');
|
|
const selectedValue = targetSelect.dataset[selectedKey] || '';
|
|
|
|
targetSelect.innerHTML = '<option value="">Pilih kelas tujuan</option>';
|
|
|
|
options.forEach((option) => {
|
|
const optionElement = document.createElement('option');
|
|
optionElement.value = option.id;
|
|
optionElement.textContent = 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');
|
|
};
|
|
|
|
rows.forEach((row) => {
|
|
const actionSelect = row.querySelector('.promotion-action-select');
|
|
if (!actionSelect) {
|
|
return;
|
|
}
|
|
|
|
applyRowActionState(row, actionSelect.value);
|
|
actionSelect.addEventListener('change', () => applyRowActionState(row, actionSelect.value));
|
|
});
|
|
|
|
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 === 'repeat') {
|
|
actionSelect.value = 'repeat';
|
|
} else if (actionValue === 'skip') {
|
|
actionSelect.value = 'skip';
|
|
}
|
|
|
|
applyRowActionState(row, actionSelect.value);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
});
|