`;
portfolioSummaryDisplay.innerHTML = html;
}
/**
* Renders a summary of upcoming payments for all loans.
*/
function renderUpcomingPayments() {
let html = '';
const today = new Date();
today.setHours(0, 0, 0, 0); // Normalize to start of day
if (portfolioData.length === 0) {
upcomingPaymentsTableBody.innerHTML = '
No upcoming payments to display. ';
return;
}
const upcomingPayments = [];
portfolioData.forEach(debt => {
// Simulate amortization to find the next payment
const schedule = calculateAmortization(debt);
if (schedule.length > 0) {
// Find the first payment date that is today or in the future
const nextPayment = schedule.find(p => p.paymentDate.getTime() >= today.getTime());
if (nextPayment) {
upcomingPayments.push({
loanName: debt.loanName,
nextPaymentDate: nextPayment.paymentDate,
paymentAmount: nextPayment.paymentAmount,
remainingPrincipal: nextPayment.startingBalance // This might be a slight oversimplification for "remaining", better to use ending balance of last payment
});
}
}
});
if (upcomingPayments.length === 0) {
html = '
No upcoming payments found in the generated schedules. ';
} else {
// Sort by payment date
upcomingPayments.sort((a, b) => a.nextPaymentDate - b.nextPaymentDate);
upcomingPayments.forEach(payment => {
html += `
${payment.loanName}
${formatDate(payment.nextPaymentDate)}
${formatCurrency(payment.paymentAmount)}
${formatCurrency(payment.remainingPrincipal)}
`;
});
}
upcomingPaymentsTableBody.innerHTML = html;
}
// --- Amortization Schedule ---
/**
* Populates the loan selection dropdown for the amortization schedule.
*/
function populateAmortizationLoanSelect() {
let html = '
-- Select a loan -- ';
portfolioData.forEach(debt => {
html += `
${debt.loanName} `;
});
amortizationLoanSelect.innerHTML = html;
}
/**
* Calculates the full amortization schedule for a given loan.
* @param {object} debt - The debt object.
* @returns {Array
} An array of payment objects, each detailing a payment.
*/
function calculateAmortization(debt) {
const schedule = [];
let balance = debt.principalAmount;
const annualInterestRate = debt.annualInterestRate / 100;
let monthlyRate;
let paymentAmount;
let totalTermMonths;
let intervalMonths;
// Adjust rate and term based on frequency
if (debt.repaymentFrequency === 'Monthly') {
monthlyRate = annualInterestRate / 12;
paymentAmount = calculateMonthlyPayment(debt.principalAmount, annualInterestRate, debt.loanTerm);
totalTermMonths = debt.loanTerm;
intervalMonths = 1;
} else if (debt.repaymentFrequency === 'Quarterly') {
monthlyRate = annualInterestRate / 12; // Still use monthly for internal calculation of balance
const quarterlyRate = annualInterestRate / 4;
const numQuarters = debt.loanTerm / 3; // Total quarters
paymentAmount = debt.principalAmount * (quarterlyRate * Math.pow(1 + quarterlyRate, numQuarters)) / (Math.pow(1 + quarterlyRate, numQuarters) - 1);
totalTermMonths = debt.loanTerm;
intervalMonths = 3;
} else if (debt.repaymentFrequency === 'Annually') {
monthlyRate = annualInterestRate / 12; // Still use monthly for internal calculation of balance
const annualRate = annualInterestRate / 1;
const numYears = debt.loanTerm / 12; // Total years
paymentAmount = debt.principalAmount * (annualRate * Math.pow(1 + annualRate, numYears)) / (Math.pow(1 + annualRate, numYears) - 1);
totalTermMonths = debt.loanTerm;
intervalMonths = 12;
}
// Handle zero interest rate scenario
if (annualInterestRate === 0) {
paymentAmount = debt.principalAmount / totalTermMonths; // Simple principal division per month
monthlyRate = 0;
}
let paymentDate = new Date(debt.originationDate);
paymentDate.setDate(paymentDate.getDate() + 1); // Start payment from next day
if (paymentDate.getDate() === new Date(debt.originationDate).getDate()) { // If origination was end of month, push to end of month
paymentDate.setMonth(paymentDate.getMonth() + 1, 0); // Last day of next month
} else {
paymentDate.setMonth(paymentDate.getMonth() + intervalMonths);
}
let paymentNumber = 0;
while (balance > 0.01 && paymentNumber < totalTermMonths + 2) { // Add a buffer to ensure it ends
paymentNumber++;
let interestPayment;
let principalPayment;
if (annualInterestRate === 0) {
interestPayment = 0;
principalPayment = Math.min(paymentAmount, balance);
if (paymentNumber > totalTermMonths) { // For zero interest, ensure it only pays for the term
principalPayment = 0; // No more payments after term
}
} else {
interestPayment = balance * (monthlyRate * intervalMonths); // Interest for the period
principalPayment = paymentAmount - interestPayment;
// Adjust final payment to clear remaining balance
if (balance - principalPayment < 0.01) { // If remaining balance is less than payment amount
principalPayment = balance;
paymentAmount = principalPayment + interestPayment;
}
}
const startingBalance = balance;
balance -= principalPayment;
balance = Math.max(0, balance); // Ensure balance doesn't go negative
schedule.push({
paymentNumber: paymentNumber,
paymentDate: new Date(paymentDate),
startingBalance: startingBalance,
paymentAmount: paymentAmount,
interestPayment: interestPayment,
principalPayment: principalPayment,
endingBalance: balance
});
// Advance payment date by the interval
paymentDate.setMonth(paymentDate.getMonth() + intervalMonths);
}
return schedule;
}
/**
* Displays the amortization schedule for the currently selected loan.
*/
window.displayAmortizationSchedule = function() {
const selectedDebtId = amortizationLoanSelect.value;
amortizationScheduleTableBody.innerHTML = ''; // Clear previous schedule
if (!selectedDebtId) {
amortizationScheduleTableBody.innerHTML = 'Please select a loan to view its amortization schedule. ';
return;
}
const selectedDebt = portfolioData.find(debt => debt.id === parseInt(selectedDebtId));
if (!selectedDebt) {
amortizationScheduleTableBody.innerHTML = 'Selected loan not found. ';
return;
}
const schedule = calculateAmortization(selectedDebt);
let html = '';
if (schedule.length === 0) {
html = 'No amortization schedule generated for this loan. ';
} else {
schedule.forEach(payment => {
html += `
${payment.paymentNumber}
${formatDate(payment.paymentDate)}
${formatCurrency(payment.startingBalance)}
${formatCurrency(payment.paymentAmount)}
${formatCurrency(payment.interestPayment)}
${formatCurrency(payment.principalPayment)}
${formatCurrency(payment.endingBalance)}
`;
});
}
amortizationScheduleTableBody.innerHTML = html;
}
// --- PDF Generation Logic ---
/**
* Generates a PDF report of the portfolio.
*/
window.generatePDF = async function() {
// Prepare content for PDF
pdfContentContainer.innerHTML = ''; // Clear previous content
let pdfHtml = `
Private Debt Portfolio Report
`;
// 1. Debt Instruments Table
pdfHtml += `Your Debt Instruments `;
if (portfolioData.length > 0) {
pdfHtml += `
Loan Name
Principal
Interest Rate
Term (M)
Orig. Date
Frequency
`;
portfolioData.forEach(debt => {
pdfHtml += `
${debt.loanName}
${formatCurrency(debt.principalAmount)}
${formatPercentage(debt.annualInterestRate / 100)}
${debt.loanTerm}
${formatDate(new Date(debt.originationDate))}
${debt.repaymentFrequency}
`;
});
pdfHtml += `
`;
} else {
pdfHtml += `No debt instruments added yet.
`;
}
// 2. Portfolio Summary
pdfHtml += `Portfolio Summary `;
if (portfolioData.length > 0) {
let totalPrincipal = 0;
let weightedInterestSum = 0;
let activeDebtsCount = portfolioData.length;
portfolioData.forEach(debt => {
totalPrincipal += debt.principalAmount;
weightedInterestSum += (debt.principalAmount * (debt.annualInterestRate / 100));
});
const weightedAverageInterestRate = totalPrincipal > 0 ? (weightedInterestSum / totalPrincipal) : 0;
pdfHtml += `
Total Principal ${formatCurrency(totalPrincipal)}
Weighted Avg. Interest Rate ${formatPercentage(weightedAverageInterestRate)}
Number of Active Debts ${activeDebtsCount}
`;
} else {
pdfHtml += `No portfolio summary available.
`;
}
// 3. Upcoming Payments Overview
pdfHtml += `Upcoming Payments Overview `;
if (portfolioData.length > 0) {
let upcomingPaymentsHtml = '';
const today = new Date();
today.setHours(0, 0, 0, 0);
const upcomingPayments = [];
portfolioData.forEach(debt => {
const schedule = calculateAmortization(debt);
if (schedule.length > 0) {
const nextPayment = schedule.find(p => p.paymentDate.getTime() >= today.getTime());
if (nextPayment) {
upcomingPayments.push({
loanName: debt.loanName,
nextPaymentDate: nextPayment.paymentDate,
paymentAmount: nextPayment.paymentAmount,
remainingPrincipal: nextPayment.startingBalance
});
}
}
});
if (upcomingPayments.length === 0) {
upcomingPaymentsHtml = 'No upcoming payments found.
';
} else {
upcomingPayments.sort((a, b) => a.nextPaymentDate - b.nextPaymentDate);
upcomingPaymentsHtml += `
Loan Name
Next Payment Date
Payment Amount
Remaining Principal
`;
upcomingPayments.forEach(payment => {
upcomingPaymentsHtml += `
${payment.loanName}
${formatDate(payment.nextPaymentDate)}
${formatCurrency(payment.paymentAmount)}
${formatCurrency(payment.remainingPrincipal)}
`;
});
upcomingPaymentsHtml += `
`;
}
pdfHtml += upcomingPaymentsHtml;
} else {
pdfHtml += `No upcoming payments data.
`;
}
// 4. Amortization Schedule (if one is selected/displayed)
const selectedAmortizationLoanId = amortizationLoanSelect.value;
if (selectedAmortizationLoanId) {
const selectedDebt = portfolioData.find(debt => debt.id === parseInt(selectedAmortizationLoanId));
if (selectedDebt) {
pdfHtml += `Amortization Schedule for: ${selectedDebt.loanName} `;
const schedule = calculateAmortization(selectedDebt);
if (schedule.length > 0) {
pdfHtml += `
Payment #
Payment Date
Starting Balance ($)
Payment ($)
Interest ($)
Principal ($)
Ending Balance ($)
`;
schedule.forEach(payment => {
pdfHtml += `
${payment.paymentNumber}
${formatDate(payment.paymentDate)}
${formatCurrency(payment.startingBalance)}
${formatCurrency(payment.paymentAmount)}
${formatCurrency(payment.interestPayment)}
${formatCurrency(payment.principalPayment)}
${formatCurrency(payment.endingBalance)}
`;
});
pdfHtml += `
`;
} else {
pdfHtml += `No amortization schedule available for this loan.
`;
}
}
}
pdfContentContainer.innerHTML = pdfHtml;
// Temporarily make the container visible (but off-screen) for html2canvas
pdfContentContainer.style.display = 'block';
// Use html2canvas to render the content container into a canvas image
const canvas = await html2canvas(pdfContentContainer, {
scale: 2, // Increase scale for better resolution
logging: false, // Disable logging
useCORS: true // Allow cross-origin images if any (though none expected here)
});
// Hide the container again immediately after rendering to canvas
pdfContentContainer.style.display = 'none';
// Get the image data from the canvas
const imgData = canvas.toDataURL('image/png');
// Initialize jsPDF
const { jsPDF } = window.jspdf;
const doc = new jsPDF('p', 'mm', 'a4'); // 'p' for portrait, 'mm' for millimeters, 'a4' for A4 size
const imgWidth = 210; // A4 width in mm
const pageHeight = 297; // A4 height in mm
const imgHeight = canvas.height * imgWidth / canvas.width;
let heightLeft = imgHeight;
let position = 0;
// Add the image to the PDF
doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
// Handle multiple pages if content is too long
while (heightLeft >= 0) {
position = heightLeft - imgHeight;
doc.addPage();
doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}
doc.save('Private_Debt_Portfolio_Report.pdf'); // Save the PDF
// Clean up: hide the content container again (redundant if already hidden above, but safe)
pdfContentContainer.innerHTML = '';
}
// --- Initial Render and Setup ---
renderDebtTable(); // Render empty table on load
showTab(0); // Show the first tab by default
});