Expense Sharing Calculator for Roommates

Expense Sharing Calculator for Roommates

Setup Roommates

Add & View Expenses

Logged Expenses

No expenses added yet.

Balances & Summary

Add roommates and expenses to see balances.

Settlement Suggestion

Calculations will appear here.

Export Report

A summary of the report will be shown here before download.

No expenses added yet.

'; return; } let tableHTML = ``; esc_expenses.forEach(exp => { const payer = esc_roommates.find(r => r.id === exp.paidBy); tableHTML += ``; }); tableHTML += '
DescriptionTotal ($)Paid BySplit MethodAction
${exp.description} ${exp.totalAmount.toFixed(2)} ${payer ? payer.name : 'N/A'} ${exp.splitMethod.charAt(0).toUpperCase() + exp.splitMethod.slice(1)}
'; container.innerHTML = tableHTML; } // --- Balances & Settlement (Tab 3) --- function esc_calculateAndDisplayBalances() { const container = document.getElementById('escBalancesTableContainer'); const settlementContainer = document.getElementById('escSettlementSummaryContainer'); if (esc_roommates.length === 0) { container.innerHTML = '

Add roommates to see balances.

'; settlementContainer.innerHTML = '

Settlement Suggestion

Add roommates and expenses first.

'; return; } let balances = {}; esc_roommates.forEach(r => balances[r.id] = { paid: 0, share: 0, net: 0, name: r.name }); esc_expenses.forEach(exp => { if (exp.paidBy && balances[exp.paidBy]) { // Check if payer exists in current roommates balances[exp.paidBy].paid += exp.totalAmount; } for (const pId in exp.calculatedShares) { if (balances[pId]) { // Check if participant exists balances[pId].share += exp.calculatedShares[pId]; } } }); Object.keys(balances).forEach(id => balances[id].net = balances[id].paid - balances[id].share); let tableHTML = `

Final Balances

`; for (const id in balances) { const b = balances[id]; let netClass = b.net < -0.001 ? 'balance-owed' : (b.net > 0.001 ? 'balance-is-owed' : ''); let netText = b.net.toFixed(2); if (netClass === 'balance-owed') netText = `Owes ${Math.abs(b.net).toFixed(2)}`; if (netClass === 'balance-is-owed') netText = `Is Owed ${b.net.toFixed(2)}`; if (Math.abs(b.net) < 0.01) netText = "Settled (0.00)"; // Handle floating point zeros tableHTML += ``; } tableHTML += '
RoommateTotal Paid ($)Total Share ($)Net Balance ($)
${b.name}${b.paid.toFixed(2)} ${b.share.toFixed(2)}${netText}
'; container.innerHTML = tableHTML; // Simple Settlement Suggestion let settlementText = '

Settlement Suggestion

    '; const owers = Object.values(balances).filter(b => b.net < -0.001).sort((a,b) => a.net - b.net); // Most negative first const receivers = Object.values(balances).filter(b => b.net > 0.001).sort((a,b) => b.net - a.net); // Most positive first if (owers.length > 0 && receivers.length > 0) { settlementText += "To settle up, those who owe should pay those who are owed. For example:
      "; // This is a simplified settlement, not necessarily the fewest transactions. // A true "Splitwise" algorithm is more complex. // For this version, we'll just list who owes and who is owed. owers.forEach(ower => { settlementText += `
    • ${ower.name} owes $${Math.abs(ower.net).toFixed(2)} to the group.
    • `; }); receivers.forEach(receiver => { settlementText += `
    • ${receiver.name} is owed $${receiver.net.toFixed(2)} by the group.
    • `; }); settlementText += "

    The group can decide the exact transactions based on these net balances.

    "; } else if (esc_expenses.length > 0) { settlementText += "

    All balances are settled, or no outstanding amounts to transfer among roommates based on logged expenses.

    "; } else { settlementText += "

    Add expenses to see settlement suggestions.

    "; } settlementText += "
"; settlementContainer.innerHTML = settlementText; return balances; // Return for PDF } // --- Export (Tab 4) --- function esc_updateReportPreview() { const previewDiv = document.getElementById('escReportPreview'); const balances = esc_calculateAndDisplayBalances(); // Recalculate to ensure fresh data if needed if (!balances || esc_roommates.length === 0) { previewDiv.innerHTML = "

Add roommates and expenses to generate a report preview.

"; return; } let totalPaidAll = 0; let totalShareAll = 0; Object.values(balances).forEach(b => { totalPaidAll += b.paid; totalShareAll += b.share; // Should be same as totalPaidAll if all expenses are accounted for }); let html = `

Report Preview

Total Roommates: ${esc_roommates.length}

Total Expenses Logged: ${esc_expenses.length}

Sum of All Expenses: $${totalPaidAll.toFixed(2)}

Key Balances:

    `; Object.values(balances).forEach(b => { html += `
  • ${b.name}: Net $${b.net.toFixed(2)} (Paid $${b.paid.toFixed(2)}, Share $${b.share.toFixed(2)})
  • `; }); html += `

The full PDF will contain detailed expense breakdowns.

