Expense Breakdown by Category
`;
// Call the dashboard renderer on the clone
bpt_renderDashboard(bpt_pdfRenderClone, true);
}
/**
* Generates and downloads a PDF of the dashboard
*/
async function bpt_downloadPDF() {
if (typeof jspdf === 'undefined' || typeof html2canvas === 'undefined') {
console.error("BPT Tool Error: jsPDF or html2canvas library not loaded.");
alert("Error: PDF libraries failed to load. Please check console.");
return;
}
// 1. Create and render the high-res clone
bpt_renderPdfClone();
const { jsPDF } = window.jspdf;
try {
// 2. Render canvas from the clone
const canvas = await html2canvas(bpt_pdfRenderClone, {
scale: 2, // High resolution
useCORS: true,
});
const imgData = canvas.toDataURL('image/png');
const imgWidth = canvas.width;
const imgHeight = canvas.height;
// Use 'pt' for units. A4 is 595.28 x 841.89
const pdf = new jsPDF({ orientation: 'p', unit: 'pt', format: 'a4' });
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdf.internal.pageSize.getHeight();
// Scale image to fit pdf width
const ratio = imgHeight / imgWidth;
const scaledImgHeight = (pdfWidth / imgWidth) * imgHeight;
let heightLeft = scaledImgHeight;
let position = 0; // y-position of the image on the page
const margin = 40; // 40pt margin
const contentWidth = pdfWidth - (margin * 2);
const contentHeight = (contentWidth * imgHeight) / imgWidth;
// 3. Add the first page
pdf.addImage(imgData, 'PNG', margin, position + margin, contentWidth, contentHeight);
heightLeft -= (pdfHeight - margin * 2);
// 4. Add subsequent pages if needed
while (heightLeft > 0) {
position -= (pdfHeight - margin * 2);
pdf.addPage();
pdf.addImage(imgData, 'PNG', margin, position + margin, contentWidth, contentHeight);
heightLeft -= (pdfHeight - margin * 2);
}
pdf.save('Budget_Report.pdf');
} catch (error) {
console.error("BPT Tool Error: PDF generation failed.", error);
alert("An error occurred while generating the PDF. Please try again.");
}
}
// --- EVENT LISTENERS ---
// Tab link clicks
bpt_tabLinks.forEach((link, index) => {
link.addEventListener('click', () => bpt_switchTab(index));
});
// Next/Prev button clicks
if (bpt_prevButton) {
bpt_prevButton.addEventListener('click', () => {
if (bpt_currentTab > 0) bpt_switchTab(bpt_currentTab - 1);
});
}
if (bpt_nextButton) {
bpt_nextButton.addEventListener('click', () => {
if (bpt_currentTab < bpt_tabLinks.length - 1) bpt_switchTab(bpt_currentTab + 1);
});
}
// PDF download
if (bpt_downloadPdfButton) {
bpt_downloadPdfButton.addEventListener('click', bpt_downloadPDF);
}
// --- Config Tab "Add" Buttons ---
if (bpt_addIncomeButton) {
bpt_addIncomeButton.addEventListener('click', () => {
const newItem = { id: bpt_incomeIdCounter++, source: '', amount: 0 };
bpt_data.income.push(newItem);
bpt_incomeContainer.appendChild(bpt_createIncomeInput(newItem));
});
}
if (bpt_addExpenseButton) {
bpt_addExpenseButton.addEventListener('click', () => {
const newItem = { id: bpt_expenseIdCounter++, item: '', category: 'Other', amount: 0 };
bpt_data.expenses.push(newItem);
bpt_expensesContainer.appendChild(bpt_createExpenseInput(newItem));
});
}
// --- Config Tab "Remove" Buttons & Input Changes (Event Delegation) ---
if (bpt_configTab) {
// Handle remove clicks
bpt_configTab.addEventListener('click', (e) => {
const removeButton = e.target.closest('.bpt-remove-item');
if (!removeButton) return;
const entryDiv = removeButton.closest('[data-id]');
const id = parseInt(entryDiv.getAttribute('data-id'), 10);
if (entryDiv.querySelector('.bpt-input-income-source')) {
bpt_data.income = bpt_data.income.filter(item => item.id !== id);
} else if (entryDiv.querySelector('.bpt-input-expense-item')) {
bpt_data.expenses = bpt_data.expenses.filter(item => item.id !== id);
}
entryDiv.remove();
// Live update dashboard if active
if (bpt_currentTab === 0) {
bpt_renderDashboard();
}
});
// Handle input changes
bpt_configTab.addEventListener('change', () => {
bpt_updateDataFromConfig();
if (bpt_currentTab === 0) {
bpt_renderDashboard();
}
});
}
// --- INITIALIZATION ---
bpt_initSampleData();
bpt_renderConfig();
bpt_renderDashboard();
// Set initial tab state
bpt_tabPanes.forEach((pane, index) => {
pane.classList.toggle('hidden', index !== 0);
pane.classList.toggle('bpt-active', index === 0);
});
});