1. Budget vs. Actual Summary
| Category |
Allocated ($) |
Actual Spent ($) |
Variance ($) |
`;
categories.forEach(cat => {
const actual = totals.categoryTotals[cat.name] || 0;
const variance = cat.allocated - actual;
const varianceStyle = variance < 0 ? 'color: var(--danger-color);' : (variance > 0 ? 'color: var(--success-color);' : 'color: var(--secondary-color);');
html += `
| ${cat.name} |
${formatCurrency(cat.allocated)} |
${formatCurrency(actual)} |
${formatCurrency(variance)} |
`;
});
// Grand Totals Row
const overallVarianceStyle = totals.grandTotalVariance < 0 ? 'color: var(--danger-color);' : '';
html += `
| GRAND TOTALS |
${formatCurrency(totals.grandTotalAllocated)} |
${formatCurrency(totals.grandTotalActual)} |
${formatCurrency(totals.grandTotalVariance)} |
Total Budget Limit: ${formatCurrency(meta.budgetLimit)} | Remaining Available: ${formatCurrency(meta.budgetLimit - totals.grandTotalActual)}
`;
reviewArea.innerHTML = html;
// Manually build the detailed expense table for the review area
const expenseTableContainer = document.getElementById('review-expense-table-container');
let expenseTableHTML = `
| Date |
Category |
Item Description |
Cost ($) |
`;
expenses.forEach(expense => {
expenseTableHTML += `
| ${expense.date} |
${expense.category} |
${expense.item} |
${formatCurrency(expense.cost)} |
`;
});
expenseTableHTML += `
`;
expenseTableContainer.innerHTML = expenseTableHTML;
}
/**
* PDF Generation Function (Ensuring professional formatting)
*/
function downloadPDF() {
const meta = getMetaFormData();
const totals = calculateTotals();
const { jsPDF } = window.jspdf;
const doc = new jsPDF('p', 'pt', 'a4');
let currentY = 40;
const margin = 40;
const pageWidth = doc.internal.pageSize.width;
const maxWidth = pageWidth - (margin * 2);
const checkPageBreak = (spaceNeeded) => {
if (currentY + spaceNeeded > doc.internal.pageSize.height - margin) {
doc.addPage();
currentY = margin;
}
};
const addSectionHeader = (title) => {
checkPageBreak(30);
doc.setFontSize(16);
doc.setFont('Helvetica', 'bold');
doc.setTextColor(33, 150, 243); /* Info color */
doc.text(title, margin, currentY);
currentY += 10;
doc.setLineWidth(0.5);
doc.setDrawColor(200);
doc.line(margin, currentY, pageWidth - margin, currentY);
currentY += 15;
doc.setTextColor(0);
};
// --- PDF Content ---
// Title Block
doc.setFontSize(22);
doc.setFont('Helvetica', 'bold');
doc.setTextColor(76, 175, 80); /* Primary color */
doc.text("Science Fair Budget Report", pageWidth / 2, currentY, { align: 'center' });
currentY += 15;
doc.setFontSize(12);
doc.setFont('Helvetica', 'normal');
doc.setTextColor(108, 117, 125);
doc.text(`Project: ${meta.projectName} | Student: ${meta.studentName}`, pageWidth / 2, currentY, { align: 'center' });
currentY += 15;
doc.text(`Competition: ${meta.schoolName}`, pageWidth / 2, currentY, { align: 'center' });
currentY += 30;
doc.setTextColor(0);
// 1. Budget Summary Table
addSectionHeader("1. Budget vs. Actual Summary");
const summaryHead = [["Category", "Allocated ($)", "Actual Spent ($)", "Variance ($)"]];
const summaryBody = [];
categories.forEach(cat => {
const actual = totals.categoryTotals[cat.name] || 0;
const variance = cat.allocated - actual;
summaryBody.push([
cat.name,
formatCurrency(cat.allocated),
formatCurrency(actual),
formatCurrency(variance)
]);
});
doc.autoTable({
startY: currentY,
head: summaryHead,
body: summaryBody,
foot: [[
"GRAND TOTALS",
formatCurrency(totals.grandTotalAllocated),
formatCurrency(totals.grandTotalActual),
formatCurrency(totals.grandTotalVariance)
]],
theme: 'grid',
headStyles: { fillColor: [121, 85, 72], textColor: [255, 255, 255] }, /* Secondary color */
footStyles: { fillColor: [232, 245, 233], textColor: [0], fontStyle: 'bold' },
styles: { fontSize: 10, cellPadding: 5, font: 'Helvetica' },
columnStyles: { 0: { fontStyle: 'bold' }, 1: { halign: 'right' }, 2: { halign: 'right' }, 3: { halign: 'right' } }
});
currentY = doc.autoTable.previous.finalY + 10;
// Budget Limit summary text
const remaining = meta.budgetLimit - totals.grandTotalActual;
doc.setFontSize(10);
doc.setFont('Helvetica', 'bold');
doc.text(`Total Budget Limit: ${formatCurrency(meta.budgetLimit)}`, margin, currentY);
doc.text(`Remaining Available: ${formatCurrency(remaining)}`, margin + 200, currentY);
currentY += 20;
// 2. Detailed Expense Log Table
addSectionHeader("2. Detailed Expense Log");
const expenseHead = [["Date", "Category", "Item Description", "Cost ($)"]];
const expenseBody = expenses.map(expense => [
expense.date,
expense.category,
expense.item,
formatCurrency(expense.cost)
]);
doc.autoTable({
startY: currentY,
head: expenseHead,
body: expenseBody,
theme: 'grid',
headStyles: { fillColor: [76, 175, 80], textColor: [255, 255, 255] }, /* Primary color */
styles: { fontSize: 9, cellPadding: 4, font: 'Helvetica' },
columnStyles: { 3: { halign: 'right', fontStyle: 'bold' } }
});
currentY = doc.autoTable.previous.finalY + 10;
doc.save('science_fair_budget_report.pdf');
}
function getMetaFormData() {
return {
projectName: document.getElementById('project-name').value,
studentName: document.getElementById('student-name').value,
schoolName: document.getElementById('school-name').value,
budgetLimit: parseFloat(document.getElementById('budget-limit').value) || 0
};
}
// --- Tab Navigation ---
function switchTab(tabIndex) {
tabs.forEach((tab, index) => {
tab.classList.toggle('active', index === tabIndex);
contents[index].classList.toggle('active', index === tabIndex);
});
currentTab = tabIndex;
updateNavButtons();
if (tabIndex === 2) { // Review tab
generateReviewReport();
}
}
function updateNavButtons() {
prevBtn.disabled = currentTab === 0;
nextBtn.disabled = currentTab === tabs.length - 1;
}
tabs.forEach((tab, index) => {
tab.addEventListener('click', () => switchTab(index));
});
nextBtn.addEventListener('click', () => { if (currentTab < tabs.length - 1) switchTab(currentTab + 1); });
prevBtn.addEventListener('click', () => { if (currentTab > 0) switchTab(currentTab - 1); });
// --- Event Listeners ---
document.getElementById('budget-meta-form').addEventListener('input', renderBudgetTable);
document.getElementById('expense-cost').addEventListener('input', () => {
document.getElementById('expense-cost').value = (parseFloat(document.getElementById('expense-cost').value) || 0).toFixed(2);
});
document.getElementById('budget-limit').addEventListener('input', () => {
document.getElementById('budget-limit').value = (parseFloat(document.getElementById('budget-limit').value) || 0).toFixed(2);
});
document.getElementById('expense-add-update-btn').addEventListener('click', (e) => { e.preventDefault(); expenseForm.dispatchEvent(new Event('submit')); });
document.getElementById('pdf-download-btn').addEventListener('click', downloadPDF);
// --- Initial Setup ---
renderBudgetTable();
populateExpenseCategorySelect();
renderExpenseTable();
updateNavButtons();
// Set default date for expense input
document.getElementById('expense-date').value = new Date().toISOString().split('T')[0];
});