`;
html += ``;
html += ``;
html += `
`;
}
if(data.projectionTable.length > 0) {
html += `Total ROI: ${sale.totalROI_pct.toFixed(2)}%
Annualized Total ROI: ${sale.annualizedTotalROI_pct.toFixed(2)}%
Year-by-Year Projection (Illustrative):
`; html += ``;
html += `
`;
}
html += ``;
analysisResultsDiv.innerHTML = html;
}
if (calculateBtn) {
calculateBtn.addEventListener('click', runAnalysis);
}
// --- 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 (!analysisDataForPdf) { alert("No analysis data to download. Please run the analysis first."); return; }
const { jsPDF } = window.jspdf;
const doc = new jsPDF({ unit: 'pt', format: 'a4' });
const data = analysisDataForPdf;
const inputs = data.inputs;
const calc = data.calculations;
const sale = data.totalROIAnalysis;
const pageMargin = 35;
const pageWidth = doc.internal.pageSize.getWidth() - 2 * pageMargin;
let y = pageMargin;
function addMainTitle(text) {
doc.setFontSize(16); doc.setFont(undefined, 'bold'); doc.setTextColor(44, 62, 80);
doc.text(text, doc.internal.pageSize.getWidth() / 2, y, { align: 'center' }); y += 30;
}
function addSectionTitle(text, color = [52, 152, 219]) {
if (y > doc.internal.pageSize.getHeight() - 70) { doc.addPage(); y = pageMargin; }
doc.setFontSize(12); doc.setFont(undefined, 'bold'); doc.setTextColor(color[0], color[1], color[2]);
doc.text(text, pageMargin, y); y += 20;
}
function addLine(key, value, valueColor = [52,73,94]) {
if (y > doc.internal.pageSize.getHeight() - 35) { doc.addPage(); y = pageMargin; }
doc.setFontSize(9);
doc.setFont(undefined, 'bold'); doc.setTextColor(52,73,94);
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() - 45) { doc.addPage(); y = pageMargin; }
doc.setFontSize(8); doc.setFont(undefined, 'italic');
doc.setTextColor(108, 117, 125);
const splitText = doc.splitTextToSize(text, pageWidth);
doc.setFillColor(236,240,241);
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) + 12;
}
function addTable(headers, tableData, columnWidths) {
if (y > doc.internal.pageSize.getHeight() - 50) { doc.addPage(); y = pageMargin; }
doc.setFontSize(7);
const headerFillColor = [52, 152, 219];
const headerTextColor = [255,255,255];
const rowTextColor = [52,73,94];
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], 18, 'F');
doc.text(header, currentX + 3, y + 12);
currentX += columnWidths[i];
});
y += 18;
doc.setTextColor(rowTextColor[0], rowTextColor[1], rowTextColor[2]);
doc.setFont(undefined, 'normal');
tableData.forEach((rowArray) => {
if (y > doc.internal.pageSize.getHeight() - 30) {
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], 18, 'F');
doc.text(header, currentX + 3, y + 12);
currentX += columnWidths[i];
});
y += 18;
doc.setTextColor(rowTextColor[0], rowTextColor[1], rowTextColor[2]);
doc.setFont(undefined, 'normal');
}
currentX = pageMargin;
rowArray.forEach((cell, i) => {
doc.rect(currentX, y, columnWidths[i], 16);
const cellText = String(cell);
const textLines = doc.splitTextToSize(cellText, columnWidths[i] - 6);
doc.text(textLines, currentX + 3, y + 11);
currentX += columnWidths[i];
});
y += 16;
});
y += 8;
}
addMainTitle("Rental Yield & ROI Analysis Report");
addInfo(`Report Generated: ${new Date().toLocaleString()}`);
y += 5;
addSectionTitle("Input Summary: Property & Purchase");
addLine("Purchase Price:", formatCurrency(inputs.purchasePrice));
addLine("Closing Costs:", formatCurrency(inputs.closingCosts));
addLine("Initial Renovations:", formatCurrency(inputs.initialRenovations));
y += 5;
addSectionTitle("Input Summary: Income & Operating Expenses");
addLine("Monthly Gross Rental Income:", formatCurrency(inputs.monthlyRent));
addLine("Annual Vacancy Rate:", formatPercent(inputs.vacancyRate));
addLine("Annual Property Taxes:", formatCurrency(inputs.propertyTaxesAnnual));
addLine("Annual Property Insurance:", formatCurrency(inputs.propertyInsuranceAnnual));
addLine("Annual HOA Fees:", formatCurrency(inputs.hoaFeesAnnual));
addLine("Annual Maintenance:", formatCurrency(inputs.maintenanceAnnual));
addLine("Property Management Fees:", formatPercent(inputs.propMgmtFeesPct));
addLine("Other Annual Operating Expenses:", formatCurrency(inputs.otherOpExAnnual));
y += 5;
addSectionTitle("Input Summary: Financing & Sale (If Applicable)");
addLine("Down Payment:", `${formatPercent(inputs.downPaymentPct)} (${formatCurrency(inputs.purchasePrice * inputs.downPaymentPct)})`);
addLine("Loan Interest Rate (Annual):", formatPercent(inputs.loanInterestRateAnnual));
addLine("Loan Term:", `${inputs.loanTermYears} years`);
if (inputs.holdingPeriod > 0) {
addLine("Holding Period:", `${inputs.holdingPeriod} years`);
addLine("Expected Annual Property Appreciation:", formatPercent(inputs.annualAppreciation));
addLine("Selling Costs (% of Sale Price):", formatPercent(inputs.sellingCostsPct));
}
y += 10;
addSectionTitle("Key Performance Indicators (First Year Operation)");
addLine("Total Initial Cash Outlay:", formatCurrency(calc.totalInitialCashOutlay));
addLine("Gross Annual Rental Income:", formatCurrency(calc.grossAnnualRent));
addLine("Effective Gross Income (EGI):", formatCurrency(calc.effectiveGrossIncome));
addLine("Total Annual Operating Expenses:", formatCurrency(calc.totalAnnualOperatingExpenses));
addLine("Net Operating Income (NOI):", formatCurrency(calc.noi), [44, 62, 80], [38, 166, 154]);
addLine("Gross Rental Yield:", formatPercent(calc.grossRentalYield_pct/100), [44, 62, 80], [38, 166, 154]);
addLine("Net Rental Yield (Cap Rate):", formatPercent(calc.netRentalYieldCapRate_pct/100), [44, 62, 80], [38, 166, 154]);
if (calc.loanAmount > 0) {
addLine("Annual Debt Service (Mortgage):", formatCurrency(calc.annualDebtService));
}
addLine("Annual Pre-Tax Cash Flow:", formatCurrency(calc.preTaxCashFlowAnnual), [44, 62, 80], [38, 166, 154]);
addLine("Cash-on-Cash ROI (Pre-Tax):", formatPercent(calc.cashOnCashROI_pct/100), [44, 62, 80], [142, 68, 173]);
y += 10;
if (sale) {
addSectionTitle(`Total Return Analysis (Over ${inputs.holdingPeriod} Years)`);
addLine("Projected Sale Price:", formatCurrency(sale.projectedSalePrice));
addLine("Net Cash From Sale:", formatCurrency(sale.netCashFromSale));
addLine("Total Profit:", formatCurrency(sale.totalProfit), [44, 62, 80], [142, 68, 173]);
addLine("Total ROI:", formatPercent(sale.totalROI_pct/100), [44, 62, 80], [142, 68, 173]);
addLine("Annualized Total ROI:", formatPercent(sale.annualizedTotalROI_pct/100), [44, 62, 80], [142, 68, 173]);
y += 10;
}
if (data.projectionTable.length > 0) {
addSectionTitle("Year-by-Year Projection (Illustrative)");
const tableH = ["Yr", "Prop.Val", "Loan Bal.", "Equity", "Cash Flow", "Cum. CF"];
const tableCW = [30, 80, 80, 80, 70, 80];
const tableFData = data.projectionTable.map(r => [
r.year, formatCurrency(r.propertyValue,0,0), formatCurrency(r.loanBalance,0,0),
formatCurrency(r.equity,0,0), formatCurrency(r.cashFlow,0,0),
formatCurrency(r.cumulativeCashFlow,0,0)
]);
addTable(tableH, tableFData, tableCW);
}
addInfo("This analysis is illustrative and uses average assumptions. It does not account for taxes (income or capital gains), inflation on expenses/rent beyond initial inputs, or unexpected major repairs. Consult with financial advisors 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} - Rental Yield & ROI Calculator`, pageMargin, doc.internal.pageSize.getHeight() - 15);
}
doc.save('Rental_Yield_ROI_Analysis.pdf');
}
if (downloadPdfBtn) {
downloadPdfBtn.addEventListener('click', () => loadJsPdfIfNeeded(downloadReportAsPdf));
}
// --- Initialization ---
showTab(0);
if (downloadPdfBtn) downloadPdfBtn.disabled = true;
if (pdfButtonContainer) pdfButtonContainer.style.display = 'block';
loadJsPdfIfNeeded();
});
| Year | Property Value | Loan Balance | Equity | Cash Flow | Cum. Cash Flow |
|---|---|---|---|---|---|
| ${row.year} | ${formatCurrency(row.propertyValue)} | ${formatCurrency(row.loanBalance)} | ${formatCurrency(row.equity)} | ${formatCurrency(row.cashFlow)} | ${formatCurrency(row.cumulativeCashFlow)} |