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.
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(); });