Cash Flow Projection Calculator
Initial Setup
This is for display; calculations are by period number.
Income Streams
Recurring Income
One-Time Income
Expense Streams
Recurring Expenses
One-Time Expenses
Cash Flow Projection Report
Click "Calculate Projection" to view results. Ensure all inputs are set.
No items added yet.
`; return; } items.forEach(item => { const itemDiv = document.createElement('div'); itemDiv.className = 'cfpc-dynamic-list-item'; let text = ''; if (type.includes('Rec')) { // Recurring text = `${item.name}: ${formatCurrency(item.amount)} (${item.freq}, Months ${item.startMonth}-${item.endMonth || 'End'})`; } else { // One-Time text = `${item.desc}: ${formatCurrency(item.amount)} (Month ${item.month})`; } itemDiv.innerHTML = `${text} `; listDiv.appendChild(itemDiv); }); // Add event listeners to new remove buttons listDiv.querySelectorAll('.cfpc-btn-remove-item').forEach(button => { button.addEventListener('click', (e) => { const itemId = parseInt(e.target.dataset.id); const itemType = e.target.dataset.type; removeItem(itemType, itemId); }); }); } function removeItem(type, itemId) { let itemArray, listDivId; if (type === 'recIncome') { itemArray = recurringIncomes; listDivId = 'cfpcRecurringIncomeList'; } else if (type === 'otIncome') { itemArray = oneTimeIncomes; listDivId = 'cfpcOneTimeIncomeList'; } else if (type === 'recExpense') { itemArray = recurringExpenses; listDivId = 'cfpcRecurringExpenseList'; } else if (type === 'otExpense') { itemArray = oneTimeExpenses; listDivId = 'cfpcOneTimeExpenseList'; } if (itemArray) { const initialLength = itemArray.length; itemArray = itemArray.filter(item => item.id !== itemId); // Re-assign the filtered array back to the global scope if (type === 'recIncome') recurringIncomes = itemArray; else if (type === 'otIncome') oneTimeIncomes = itemArray; else if (type === 'recExpense') recurringExpenses = itemArray; else if (type === 'otExpense') oneTimeExpenses = itemArray; if (itemArray.length < initialLength) { // Check if an item was actually removed renderItemList(listDivId, itemArray, type); } } } function clearItemInputs(type) { if (type === 'recIncome') { ['cfpcRecIncomeName', 'cfpcRecIncomeAmount', 'cfpcRecIncomeEnd'].forEach(id => { if(document.getElementById(id)) document.getElementById(id).value = ''; }); if(document.getElementById('cfpcRecIncomeFreq')) document.getElementById('cfpcRecIncomeFreq').value = 'monthly'; if(document.getElementById('cfpcRecIncomeStart')) document.getElementById('cfpcRecIncomeStart').value = '1'; } else if (type === 'otIncome') { ['cfpcOtIncomeDesc', 'cfpcOtIncomeAmount', 'cfpcOtIncomeMonth'].forEach(id => { if(document.getElementById(id)) document.getElementById(id).value = ''; }); } else if (type === 'recExpense') { ['cfpcRecExpenseName', 'cfpcRecExpenseAmount', 'cfpcRecExpenseEnd'].forEach(id => { if(document.getElementById(id)) document.getElementById(id).value = ''; }); if(document.getElementById('cfpcRecExpenseFreq')) document.getElementById('cfpcRecExpenseFreq').value = 'monthly'; if(document.getElementById('cfpcRecExpenseStart')) document.getElementById('cfpcRecExpenseStart').value = '1'; } else if (type === 'otExpense') { ['cfpcOtExpenseDesc', 'cfpcOtExpenseAmount', 'cfpcOtExpenseMonth'].forEach(id => { if(document.getElementById(id)) document.getElementById(id).value = ''; }); } } // Attach event listeners for Add buttons const addRecIncomeBtn = document.getElementById('cfpcAddRecIncomeBtn'); if (addRecIncomeBtn) addRecIncomeBtn.addEventListener('click', () => addItem('recIncome')); const addOtIncomeBtn = document.getElementById('cfpcAddOtIncomeBtn'); if (addOtIncomeBtn) addOtIncomeBtn.addEventListener('click', () => addItem('otIncome')); const addRecExpenseBtn = document.getElementById('cfpcAddRecExpenseBtn'); if (addRecExpenseBtn) addRecExpenseBtn.addEventListener('click', () => addItem('recExpense')); const addOtExpenseBtn = document.getElementById('cfpcAddOtExpenseBtn'); if (addOtExpenseBtn) addOtExpenseBtn.addEventListener('click', () => addItem('otExpense')); // --- Projection Logic --- function runProjection() { if (!projectionResultsDiv) { console.error("Projection results div not found."); return; } projectionResultsDiv.innerHTML = 'Calculating projection...
'; projectionDataForPdf = null; if (downloadPdfBtn) downloadPdfBtn.disabled = true; const initialBalance = getElNumVal('cfpcInitialBalance', 0); const projectionMonths = getElNumVal('cfpcProjectionMonths', 12); const projectionStartDate = getElStrVal('cfpcProjectionStartDate', 'Month 1'); let errors = []; if (isNaN(initialBalance) || initialBalance < 0) errors.push("Initial balance must be a non-negative number."); if (isNaN(projectionMonths) || projectionMonths <= 0 || !Number.isInteger(projectionMonths)) errors.push("Projection months must be a positive integer."); // Further validation for item amounts and months could be added here if needed if (errors.length > 0) { projectionResultsDiv.innerHTML = `Input Errors:
`;
if (downloadPdfBtn) downloadPdfBtn.disabled = true;
return;
}
const monthlyProjections = [];
let currentBalance = initialBalance;
let totalInflows = 0;
let totalOutflows = 0;
for (let month = 1; month <= projectionMonths; month++) {
let monthInflows = 0;
let monthOutflows = 0;
const startOfMonthBalance = currentBalance;
// Recurring Incomes
recurringIncomes.forEach(item => {
if (month >= item.startMonth && month <= item.endMonth) {
if (item.freq === 'monthly') {
monthInflows += item.amount;
} else if (item.freq === 'quarterly' && (month - item.startMonth + 1) % 3 === 1) { // Occurs in first month of its active quarter
monthInflows += item.amount;
} else if (item.freq === 'annually' && (month - item.startMonth + 1) % 12 === 1) { // Occurs in first month of its active year
monthInflows += item.amount;
}
}
});
// One-Time Incomes
oneTimeIncomes.forEach(item => {
if (item.month === month) {
monthInflows += item.amount;
}
});
// Recurring Expenses
recurringExpenses.forEach(item => {
if (month >= item.startMonth && month <= item.endMonth) {
if (item.freq === 'monthly') {
monthOutflows += item.amount;
} else if (item.freq === 'quarterly' && (month - item.startMonth + 1) % 3 === 1) {
monthOutflows += item.amount;
} else if (item.freq === 'annually' && (month - item.startMonth + 1) % 12 === 1) {
monthOutflows += item.amount;
}
}
});
// One-Time Expenses
oneTimeExpenses.forEach(item => {
if (item.month === month) {
monthOutflows += item.amount;
}
});
currentBalance += monthInflows - monthOutflows;
totalInflows += monthInflows;
totalOutflows += monthOutflows;
monthlyProjections.push({
month,
startBalance: startOfMonthBalance,
inflows: monthInflows,
outflows: monthOutflows,
netFlow: monthInflows - monthOutflows,
endBalance: currentBalance
});
}
projectionDataForPdf = {
inputs: { initialBalance, projectionMonths, projectionStartDate },
summary: { totalInflows, totalOutflows, netCashFlow: totalInflows - totalOutflows, finalBalance: currentBalance },
monthlyProjections,
incomeItems: { recurring: recurringIncomes, oneTime: oneTimeIncomes }, // For PDF
expenseItems: { recurring: recurringExpenses, oneTime: oneTimeExpenses } // For PDF
};
displayProjectionResults(projectionDataForPdf);
if (downloadPdfBtn) downloadPdfBtn.disabled = false;
if (pdfButtonContainer) pdfButtonContainer.style.display = 'block';
}
function displayProjectionResults(data) {
if (!projectionResultsDiv || !data) return;
let html = `- ${errors.map(e => `
- ${e} `).join('')}
Cash Flow Projection Summary:
`; html += `Projection Period: ${data.inputs.projectionMonths} months (Starting from ${data.inputs.projectionStartDate || 'Period 1'})
`; html += ``;
html += ``;
html += ``;
html += ``;
const netCashFlowClass = data.summary.netCashFlow >= 0 ? 'positive-cashflow' : 'negative-cashflow';
html += ``;
const finalBalanceClass = data.summary.finalBalance >= 0 ? '' : 'negative-balance';
html += ``;
html += `
`;
html += `Initial Balance: ${formatCurrency(data.inputs.initialBalance)}
Total Inflows: ${formatCurrency(data.summary.totalInflows)}
Total Outflows: ${formatCurrency(data.summary.totalOutflows)}
Net Cash Flow: ${formatCurrency(data.summary.netCashFlow)}
Final Balance: ${formatCurrency(data.summary.finalBalance)}
Month-by-Month Projection:
`; if(data.monthlyProjections.length > 0) { html += ``;
html += `
`;
} else {
html += `| Month | Start Balance | Inflows | Outflows | Net Flow | End Balance |
|---|---|---|---|---|---|
| ${row.month} | ${formatCurrency(row.startBalance)} | ${formatCurrency(row.inflows)} | ${formatCurrency(row.outflows)} | ${formatCurrency(row.netFlow)} | ${formatCurrency(row.endBalance)} |
No projection data to display.
`; } projectionResultsDiv.innerHTML = html; } if (calculateProjectionBtn) { calculateProjectionBtn.addEventListener('click', runProjection); } // --- 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 (!projectionDataForPdf) { alert("No projection data to download. Please run the projection first."); return; } const { jsPDF } = window.jspdf; const doc = new jsPDF({ unit: 'pt', format: 'a4' }); const data = projectionDataForPdf; 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(30, 58, 138); // Primary doc.text(text, doc.internal.pageSize.getWidth() / 2, y, { align: 'center' }); y += 30; } function addSectionTitle(text) { if (y > doc.internal.pageSize.getHeight() - 70) { doc.addPage(); y = pageMargin; } doc.setFontSize(12); doc.setFont(undefined, 'bold'); doc.setTextColor(59, 130, 246); // Secondary doc.text(text, pageMargin, y); y += 20; } function addLine(key, value, valueColor = [31,41,55]) { if (y > doc.internal.pageSize.getHeight() - 35) { doc.addPage(); y = pageMargin; } doc.setFontSize(9); doc.setFont(undefined, 'bold'); doc.setTextColor(31,41,55); 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 + 150, y, { align: 'left', maxWidth: pageWidth - 150 - 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(107, 114, 128); const splitText = doc.splitTextToSize(text, pageWidth); doc.setFillColor(243,244,246); 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.5); const headerFillColor = [59, 130, 246]; const headerTextColor = [255,255,255]; const rowTextColor = [31,41,55]; 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); let cellColor = rowTextColor; if (headers[i] === "Net Flow" && parseFloat(cellText.replace(/[$,]/g, '')) < 0) cellColor = [239,68,68]; // Red else if (headers[i] === "Net Flow" && parseFloat(cellText.replace(/[$,]/g, '')) > 0) cellColor = [16,185,129]; // Green else if (headers[i] === "End Balance" && parseFloat(cellText.replace(/[$,]/g, '')) < 0) cellColor = [239,68,68]; // Red doc.setTextColor(cellColor[0], cellColor[1], cellColor[2]); const textLines = doc.splitTextToSize(cellText, columnWidths[i] - 6); doc.text(textLines, currentX + 3, y + 11); currentX += columnWidths[i]; }); doc.setTextColor(rowTextColor[0], rowTextColor[1], rowTextColor[2]); // Reset color y += 16; }); y += 8; } addMainTitle("Cash Flow Projection Report"); addInfo(`Report Generated: ${new Date().toLocaleString()}`); y += 5; addSectionTitle("Projection Setup"); addLine("Initial Cash Balance:", formatCurrency(data.inputs.initialBalance)); addLine("Projection Period:", `${data.inputs.projectionMonths} months`); addLine("Projection Start (Reference):", data.inputs.projectionStartDate || 'Period 1'); y += 10; addSectionTitle("Overall Summary"); addLine("Total Inflows:", formatCurrency(data.summary.totalInflows), [16,185,129], [16,185,129]); addLine("Total Outflows:", formatCurrency(data.summary.totalOutflows), [239,68,68], [239,68,68]); const netFlowColor = data.summary.netCashFlow >= 0 ? [16,185,129] : [239,68,68]; addLine("Net Cash Flow:", formatCurrency(data.summary.netCashFlow), netFlowColor, netFlowColor); const finalBalanceColor = data.summary.finalBalance >= 0 ? [31,41,55] : [239,68,68]; addLine("Final Balance:", formatCurrency(data.summary.finalBalance), finalBalanceColor, finalBalanceColor); y += 10; // Could add sections for income/expense items list here if desired for PDF, but keeping it concise for now. if (data.monthlyProjections.length > 0) { addSectionTitle("Month-by-Month Projection"); const tableH = ["Month", "Start Bal.", "Inflows", "Outflows", "Net Flow", "End Bal."]; const tableCW = [40, 90, 80, 80, 80, 90]; const tableFData = data.monthlyProjections.map(r => [ r.month, formatCurrency(r.startBalance,0,0), formatCurrency(r.inflows,0,0), formatCurrency(r.outflows,0,0), formatCurrency(r.netFlow,0,0), formatCurrency(r.endBalance,0,0) ]); addTable(tableH, tableFData, tableCW); } addInfo("This projection is based on the inputs provided and does not account for taxes, investment growth/loss on cash balances, or unforeseen circumstances. It is for illustrative purposes only."); 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} - Cash Flow Projection Calculator`, pageMargin, doc.internal.pageSize.getHeight() - 15); } doc.save('Cash_Flow_Projection.pdf'); } if (downloadPdfBtn) { downloadPdfBtn.addEventListener('click', () => loadJsPdfIfNeeded(downloadReportAsPdf)); downloadPdfBtn.disabled = true; } // --- Initialization --- showTab(0); if (downloadPdfBtn) downloadPdfBtn.disabled = true; if (pdfButtonContainer) pdfButtonContainer.style.display = 'block'; // Make container visible // Initialize empty lists on load renderItemList('cfpcRecurringIncomeList', recurringIncomes, 'recIncome'); renderItemList('cfpcOneTimeIncomeList', oneTimeIncomes, 'otIncome'); renderItemList('cfpcRecurringExpenseList', recurringExpenses, 'recExpense'); renderItemList('cfpcOneTimeExpenseList', oneTimeExpenses, 'otExpense'); loadJsPdfIfNeeded(); });