`;
} else {
html += ``;
}
html += `
`;
html += `Funds lasted for the ${data.inputs.planningHorizon}-year planning horizon.
`; html += `Final Total Portfolio Value: ${formatCurrency(data.summary.finalPortfolioValue)}
Year-by-Year Projection:
`; if(data.projectionTable.length > 0) { html += ``;
html += `
`;
} else {
html += `| Yr | Age | Spending Need | Taxable W/D | Tax-Def W/D | Tax-Free W/D | Taxes Paid | Taxable End | Tax-Def End | Tax-Free End | Total End Bal |
|---|---|---|---|---|---|---|---|---|---|---|
| ${row.year} | ${row.age} | ${formatCurrency(row.spendingNeed,0,0)} | ${formatCurrency(row.taxableWithdrawal,0,0)} | ${formatCurrency(row.taxDeferredWithdrawal,0,0)} | ${formatCurrency(row.taxFreeWithdrawal,0,0)} | ${formatCurrency(row.taxesPaidThisYear,0,0)} | ${formatCurrency(row.taxableEnd,0,0)} | ${formatCurrency(row.taxDeferredEnd,0,0)} | ${formatCurrency(row.taxFreeEnd,0,0)} | ${formatCurrency(row.totalPortfolioEnd,0,0)} ${row.notes ? `(${row.notes})` : ''} |
No projection data to display.
`; } html += ``; optimizationResultsDiv.innerHTML = html; } if (optimizeBtn) { optimizeBtn.addEventListener('click', runOptimizationProjection); } // --- PDF Download --- function loadJsPdfIfNeeded(callback) { if (jsPdfLoaded) { if (callback) callback(); return; } const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js'; script.onload = () => { jsPdfLoaded = true; console.log("jsPDF loaded dynamically."); if (callback) callback(); }; script.onerror = () => { console.error("Failed to load jsPDF."); alert("Error: Could not load PDF library."); }; document.head.appendChild(script); } function downloadReportAsPdf() { if (!jsPdfLoaded) { alert("PDF library not loaded."); return; } if (!optimizationDataForPdf) { alert("No projection data to download. Please run the projection first."); return; } const { jsPDF } = window.jspdf; const doc = new jsPDF({ unit: 'pt', format: 'a4', orientation: 'landscape' }); // Landscape for wider table const data = optimizationDataForPdf; const pageMargin = 30; // Adjusted for landscape const pageWidth = doc.internal.pageSize.getWidth() - 2 * pageMargin; let y = pageMargin; function addMainTitle(text) { doc.setFontSize(16); doc.setFont(undefined, 'bold'); doc.setTextColor(13, 71, 161); // Primary doc.text(text, doc.internal.pageSize.getWidth() / 2, y, { align: 'center' }); y += 30; } function addSectionTitle(text) { if (y > doc.internal.pageSize.getHeight() - 60) { doc.addPage(); y = pageMargin; } doc.setFontSize(12); doc.setFont(undefined, 'bold'); doc.setTextColor(25, 118, 210); // Secondary doc.text(text, pageMargin, y); y += 20; } function addLine(key, value, keyColor = [33,33,33], valueColor = [33,33,33]) { if (y > doc.internal.pageSize.getHeight() - 30) { doc.addPage(); y = pageMargin; } doc.setFontSize(9); doc.setFont(undefined, 'bold'); doc.setTextColor(keyColor[0], keyColor[1], keyColor[2]); doc.text(key, pageMargin, y); doc.setFont(undefined, 'normal'); doc.setTextColor(valueColor[0], valueColor[1], valueColor[2]); const valueText = String(value); doc.text(valueText, pageMargin + 220, y, { align: 'left', maxWidth: pageWidth - 220 - 5 }); y += 16; } function addInfo(text) { if (y > doc.internal.pageSize.getHeight() - 40) { doc.addPage(); y = pageMargin; } doc.setFontSize(8); doc.setFont(undefined, 'italic'); doc.setTextColor(100,100,100); const splitText = doc.splitTextToSize(text, pageWidth); doc.setFillColor(245,245,245); doc.rect(pageMargin -5, y - (doc.getTextDimensions(splitText).h / 2) - 2 , pageWidth + 10, doc.getTextDimensions(splitText).h + 8, 'F'); doc.text(splitText, pageMargin, y); y += (doc.getTextDimensions(splitText).h) + 10; } function addTable(headers, tableData, columnWidths) { if (y > doc.internal.pageSize.getHeight() - 40) { doc.addPage(); y = pageMargin; } // Check for table header space doc.setFontSize(6.5); // Even smaller font for landscape table const headerFillColor = [25, 118, 210]; const headerTextColor = [255,255,255]; const rowTextColor = [33,33,33]; doc.setFillColor(headerFillColor[0], headerFillColor[1], headerFillColor[2]); doc.setTextColor(headerTextColor[0], headerTextColor[1], headerTextColor[2]); doc.setFont(undefined, 'bold'); let currentX = pageMargin; headers.forEach((header, i) => { doc.rect(currentX, y, columnWidths[i], 16, 'F'); // Shorter header doc.text(header, currentX + 2, y + 11); // Adjust padding currentX += columnWidths[i]; }); y += 16; doc.setTextColor(rowTextColor[0], rowTextColor[1], rowTextColor[2]); doc.setFont(undefined, 'normal'); tableData.forEach((rowArray) => { if (y > doc.internal.pageSize.getHeight() - 25) { doc.addPage(); y = pageMargin; currentX = pageMargin; doc.setFillColor(headerFillColor[0], headerFillColor[1], headerFillColor[2]); doc.setTextColor(headerTextColor[0], headerTextColor[1], headerTextColor[2]); doc.setFont(undefined, 'bold'); headers.forEach((header, i) => { doc.rect(currentX, y, columnWidths[i], 16, 'F'); doc.text(header, currentX + 2, y + 11); currentX += columnWidths[i]; }); y += 16; doc.setTextColor(rowTextColor[0], rowTextColor[1], rowTextColor[2]); doc.setFont(undefined, 'normal'); } currentX = pageMargin; rowArray.forEach((cell, i) => { doc.rect(currentX, y, columnWidths[i], 14); // Shorter row const cellText = String(cell); if (cellText.toLowerCase().includes("depleted")) { doc.setTextColor(211, 47, 47); // Error color } else { doc.setTextColor(rowTextColor[0], rowTextColor[1], rowTextColor[2]); } const textLines = doc.splitTextToSize(cellText, columnWidths[i] - 4); doc.text(textLines, currentX + 2, y + 10); currentX += columnWidths[i]; }); doc.setTextColor(rowTextColor[0], rowTextColor[1], rowTextColor[2]); y += 14; }); y += 7; } addMainTitle("Tax-Efficient Spending Strategy Report"); addInfo(`Report Generated: ${new Date().toLocaleString()}`); y += 5; addSectionTitle("Input Parameters"); addLine("Retirement Age:", `${data.inputs.retirementAge} years`); addLine("Planning Horizon:", `${data.inputs.planningHorizon} years`); addLine("Initial Annual Spending:", formatCurrency(data.inputs.initialAnnualSpending)); addLine("Initial Taxable Account Balance:", formatCurrency(data.inputs.initialTaxableBalance)); addLine("Taxable Account Cost Basis:", formatCurrency(data.inputs.initialTaxableCostBasis)); addLine("Initial Tax-Deferred Account Balance:", formatCurrency(data.inputs.initialTaxDeferredBalance)); addLine("Initial Tax-Free Account Balance:", formatCurrency(data.inputs.initialTaxFreeBalance)); y+=5; addLine("Avg. Annual Investment Return:", `${data.inputs.investmentReturnRatePct.toFixed(1)}%`); addLine("Avg. Annual Inflation Rate:", `${data.inputs.inflationRatePct.toFixed(1)}%`); addLine("Ordinary Income Tax Rate (Retirement):", `${data.inputs.ordinaryIncomeTaxRatePct.toFixed(0)}%`); addLine("Long-Term Capital Gains Tax Rate:", `${data.inputs.capitalGainsTaxRatePct.toFixed(0)}%`); addLine("Withdrawal Order Strategy:", data.inputs.withdrawalOrder.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())); y += 10; addSectionTitle("Projection Summary"); addLine("Total Projected Taxes Paid:", formatCurrency(data.summary.totalTaxesPaid), [239,68,68],[239,68,68]); // Red for taxes if (data.summary.fundsDepletedYear !== null && data.summary.fundsDepletedYear <= data.inputs.planningHorizon) { const ageAtDepletion = data.inputs.retirementAge + data.summary.fundsDepletedYear -1; addLine("Funds Longevity:", `Depleted in Year ${data.summary.fundsDepletedYear} of retirement (Age ${ageAtDepletion})`,[239,68,68],[239,68,68]); } else { addLine("Funds Longevity:", `Lasted for ${data.inputs.planningHorizon} years`,[66,187,106],[66,187,106]); // Green addLine("Final Total Portfolio Value:", formatCurrency(data.summary.finalPortfolioValue)); } y += 10; if (data.projectionTable.length > 0) { addSectionTitle("Year-by-Year Spending & Tax Projection"); const tableH = ["Yr", "Age", "Spending", "Taxable W/D", "Tax-Def W/D", "Tax-Free W/D", "Taxes Paid", "Taxable End", "Tax-Def End", "Tax-Free End", "Total End"]; const tableCW = [25, 25, 55, 55, 55, 55, 55, 60, 60, 60, 65]; // Adjusted for landscape const tableFData = data.projectionTable.map(r => [ r.year, r.age, formatCurrency(r.spendingNeed,0,0), formatCurrency(r.taxableWithdrawal,0,0), formatCurrency(r.taxDeferredWithdrawal,0,0), formatCurrency(r.taxFreeWithdrawal,0,0), formatCurrency(r.taxesPaidThisYear,0,0), formatCurrency(r.taxableEnd,0,0), formatCurrency(r.taxDeferredEnd,0,0), formatCurrency(r.taxFreeEnd,0,0), formatCurrency(r.totalPortfolioEnd,0,0) + (r.notes ? ` (${r.notes.substring(0,8)})` : '') ]); addTable(tableH, tableFData, tableCW); } addInfo("This projection is illustrative and uses average rates. Actual market conditions, tax laws, and personal circumstances will vary. This tool does not account for Required Minimum Distributions (RMDs) or complex tax scenarios (e.g., state taxes, detailed tax brackets). Consult with qualified financial and tax professionals for personalized advice."); const pageCount = doc.internal.getNumberOfPages(); for (let i = 1; i <= pageCount; i++) { doc.setPage(i); doc.setFontSize(7); doc.setTextColor(150); doc.text(`Page ${i} of ${pageCount} - Tax-Efficient Spending Optimizer`, pageMargin, doc.internal.pageSize.getHeight() - 15); } doc.save('Tax_Efficient_Spending_Strategy.pdf'); } if (downloadPdfBtn) { downloadPdfBtn.addEventListener('click', () => loadJsPdfIfNeeded(downloadReportAsPdf)); } // --- Initialization --- showTab(0); if (downloadPdfBtn) downloadPdfBtn.disabled = true; if (pdfButtonContainer) pdfButtonContainer.style.display = 'block'; loadJsPdfIfNeeded(); });