feat: add graduation archive and public document services
This commit is contained in:
+189
-3
@@ -369,14 +369,179 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
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: <span class="font-semibold">${counts.promote}</span> naik, <span class="font-semibold">${counts.repeat}</span> tinggal kelas, <span class="font-semibold">${counts.graduate}</span> lulus, dan <span class="font-semibold">${counts.skip}</span> dilewati.`;
|
||||
}
|
||||
};
|
||||
|
||||
const updateFinalPreview = () => {
|
||||
if (!finalPreview) {
|
||||
return;
|
||||
}
|
||||
|
||||
const counts = calculateCounts();
|
||||
const totalProcessed = counts.promote + counts.repeat + counts.graduate;
|
||||
|
||||
finalPreview.classList.remove('hidden');
|
||||
finalPreview.innerHTML = `
|
||||
<div class="font-semibold">Preview final sebelum submit</div>
|
||||
<div class="mt-2">Batch ini akan memproses <span class="font-semibold">${totalProcessed}</span> siswa.</div>
|
||||
<ul class="mt-2 list-disc space-y-1 pl-5">
|
||||
<li><span class="font-semibold">${counts.promote}</span> siswa naik kelas</li>
|
||||
<li><span class="font-semibold">${counts.repeat}</span> siswa tinggal kelas</li>
|
||||
<li><span class="font-semibold">${counts.graduate}</span> siswa lulus</li>
|
||||
<li><span class="font-semibold">${counts.skip}</span> siswa dilewati</li>
|
||||
</ul>
|
||||
`;
|
||||
};
|
||||
|
||||
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 = `<div class="font-semibold">Konflik kapasitas terdeteksi sebelum submit.</div><ul class="mt-2 list-disc space-y-1 pl-5">${conflicts.map((entry) => `<li>${entry.name}: alokasi ${entry.planned} siswa, sisa kuota hanya ${entry.remaining}. (${entry.capacityLabel})</li>`).join('')}</ul>`;
|
||||
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 = `<div class="font-semibold">Perhatian: alokasi mendekati batas kuota.</div><ul class="mt-2 list-disc space-y-1 pl-5">${warnings.map((entry) => `<li>${entry.name}: alokasi ${entry.planned} siswa. ${entry.capacityLabel}.</li>`).join('')}</ul>`;
|
||||
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 = '<div class="font-semibold">Cek kapasitas aman.</div><div class="mt-1">Belum ada konflik kuota pada alokasi kelas tujuan saat ini.</div>';
|
||||
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 optionKey = actionValue === 'repeat' ? 'optionsRepeat' : 'optionsPromote';
|
||||
const selectedKey = actionValue === 'repeat' ? 'selectedRepeat' : 'selectedPromote';
|
||||
const options = JSON.parse(targetSelect.dataset[optionKey] || '[]');
|
||||
const options = getOptionsForAction(targetSelect, actionValue);
|
||||
const selectedValue = targetSelect.dataset[selectedKey] || '';
|
||||
|
||||
targetSelect.innerHTML = '<option value="">Pilih kelas tujuan</option>';
|
||||
@@ -384,7 +549,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
options.forEach((option) => {
|
||||
const optionElement = document.createElement('option');
|
||||
optionElement.value = option.id;
|
||||
optionElement.textContent = option.name;
|
||||
optionElement.textContent = option.option_label || option.name;
|
||||
if (String(option.id) === String(selectedValue)) {
|
||||
optionElement.selected = true;
|
||||
}
|
||||
@@ -439,16 +604,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
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) => {
|
||||
@@ -463,6 +638,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
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') {
|
||||
@@ -473,5 +650,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user