Average Grade
${avgGrade ? avgGrade.toFixed(1) + '%' : 'N/A'}
`;
// Table and Chart
outputHTML += `
`;
outputHTML += `
${renderTable(data, columns)}
`;
outputHTML += `
`;
outputHTML += `
`;
reportOutput.innerHTML = outputHTML;
renderGradeDistributionChart(data);
}
function renderTable(data, columns) {
if (data.length === 0) return '
No data matches your selected filters.
';
let table = '
';
table += '';
columns.forEach(col => {
table += `| ${col.label} | `;
});
table += '
';
data.forEach(row => {
table += '';
columns.forEach(col => {
table += `| ${row[col.id] !== null ? row[col.id] : 'N/A'} | `;
});
table += '
';
});
table += '
';
return table;
}
function renderGradeDistributionChart(data) {
const ctx = document.getElementById('gradeDistChart')?.getContext('2d');
if (!ctx) return;
if (gradeDistChart) gradeDistChart.destroy();
const gradeBins = { '90-100': 0, '80-89': 0, '70-79': 0, '60-69': 0, '<60': 0 };
data.forEach(item => {
if (item.grade === null) return;
if (item.grade >= 90) gradeBins['90-100']++;
else if (item.grade >= 80) gradeBins['80-89']++;
else if (item.grade >= 70) gradeBins['70-79']++;
else if (item.grade >= 60) gradeBins['60-69']++;
else gradeBins['<60']++;
});
gradeDistChart = new Chart(ctx, {
type: 'bar',
data: {
labels: Object.keys(gradeBins),
datasets: [{
label: 'Number of Students',
data: Object.values(gradeBins),
backgroundColor: ['#10b981', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6']
}]
},
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } } }
});
}
// --- PDF and UI Helpers ---
function addEventListeners() {
reportViewTab.addEventListener('click', () => setActiveTab(reportViewTab));
builderTab.addEventListener('click', () => setActiveTab(builderTab));
generateReportBtn.addEventListener('click', handleGenerateReport);
downloadPdfBtn.addEventListener('click', handlePdfDownload);
document.getElementById('alert-close-btn').addEventListener('click', hideAlert);
document.getElementById('courses-data').addEventListener('keyup', updateCourseFilter);
}
function setActiveTab(tabElement) {
[reportViewTab, builderTab].forEach(t => t.classList.replace('tab-active', 'tab-inactive'));
[reportViewContent, builderContent].forEach(c => c.classList.add('hidden'));
tabElement.classList.replace('tab-inactive', 'tab-active');
document.getElementById(tabElement.getAttribute('aria-controls')).classList.remove('hidden');
}
function showAlert(title, message) {
const modal = document.getElementById('alert-modal');
document.getElementById('alert-title').textContent = title;
document.getElementById('alert-message').textContent = message;
modal.classList.remove('hidden');
modal.classList.add('flex');
}
function hideAlert() {
document.getElementById('alert-modal').classList.add('hidden');
}
async function handlePdfDownload() {
const { jsPDF } = window.jspdf;
const pdfContent = document.getElementById('pdf-content');
if (!pdfContent) {
showAlert('Error', 'Could not find content to download.');
return;
}
const originalButtonText = downloadPdfBtn.textContent;
downloadPdfBtn.textContent = 'Generating...';
downloadPdfBtn.disabled = true;
try {
const canvas = await html2canvas(pdfContent, { scale: 2, useCORS: true, logging: false, backgroundColor: '#ffffff' });
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF({ orientation: 'portrait', unit: 'pt', format: 'a4' });
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdf.internal.pageSize.getHeight();
const margin = 40;
const imgProps = pdf.getImageProperties(imgData);
const ratio = imgProps.width / imgProps.height;
let finalWidth = pdfWidth - margin * 2;
let finalHeight = finalWidth / ratio;
if (finalHeight > pdfHeight - margin * 2) {
finalHeight = pdfHeight - margin * 2;
finalWidth = finalHeight * ratio;
}
const x = (pdfWidth - finalWidth) / 2;
const y = margin;
pdf.addImage(imgData, 'PNG', x, y, finalWidth, finalHeight);
const reportTitle = document.getElementById('report-title-input').value || 'report';
pdf.save(`${reportTitle.replace(/ /g, '_')}.pdf`);
} catch (error) {
console.error('Error generating PDF:', error);
showAlert('PDF Error', 'An error occurred while generating the PDF.');
} finally {
downloadPdfBtn.textContent = originalButtonText;
downloadPdfBtn.disabled = false;
}
}
// --- Run Initialization ---
initialize();
});