Cash Flow Projection Calculator

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:
    ${errors.map(e => `
  • ${e}
  • `).join('')}
`; 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 = `

Cash Flow Projection Summary:

`; html += `

Projection Period: ${data.inputs.projectionMonths} months (Starting from ${data.inputs.projectionStartDate || 'Period 1'})

`; html += `
`; html += `

Initial Balance: ${formatCurrency(data.inputs.initialBalance)}

`; html += `

Total Inflows: ${formatCurrency(data.summary.totalInflows)}

`; html += `

Total Outflows: ${formatCurrency(data.summary.totalOutflows)}

`; const netCashFlowClass = data.summary.netCashFlow >= 0 ? 'positive-cashflow' : 'negative-cashflow'; html += `

Net Cash Flow: ${formatCurrency(data.summary.netCashFlow)}

`; const finalBalanceClass = data.summary.finalBalance >= 0 ? '' : 'negative-balance'; html += `

Final Balance: ${formatCurrency(data.summary.finalBalance)}

`; html += `
`; html += `

Month-by-Month Projection:

`; if(data.monthlyProjections.length > 0) { html += `
`; html += ``; data.monthlyProjections.forEach(row => { const netFlowClass = row.netFlow >= 0 ? 'positive-cashflow' : 'negative-cashflow'; const endBalanceClass = row.endBalance >= 0 ? '' : 'negative-balance'; html += ``; }); html += `
MonthStart BalanceInflowsOutflowsNet FlowEnd Balance
${row.month} ${formatCurrency(row.startBalance)} ${formatCurrency(row.inflows)} ${formatCurrency(row.outflows)} ${formatCurrency(row.netFlow)} ${formatCurrency(row.endBalance)}
`; } else { html += `

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(); });
Scroll to Top