πŸ’° Group Expense Tracker

πŸ“ˆ Current Balances

Loading balances...

πŸ“Š Monthly Summary

Loading monthly data...

βž• Add New Expense

πŸ“‹ All Expenses

Loading expenses...

πŸ”„ Recurring Expenses

Loading recurring expenses...

πŸ‘₯ Group Members

Set up your group members. You can change these anytime.

🌍 Currency Settings

πŸ’± Select Currency

Choose your preferred currency for all expenses and calculations.

πŸ“‚ Expense Categories

βž• Add New Category

πŸ“‹ Current Categories

Loading categories...

πŸ“€ Export Data

Download your expense data for backup or analysis.

πŸ—‘οΈ Data Management

⚠️ Danger Zone - These actions cannot be undone!

, 'EUR': '€', 'CZK': 'Kč' }; // Initialize the application document.addEventListener('DOMContentLoaded', function() { // Set today's date as default document.getElementById('expense-date').valueAsDate = new Date(); // Load initial data loadMembers(); loadCategories(); loadExpenses(); loadRecurringExpenses(); updatePaidByDropdown(); updateCategoryDropdowns(); }); // Tab Management function showTab(tabName) { // Hide all tabs document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); document.querySelectorAll('.tab').forEach(tab => { tab.classList.remove('active'); }); // Show selected tab document.getElementById(tabName).classList.add('active'); event.target.classList.add('active'); // Load data based on tab if (tabName === 'dashboard') { loadDashboard(); } else if (tabName === 'expenses') { loadExpensesList(); } else if (tabName === 'recurring') { loadRecurringExpenses(); } else if (tabName === 'settings') { loadCategoriesList(); } } // Member Management function loadMembers() { fetch('api.php?action=get_members') .then(response => response.json()) .then(data => { if (data.success) { members = data.members; updateMemberInputs(); updatePaidByDropdown(); } }) .catch(error => console.error('Error loading members:', error)); } // Category Management function loadCategories() { fetch('api.php?action=get_categories') .then(response => response.json()) .then(data => { if (data.success) { categories = data.categories; updateCategoryDropdowns(); } }) .catch(error => console.error('Error loading categories:', error)); } function updateCategoryDropdowns() { const selects = [ document.getElementById('expense-category'), document.getElementById('filter-category') ]; selects.forEach(select => { if (select) { const currentValue = select.value; select.innerHTML = ''; categories.forEach(category => { const option = document.createElement('option'); option.value = category.name; option.textContent = `${category.emoji} ${category.label}`; select.appendChild(option); }); // Restore previous selection if (currentValue) { select.value = currentValue; } } }); } function addCategory() { const name = document.getElementById('new-category-name').value.trim(); const emoji = document.getElementById('new-category-emoji').value.trim(); if (!name) { showAlert('Please enter a category name', 'error'); return; } // Create category object const category = { name: name.toLowerCase().replace(/\s+/g, '_'), emoji: emoji || 'πŸ“¦', label: name }; fetch('api.php?action=add_category', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(category) }) .then(response => response.json()) .then(data => { if (data.success) { showAlert('Category added successfully!', 'success'); document.getElementById('new-category-name').value = ''; document.getElementById('new-category-emoji').value = ''; loadCategories(); loadCategoriesList(); } else { showAlert('Error adding category: ' + data.message, 'error'); } }) .catch(error => { showAlert('Error adding category: ' + error.message, 'error'); }); } function deleteCategory(categoryName) { if (confirm('Are you sure you want to delete this category?')) { fetch('api.php?action=delete_category', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: categoryName }) }) .then(response => response.json()) .then(data => { if (data.success) { showAlert('Category deleted successfully!', 'success'); loadCategories(); loadCategoriesList(); } else { showAlert('Error deleting category: ' + data.message, 'error'); } }) .catch(error => { showAlert('Error deleting category: ' + error.message, 'error'); }); } } function loadCategoriesList() { const container = document.getElementById('categories-list'); container.innerHTML = ''; categories.forEach(category => { const item = document.createElement('div'); item.className = 'expense-item'; item.innerHTML = `
${category.emoji} ${category.label}
ID: ${category.name}
`; container.appendChild(item); }); } function updateMemberInputs() { for (let i = 0; i < 4; i++) { const input = document.getElementById(`member${i + 1}`); if (input) { input.value = members[i] || ''; } } } function updatePaidByDropdown() { const select = document.getElementById('expense-paid-by'); select.innerHTML = ''; members.forEach((member, index) => { if (member.trim()) { const option = document.createElement('option'); option.value = member; option.textContent = member; select.appendChild(option); } }); } function saveMembers() { const newMembers = []; for (let i = 1; i <= 4; i++) { const value = document.getElementById(`member${i}`).value.trim(); newMembers.push(value || `Person ${i}`); } fetch('api.php?action=save_members', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ members: newMembers }) }) .then(response => response.json()) .then(data => { if (data.success) { members = newMembers; updatePaidByDropdown(); showAlert('Members saved successfully!', 'success'); } else { showAlert('Error saving members: ' + data.message, 'error'); } }) .catch(error => { showAlert('Error saving members: ' + error.message, 'error'); }); } // Expense Management function loadExpenses() { fetch('api.php?action=get_expenses') .then(response => response.json()) .then(data => { if (data.success) { expenses = data.expenses; calculateBalances(); } }) .catch(error => console.error('Error loading expenses:', error)); } function loadRecurringExpenses() { fetch('api.php?action=get_recurring') .then(response => response.json()) .then(data => { if (data.success) { recurringExpenses = data.recurring; loadRecurringList(); } }) .catch(error => console.error('Error loading recurring expenses:', error)); } function toggleRecurringFields() { const frequency = document.getElementById('expense-frequency').value; const recurringFields = document.getElementById('recurring-fields'); if (frequency === 'one-time') { recurringFields.style.display = 'none'; } else { recurringFields.style.display = 'block'; } } document.getElementById('expense-form').addEventListener('submit', function(e) { e.preventDefault(); const expense = { date: document.getElementById('expense-date').value, category: document.getElementById('expense-category').value, amount: parseFloat(document.getElementById('expense-amount').value), paid_by: document.getElementById('expense-paid-by').value, description: document.getElementById('expense-description').value, frequency: document.getElementById('expense-frequency').value, recurring_until: document.getElementById('recurring-until').value }; const action = expense.frequency === 'one-time' ? 'add_expense' : 'add_recurring'; fetch(`api.php?action=${action}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(expense) }) .then(response => response.json()) .then(data => { if (data.success) { const message = expense.frequency === 'one-time' ? 'Expense added successfully!' : 'Recurring expense created successfully!'; showAlert(message, 'success', 'expense-alert'); document.getElementById('expense-form').reset(); document.getElementById('expense-date').valueAsDate = new Date(); document.getElementById('expense-frequency').value = 'one-time'; toggleRecurringFields(); loadExpenses(); loadRecurringExpenses(); } else { showAlert('Error adding expense: ' + data.message, 'error', 'expense-alert'); } }) .catch(error => { showAlert('Error adding expense: ' + error.message, 'error', 'expense-alert'); }); }); function calculateBalances() { balances = {}; // Initialize balances members.forEach(member => { balances[member] = { paid: 0, owes: 0, balance: 0 }; }); // Calculate totals expenses.forEach(expense => { const amount = parseFloat(expense.amount); const sharePerPerson = amount / members.length; // Add to paid amount if (balances[expense.paid_by]) { balances[expense.paid_by].paid += amount; } // Add to what each person owes members.forEach(member => { if (balances[member]) { balances[member].owes += sharePerPerson; } }); }); // Calculate final balances Object.keys(balances).forEach(member => { balances[member].balance = balances[member].paid - balances[member].owes; }); } function loadDashboard() { calculateBalances(); // Update balance cards const balancesContainer = document.getElementById('balances-summary'); balancesContainer.innerHTML = ''; Object.keys(balances).forEach(member => { const balance = balances[member]; const card = document.createElement('div'); card.className = 'summary-card'; const balanceClass = balance.balance >= 0 ? 'balance-positive' : 'balance-negative'; const status = balance.balance >= 0 ? 'Is owed' : 'Owes'; card.innerHTML = `

${member}

$${Math.abs(balance.balance).toFixed(2)}
${status}
Paid: $${balance.paid.toFixed(2)} | Owes: $${balance.owes.toFixed(2)} `; balancesContainer.appendChild(card); }); // Update monthly summary const currentMonth = new Date().getMonth(); const currentYear = new Date().getFullYear(); const monthlyExpenses = expenses.filter(expense => { const expenseDate = new Date(expense.date); return expenseDate.getMonth() === currentMonth && expenseDate.getFullYear() === currentYear; }); const monthlyTotal = monthlyExpenses.reduce((sum, expense) => sum + parseFloat(expense.amount), 0); const monthlyPerPerson = monthlyTotal / members.length; document.getElementById('monthly-summary').innerHTML = `

This Month

$${monthlyTotal.toFixed(2)}
Total Expenses

Per Person

$${monthlyPerPerson.toFixed(2)}
Monthly Share

Transactions

${monthlyExpenses.length}
This Month
`; } function loadExpensesList() { const container = document.getElementById('expenses-list'); container.innerHTML = ''; if (expenses.length === 0) { container.innerHTML = '

No expenses recorded yet.

'; return; } expenses.sort((a, b) => new Date(b.date) - new Date(a.date)); expenses.forEach((expense, index) => { const categoryObj = categories.find(cat => cat.name === expense.category); const categoryLabel = categoryObj ? `${categoryObj.emoji} ${categoryObj.label}` : expense.category; const item = document.createElement('div'); item.className = 'expense-item'; item.innerHTML = `
${expense.date}
${categoryLabel}
${parseFloat(expense.amount).toFixed(2)}
Paid by ${expense.paid_by}
Each owes:
${(parseFloat(expense.amount) / members.length).toFixed(2)}
${expense.description || 'No description'}
`; container.appendChild(item); }); } function loadRecurringList() { const container = document.getElementById('recurring-list'); container.innerHTML = ''; if (recurringExpenses.length === 0) { container.innerHTML = '

No recurring expenses set up yet.

'; return; } recurringExpenses.forEach((recurring, index) => { const nextDue = calculateNextDue(recurring); const isDue = nextDue <= new Date(); const item = document.createElement('div'); item.className = 'expense-item'; item.style.borderLeft = isDue ? '4px solid #e74c3c' : '4px solid #3498db'; item.innerHTML = `
${recurring.description}
${recurring.frequency} - ${recurring.category}
${parseFloat(recurring.amount).toFixed(2)}
Paid by ${recurring.paid_by}
Next Due:
${nextDue.toLocaleDateString()}
Status:
${isDue ? 'DUE NOW' : 'Active'}
`; container.appendChild(item); }); } function calculateNextDue(recurring) { const lastProcessed = new Date(recurring.last_processed || recurring.start_date); const nextDue = new Date(lastProcessed); switch (recurring.frequency) { case 'weekly': nextDue.setDate(nextDue.getDate() + 7); break; case 'monthly': nextDue.setMonth(nextDue.getMonth() + 1); break; case 'yearly': nextDue.setFullYear(nextDue.getFullYear() + 1); break; } return nextDue; } function processRecurringExpenses() { fetch('api.php?action=process_recurring', { method: 'POST' }) .then(response => response.json()) .then(data => { if (data.success) { showAlert(`Processed ${data.processed} recurring expenses`, 'success'); loadExpenses(); loadRecurringExpenses(); } else { showAlert('Error processing recurring expenses: ' + data.message, 'error'); } }) .catch(error => { showAlert('Error processing recurring expenses: ' + error.message, 'error'); }); } function deleteRecurring(index) { if (confirm('Are you sure you want to delete this recurring expense?')) { fetch('api.php?action=delete_recurring', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ index: index }) }) .then(response => response.json()) .then(data => { if (data.success) { showAlert('Recurring expense deleted successfully!', 'success'); loadRecurringExpenses(); } else { showAlert('Error deleting recurring expense: ' + data.message, 'error'); } }) .catch(error => { showAlert('Error deleting recurring expense: ' + error.message, 'error'); }); } if (confirm('Are you sure you want to delete this expense?')) { fetch('api.php?action=delete_expense', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ index: index }) }) .then(response => response.json()) .then(data => { if (data.success) { loadExpenses(); showAlert('Expense deleted successfully!', 'success'); } else { showAlert('Error deleting expense: ' + data.message, 'error'); } }) .catch(error => { showAlert('Error deleting expense: ' + error.message, 'error'); }); } } function filterExpenses() { const month = document.getElementById('filter-month').value; const category = document.getElementById('filter-category').value; let filteredExpenses = expenses; if (month) { filteredExpenses = filteredExpenses.filter(expense => { return expense.date.startsWith(month); }); } if (category) { filteredExpenses = filteredExpenses.filter(expense => { return expense.category === category; }); } // Display filtered expenses const container = document.getElementById('expenses-list'); container.innerHTML = ''; filteredExpenses.forEach((expense, index) => { const item = document.createElement('div'); item.className = 'expense-item'; item.innerHTML = `
${expense.date}
${expense.category}
$${parseFloat(expense.amount).toFixed(2)}
Paid by ${expense.paid_by}
Each owes:
$${(parseFloat(expense.amount) / members.length).toFixed(2)}
${expense.description || 'No description'}
`; container.appendChild(item); }); } // Export Functions function exportData(format) { const url = `api.php?action=export&format=${format}`; window.open(url, '_blank'); } function clearAllData() { if (confirm('Are you sure you want to clear ALL data? This cannot be undone!')) { if (confirm('This will delete all expenses and reset everything. Are you absolutely sure?')) { fetch('api.php?action=clear_all', { method: 'POST' }) .then(response => response.json()) .then(data => { if (data.success) { expenses = []; balances = {}; loadDashboard(); showAlert('All data cleared successfully!', 'success'); } else { showAlert('Error clearing data: ' + data.message, 'error'); } }) .catch(error => { showAlert('Error clearing data: ' + error.message, 'error'); }); } } } // Utility Functions function showAlert(message, type, containerId = null) { const alertDiv = document.createElement('div'); alertDiv.className = `alert alert-${type}`; alertDiv.textContent = message; const container = containerId ? document.getElementById(containerId) : document.querySelector('.container'); container.insertBefore(alertDiv, container.firstChild); setTimeout(() => { alertDiv.remove(); }, 5000); }