room-app / index.html
Ultronprime's picture
Add 3 files
b4ae449 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Roommate Expense Splitter</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in {
animation: fadeIn 0.3s ease-out;
}
.tabs {
position: relative;
}
.tabs::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: #e2e8f0;
z-index: 1;
}
.tab {
position: relative;
z-index: 2;
}
.tab.active {
border-bottom: 3px solid #4f46e5;
margin-bottom: -1px;
}
.chart-container {
width: 100%;
max-width: 300px;
height: 300px;
position: relative;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="container mx-auto px-4 py-8 max-w-4xl">
<header class="mb-8 text-center">
<h1 class="text 3xl md:text-4xl font-bold text-indigo-700 mb-2">Roommate Expense Splitter</h1>
<p class="text-gray-600">Track and split expenses equally among Althaf, Jamzith & Rasheed</p>
</header>
<div class="bg-white rounded-lg shadow-md overflow-hidden mb-8">
<div class="tabs flex border-b">
<button onclick="switchTab('add')" class="tab active flex-1 py-3 px-4 text-center font-medium text-gray-700 hover:text-indigo-600 transition">
<i class="fas fa-plus-circle mr-2"></i>Add Expense
</button>
<button onclick="switchTab('history')" class="tab flex-1 py-3 px-4 text-center font-medium text-gray-700 hover:text-indigo-600 transition">
<i class="fas fa-history mr-2"></i>Expense History
</button>
<button onclick="switchTab('balance')" class="tab flex-1 py-3 px-4 text-center font-medium text-gray-700 hover:text-indigo-600 transition">
<i class="fas fa-scale-balanced mr-2"></i>Balances
</button>
</div>
<!-- Add Expense Tab -->
<div id="add-tab" class="p-6 fade-in">
<h2 class="text-xl font-semibold mb-4 text-gray-800">Add New Expense</h2>
<form id="expense-form" class="space-y-4">
<div class="grid md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Paid By</label>
<select id="paid-by" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" required>
<option value="" disabled selected>Select roommate</option>
<option value="Althaf">Althaf</option>
<option value="Jamzith">Jamzith</option>
<option value="Rasheed">Rasheed</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Amount (₹)</label>
<input id="amount" type="number" min="0" step="0.01" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" placeholder="0.00" required>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<input id="description" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" placeholder="What was this expense for?" required>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Date</label>
<input id="expense-date" type="date" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" required>
</div>
<div class="pt-2">
<button type="submit" class="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition">
<i class="fas fa-check-circle mr-2"></i>Add Expense
</button>
</div>
</form>
</div>
<!-- Expense History Tab -->
<div id="history-tab" class="p-6 hidden fade-in">
<h2 class="text-xl font-semibold mb-4 text-gray-800">Expense History</h2>
<div class="mb-4 flex justify-between items-center">
<p class="text-sm text-gray-600">Total expenses: <span id="total-expenses" class="font-medium">₹0.00</span></p>
<button onclick="clearAllExpenses()" class="text-sm text-red-500 hover:text-red-700 flex items-center">
<i class="fas fa-trash mr-1"></i> Clear All
</button>
</div>
<div class="border rounded-lg overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Paid By</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Amount</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th>
</tr>
</thead>
<tbody id="expense-list" class="bg-white divide-y divide-gray-200">
<!-- Expenses will be listed here -->
<tr id="no-expenses" class="text-center py-4">
<td colspan="5" class="px-4 py-4 text-sm text-gray-500">No expenses added yet</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Balances Tab -->
<div id="balance-tab" class="p-6 hidden fade-in">
<h2 class="text-xl font-semibold mb-4 text-gray-800">Balances</h2>
<div class="grid md:grid-cols-2 gap-6">
<div>
<h3 class="text-lg font-medium mb-3 text-gray-700">Summary</h3>
<div class="bg-indigo-50 rounded-lg p-4 mb-4">
<div class="flex justify-between items-center mb-2">
<span class="text-indigo-700">Total Spent</span>
<span id="total-spent" class="font-medium">₹0.00</span>
</div>
<div class="flex justify-between items-center mb-2">
<span class="text-indigo-700">Equal Share</span>
<span id="equal-share" class="font-medium">₹0.00</span>
</div>
</div>
<h3 class="text-lg font-medium mb-3 text-gray-700">Individual Balances</h3>
<div class="space-y-3">
<div class="bg-white border rounded-lg p-3 shadow-sm">
<div class="flex justify-between items-center">
<span class="font-medium">Althaf</span>
<span id="althaf-balance" class="text-gray-700">₹0.00</span>
</div>
<div id="althaf-owes" class="text-xs text-gray-500 mt-1"></div>
</div>
<div class="bg-white border rounded-lg p-3 shadow-sm">
<div class="flex justify-between items-center">
<span class="font-medium">Jamzith</span>
<span id="jamzith-balance" class="text-gray-700">₹0.00</span>
</div>
<div id="jamzith-owes" class="text-xs text-gray-500 mt-1"></div>
</div>
<div class="bg-white border rounded-lg p-3 shadow-sm">
<div class="flex justify-between items-center">
<span class="font-medium">Rasheed</span>
<span id="rasheed-balance" class="text-gray-700">₹0.00</span>
</div>
<div id="rasheed-owes" class="text-xs text-gray-500 mt-1"></div>
</div>
</div>
</div>
<div>
<h3 class="text-lg font-medium mb-3 text-gray-700">Visualization</h3>
<div class="chart-container mx-auto">
<canvas id="balance-chart"></canvas>
</div>
<div id="settlement-instructions" class="mt-4 text-sm text-gray-600 p-3 bg-gray-50 rounded-lg">
<p>No settlements needed yet. All balances are zero.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Initialize data
let expenses = JSON.parse(localStorage.getItem('roommate-expenses')) || [];
let balanceChart = null;
// DOM elements
const expenseForm = document.getElementById('expense-form');
const expenseList = document.getElementById('expense-list');
const noExpensesRow = document.getElementById('no-expenses');
const totalExpensesEl = document.getElementById('total-expenses');
const totalSpentEl = document.getElementById('total-spent');
const equalShareEl = document.getElementById('equal-share');
// Balance elements
const althafBalanceEl = document.getElementById('althaf-balance');
const jamzithBalanceEl = document.getElementById('jamzith-balance');
const rasheedBalanceEl = document.getElementById('rasheed-balance');
const althafOwesEl = document.getElementById('althaf-owes');
const jamzithOwesEl = document.getElementById('jamzith-owes');
const rasheedOwesEl = document.getElementById('rasheed-owes');
const settlementInstructionsEl = document.getElementById('settlement-instructions');
// Set today's date as default
document.getElementById('expense-date').valueAsDate = new Date();
// Initialize the app
function init() {
renderExpenseList();
calculateBalances();
}
// Switch between tabs
function switchTab(tabName) {
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('#add-tab, #history-tab, #balance-tab').forEach(tab => tab.classList.add('hidden'));
if (tabName === 'add') {
document.querySelector('.tab:nth-child(1)').classList.add('active');
document.getElementById('add-tab').classList.remove('hidden');
} else if (tabName === 'history') {
document.querySelector('.tab:nth-child(2)').classList.add('active');
document.getElementById('history-tab').classList.remove('hidden');
} else if (tabName === 'balance') {
document.querySelector('.tab:nth-child(3)').classList.add('active');
document.getElementById('balance-tab').classList.remove('hidden');
updateChart();
}
}
// Handle expense form submission
expenseForm.addEventListener('submit', function(e) {
e.preventDefault();
const paidBy = document.getElementById('paid-by').value;
const amount = parseFloat(document.getElementById('amount').value);
const description = document.getElementById('description').value;
const date = document.getElementById('expense-date').value;
const expense = {
id: Date.now().toString(),
paidBy,
amount,
description,
date,
timestamp: new Date(date).getTime()
};
expenses.push(expense);
saveExpenses();
renderExpenseList();
calculateBalances();
// Reset form
expenseForm.reset();
document.getElementById('expense-date').valueAsDate = new Date();
// Show notification
showNotification('Expense added successfully!', 'success');
});
// Render the expense list
function renderExpenseList() {
if (expenses.length === 0) {
noExpensesRow.classList.remove('hidden');
expenseList.innerHTML = '';
expenseList.appendChild(noExpensesRow);
totalExpensesEl.textContent = '₹0.00';
return;
}
noExpensesRow.classList.add('hidden');
// Sort expenses by date (newest first)
expenses.sort((a, b) => b.timestamp - a.timestamp);
expenseList.innerHTML = '';
let total = 0;
expenses.forEach(expense => {
total += expense.amount;
const row = document.createElement('tr');
row.className = 'hover:bg-gray-50';
row.innerHTML = `
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${formatDate(expense.date)}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm font-medium text-gray-900">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800">
${expense.paidBy}
</span>
</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">${expense.description}</td>
<td class="px-4 py-3 whitespace-nowrap text-sm font-medium text-gray-900">₹${expense.amount.toFixed(2)}</td>
<td class="px-4 py-3 whitespace-nowrap text-right text-sm font-medium">
<button onclick="deleteExpense('${expense.id}')" class="text-red-500 hover:text-red-700">
<i class="fas fa-trash"></i>
</button>
</td>
`;
expenseList.appendChild(row);
});
totalExpensesEl.textContent = `₹${total.toFixed(2)}`;
}
// Delete an expense
function deleteExpense(id) {
if (confirm('Are you sure you want to delete this expense?')) {
expenses = expenses.filter(expense => expense.id !== id);
saveExpenses();
renderExpenseList();
calculateBalances();
showNotification('Expense deleted successfully!', 'success');
}
}
// Clear all expenses
function clearAllExpenses() {
if (expenses.length === 0) return;
if (confirm('Are you sure you want to delete ALL expenses? This cannot be undone.')) {
expenses = [];
saveExpenses();
renderExpenseList();
calculateBalances();
showNotification('All expenses cleared!', 'success');
}
}
// Save expenses to localStorage
function saveExpenses() {
localStorage.setItem('roommate-expenses', JSON.stringify(expenses));
}
// Calculate balances and who owes whom
function calculateBalances() {
const totals = {
Althaf: 0,
Jamzith: 0,
Rasheed: 0
};
// Calculate total spent by each person
expenses.forEach(expense => {
totals[expense.paidBy] += expense.amount;
});
// Calculate total amount spent
const totalSpent = totals.Althaf + totals.Jamzith + totals.Rasheed;
totalSpentEl.textContent = `₹${totalSpent.toFixed(2)}`;
// Calculate equal share
const equalShare = totalSpent / 3;
equalShareEl.textContent = `₹${equalShare.toFixed(2)}`;
// Calculate individual balances (positive means they paid more than share, negative means they owe)
const balances = {
Althaf: (Math.round((totals.Althaf - equalShare) * 100) / 100),
Jamzith: (Math.round((totals.Jamzith - equalShare) * 100) / 100),
Rasheed: (Math.round((totals.Rasheed - equalShare) * 100) / 100)
};
// Update UI with balances
updateBalanceUI(balances);
// Generate settlement instructions
generateSettlementInstructions(balances);
}
// Update balance UI elements
function updateBalanceUI(balances) {
althafBalanceEl.textContent = formatCurrency(balances.Althaf);
jamzithBalanceEl.textContent = formatCurrency(balances.Jamzith);
rasheedBalanceEl.textContent = formatCurrency(balances.Rasheed);
// Set text color based on balance (green for positive, red for negative)
setBalanceColor(althafBalanceEl, balances.Althaf);
setBalanceColor(jamzithBalanceEl, balances.Jamzith);
setBalanceColor(rasheedBalanceEl, balances.Rasheed);
}
// Generate settlement instructions
function generateSettlementInstructions(balances) {
const people = [
{ name: 'Althaf', balance: balances.Althaf },
{ name: 'Jamzith', balance: balances.Jamzith },
{ name: 'Rasheed', balance: balances.Rasheed }
];
// Sort by balance (ascending)
people.sort((a, b) => a.balance - b.balance);
let creditors = people.filter(p => p.balance > 0);
let debtors = people.filter(p => p.balance < 0);
if (creditors.length === 0 && debtors.length === 0) {
settlementInstructionsEl.innerHTML = '<p class="text-green-600 font-medium">All balances are settled. No payments needed.</p>';
return;
}
let instructions = '';
// Generate owes text for each person
people.forEach(person => {
if (person.balance < 0) {
const owesText = person.name + ' owes ₹' + Math.abs(person.balance).toFixed(2) + ' in total';
if (person.name === 'Althaf') althafOwesEl.textContent = owesText;
else if (person.name === 'Jamzith') jamzithOwesEl.textContent = owesText;
else if (person.name === 'Rasheed') rasheedOwesEl.textContent = owesText;
} else if (person.balance > 0) {
const owedText = person.name + ' is owed ₹' + person.balance.toFixed(2) + ' in total';
if (person.name === 'Althaf') althafOwesEl.textContent = owedText;
else if (person.name === 'Jamzith') jamzithOwesEl.textContent = owedText;
else if (person.name === 'Rasheed') rasheedOwesEl.textContent = owedText;
} else {
if (person.name === 'Althaf') althafOwesEl.textContent = 'No balance - all settled';
else if (person.name === 'Jamzith') jamzithOwesEl.textContent = 'No balance - all settled';
else if (person.name === 'Rasheed') rasheedOwesEl.textContent = 'No balance - all settled';
}
});
// Generate settlement instructions
if (creditors.length > 0 && debtors.length > 0) {
instructions = '<h4 class="font-medium mb-2 text-gray-800">Settlement Instructions:</h4><ul class="list-disc pl-5 space-y-1">';
// Simplify debts (basic approach)
while (creditors.length > 0 && debtors.length > 0) {
const creditor = creditors[0];
const debtor = debtors[0];
const amount = Math.min(creditor.balance, -debtor.balance);
const roundedAmount = Math.round(amount * 100) / 100;
instructions += `<li>${debtor.name} should pay ${creditor.name}${roundedAmount.toFixed(2)}</li>`;
creditor.balance -= amount;
debtor.balance += amount;
if (Math.abs(creditor.balance) < 0.01) creditors.shift();
if (Math.abs(debtor.balance) < 0.01) debtors.shift();
}
instructions += '</ul>';
}
settlementInstructionsEl.innerHTML = instructions || '<p class="text-green-600 font-medium">All balances are settled. No payments needed.</p>';
}
// Update the balance chart
function updateChart() {
const ctx = document.getElementById('balance-chart').getContext('2d');
const totals = { Althaf: 0, Jamzith: 0, Rasheed: 0 };
expenses.forEach(expense => {
totals[expense.paidBy] += expense.amount;
});
const totalSpent = totals.Althaf + totals.Jamzith + totals.Rasheed;
const equalShare = totalSpent / 3;
const balances = {
Althaf: totals.Althaf - equalShare,
Jamzith: totals.Jamzith - equalShare,
Rasheed: totals.Rasheed - equalShare
};
if (balanceChart) {
balanceChart.destroy();
}
balanceChart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['Althaf', 'Jamzith', 'Rasheed'],
datasets: [{
label: 'Balance (₹)',
data: [balances.Althaf, balances.Jamzith, balances.Rasheed],
backgroundColor: [
'rgba(79, 70, 229, 0.7)',
'rgba(99, 102, 241, 0.7)',
'rgba(129, 140, 248, 0.7)'
],
borderColor: [
'rgba(79, 70, 229, 1)',
'rgba(99, 102, 241, 1)',
'rgba(129, 140, 248, 1)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: false
}
},
plugins: {
tooltip: {
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
const value = context.raw;
label += '₹' + value.toFixed(2);
if (value > 0) {
label += ' (owed)';
} else if (value < 0) {
label += ' (owes)';
} else {
label += ' (settled)';
}
return label;
}
}
}
}
}
});
}
// Helper functions
function formatDate(dateString) {
const options = { year: 'numeric', month: 'short', day: 'numeric' };
return new Date(dateString).toLocaleDateString(undefined, options);
}
function formatCurrency(amount) {
return '₹' + Math.abs(amount).toFixed(2);
}
function setBalanceColor(element, amount) {
if (amount > 0) {
element.classList.remove('text-red-600');
element.classList.add('text-green-600');
} else if (amount < 0) {
element.classList.remove('text-green-600');
element.classList.add('text-red-600');
} else {
element.classList.remove('text-green-600', 'text-red-600');
element.classList.add('text-gray-700');
}
}
function showNotification(message, type) {
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 z-50 px-4 py-2 rounded-md shadow-lg text-white ${
type === 'success' ? 'bg-green-500' : 'bg-red-500'
} animate-fade-in-out`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.classList.add('opacity-0', 'transition-opacity', 'duration-300');
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// Initialize the app
init();
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Ultronprime/room-app" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>