`; previewDiv.innerHTML = html; } function esc_generatePdf() { const balancesData = esc_calculateAndDisplayBalances(); // Make sure this returns the balances object if (!balancesData || esc_roommates.length === 0) { alert("Please add roommates and expenses before generating a PDF."); return; } const { jsPDF } = window.jspdf; const doc = new jsPDF(); let yPos = 20; const primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim(); const accentColor = getComputedStyle(document.documentElement).getPropertyValue('--accent-color').trim(); const dangerColor = getComputedStyle(document.documentElement).getPropertyValue('--danger-color').trim(); const textColor = getComputedStyle(document.documentElement).getPropertyValue('--text-color').trim(); doc.setFontSize(20); doc.setTextColor(primaryColor); doc.text("Roommate Expense Sharing Report", doc.internal.pageSize.getWidth() / 2, yPos, { align: 'center' }); yPos += 15; // Roommates List doc.setFontSize(14); doc.setTextColor(primaryColor); doc.text("Roommates", 14, yPos); yPos += 8; doc.setFontSize(10); doc.setTextColor(textColor); esc_roommates.forEach(r => { doc.text(`- ${r.name}`, 14, yPos); yPos += 6; }); yPos += 4; // Expenses Table if (esc_expenses.length > 0) { if (yPos > 250) { doc.addPage(); yPos = 20; } doc.setFontSize(14); doc.setTextColor(primaryColor); doc.text("Detailed Expenses", 14, yPos); yPos += 8; const expenseTableBody = []; esc_expenses.forEach(exp => { const payerName = esc_roommates.find(r => r.id === exp.paidBy)?.name || 'N/A'; let sharesString = ""; for(const pId in exp.calculatedShares) { const participantName = esc_roommates.find(r => r.id === pId)?.name || pId; sharesString += `${participantName}: $${exp.calculatedShares[pId].toFixed(2)}\n`; } expenseTableBody.push([ exp.description, `$${exp.totalAmount.toFixed(2)}`, payerName, exp.splitMethod, sharesString.trim() ]); }); doc.autoTable({ startY: yPos, head: [['Description', 'Total', 'Paid By', 'Split Method', 'Individual Shares']], body: expenseTableBody, theme: 'grid', headStyles: { fillColor: primaryColor, textColor: '#ffffff' }, styles: { fontSize: 8, cellPadding: 1.5 }, columnStyles: { 4: { cellWidth: 'wrap', overflow: 'linebreak' } }, // Share details didDrawPage: (data) => { yPos = data.cursor.y; } }); yPos = doc.lastAutoTable.finalY + 10; } // Balances Table if (yPos > 240) { doc.addPage(); yPos = 20; } doc.setFontSize(14); doc.setTextColor(primaryColor); doc.text("Final Balances", 14, yPos); yPos += 8; const balanceTableBody = Object.values(balancesData).map(b => { let netText = `$${b.net.toFixed(2)}`; if (b.net < -0.001) netText = `Owes $${Math.abs(b.net).toFixed(2)}`; else if (b.net > 0.001) netText = `Is Owed $${b.net.toFixed(2)}`; else netText = "Settled ($0.00)"; return [b.name, `$${b.paid.toFixed(2)}`, `$${b.share.toFixed(2)}`, netText]; }); doc.autoTable({ startY: yPos, head: [['Roommate', 'Total Paid', 'Total Share', 'Net Balance']], body: balanceTableBody, theme: 'grid', headStyles: { fillColor: primaryColor, textColor: '#ffffff' }, styles: { fontSize: 9, cellPadding: 2 }, didParseCell: function (data) { // Color net balance if (data.column.index === 3 && data.cell.section === 'body') { const netValue = parseFloat(Object.values(balancesData)[data.row.index].net); if (netValue < -0.001) data.cell.styles.textColor = dangerColor; else if (netValue > 0.001) data.cell.styles.textColor = accentColor; } }, didDrawPage: (data) => { yPos = data.cursor.y; } }); yPos = doc.lastAutoTable.finalY + 10; // Simple Settlement Text in PDF if (yPos > 250) { doc.addPage(); yPos = 20; } const settlementContainer = document.getElementById('escSettlementSummaryContainer'); if (settlementContainer && settlementContainer.innerText.includes("To settle up")) { doc.setFontSize(12); doc.setTextColor(primaryColor); doc.text("Settlement Suggestions", 14, yPos); yPos += 7; doc.setFontSize(9); doc.setTextColor(textColor); const owers = Object.values(balancesData).filter(b => b.net < -0.001); const receivers = Object.values(balancesData).filter(b => b.net > 0.001); if (owers.length > 0 || receivers.length > 0) { doc.text("To settle, the following transfers can be made (or similar):", 14, yPos); yPos +=5; owers.forEach(o => { doc.text(`- ${o.name} owes $${Math.abs(o.net).toFixed(2)} to the group.`, 16, yPos); yPos +=5; }); receivers.forEach(r => { doc.text(`- ${r.name} is owed $${r.net.toFixed(2)} by the group.`, 16, yPos); yPos +=5; }); yPos += 5; doc.text("The group can decide exact transactions based on these net amounts.", 14, yPos); yPos += 5; } else { doc.text("All balances appear settled.", 14, yPos); yPos += 5; } } doc.save('roommate_expense_report.pdf'); } // Initialize document.addEventListener('DOMContentLoaded', () => { esc_showTab(0); esc_renderRoommateList(); // For empty state initially esc_renderExpenseTable(); // Demo data esc_roommates.push({id: 'rm_001', name: 'Alice'}); esc_roommates.push({id: 'rm_002', name: 'Bob'}); esc_roommates.push({id: 'rm_003', name: 'Charlie'}); esc_renderRoommateList(); esc_populatePayerDropdown(); esc_populateParticipantCheckboxes(); });
Scroll to Top