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 = `| Description | Total ($) | Paid By | Split Method | Action |
|---|---|---|---|---|
| ${exp.description} | ${exp.totalAmount.toFixed(2)} | ${payer ? payer.name : 'N/A'} | ${exp.splitMethod.charAt(0).toUpperCase() + exp.splitMethod.slice(1)} |
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
| Roommate | Total Paid ($) | Total Share ($) | Net Balance ($) |
|---|---|---|---|
| ${b.name} | ${b.paid.toFixed(2)} | ${b.share.toFixed(2)} | ${netText} |
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:
- ${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 += "
- ";
// 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 += `
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 += "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(); });