Private Debt Portfolio Management Tool

Private Debt Portfolio Management Tool

Add New Debt Instrument

Your Debt Instruments

Loan Name Principal Interest Rate Term (M) Orig. Date Frequency Actions

Portfolio Summary

Upcoming Payments Overview

Loan Name Next Payment Date Payment Amount Remaining Principal

Amortization Schedule

Payment # Payment Date Starting Balance ($) Payment ($) Interest ($) Principal ($) Ending Balance ($)

${activeDebtsCount}

`; 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 = ''; portfolioData.forEach(debt => { html += ``; }); 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 += `
`; portfolioData.forEach(debt => { pdfHtml += ` `; }); pdfHtml += `
Loan Name Principal Interest Rate Term (M) Orig. Date Frequency
${debt.loanName} ${formatCurrency(debt.principalAmount)} ${formatPercentage(debt.annualInterestRate / 100)} ${debt.loanTerm} ${formatDate(new Date(debt.originationDate))} ${debt.repaymentFrequency}
`; } 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 += `
`; upcomingPayments.forEach(payment => { upcomingPaymentsHtml += ` `; }); upcomingPaymentsHtml += `
Loan Name Next Payment Date Payment Amount Remaining Principal
${payment.loanName} ${formatDate(payment.nextPaymentDate)} ${formatCurrency(payment.paymentAmount)} ${formatCurrency(payment.remainingPrincipal)}
`; } 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 += `
`; schedule.forEach(payment => { pdfHtml += ` `; }); pdfHtml += `
Payment # Payment Date Starting Balance ($) Payment ($) Interest ($) Principal ($) Ending Balance ($)
${payment.paymentNumber} ${formatDate(payment.paymentDate)} ${formatCurrency(payment.startingBalance)} ${formatCurrency(payment.paymentAmount)} ${formatCurrency(payment.interestPayment)} ${formatCurrency(payment.principalPayment)} ${formatCurrency(payment.endingBalance)}
`; } 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 });
Scroll to Top