// Transactions page JavaScript let currentPage = 1; let filters = { category_id: '', start_date: '', end_date: '', search: '' }; // Load user profile to get currency async function loadUserCurrency() { try { const profile = await apiCall('/api/settings/profile'); window.userCurrency = profile.profile.currency || 'RON'; } catch (error) { console.error('Failed to load user currency:', error); window.userCurrency = 'RON'; } } // Load transactions async function loadTransactions() { try { const params = new URLSearchParams({ page: currentPage, ...filters }); const data = await apiCall(`/api/expenses/?${params}`); displayTransactions(data.expenses); displayPagination(data.pages, data.current_page, data.total || data.expenses.length); } catch (error) { console.error('Failed to load transactions:', error); } } // Display transactions function displayTransactions(transactions) { const container = document.getElementById('transactions-list'); if (transactions.length === 0) { const noTransactionsText = window.getTranslation ? window.getTranslation('transactions.noTransactions', 'No transactions found') : 'No transactions found'; container.innerHTML = ` receipt_long

${noTransactionsText}

`; return; } container.innerHTML = transactions.map(tx => { const txDate = new Date(tx.date); const dateStr = txDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); const timeStr = txDate.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); // Get category color const categoryColors = { 'Food': { bg: 'bg-green-500/10', text: 'text-green-400', border: 'border-green-500/20', dot: 'bg-green-400' }, 'Transport': { bg: 'bg-orange-500/10', text: 'text-orange-400', border: 'border-orange-500/20', dot: 'bg-orange-400' }, 'Entertainment': { bg: 'bg-purple-500/10', text: 'text-purple-400', border: 'border-purple-500/20', dot: 'bg-purple-400' }, 'Shopping': { bg: 'bg-blue-500/10', text: 'text-blue-400', border: 'border-blue-500/20', dot: 'bg-blue-400' }, 'Healthcare': { bg: 'bg-red-500/10', text: 'text-red-400', border: 'border-red-500/20', dot: 'bg-red-400' }, 'Bills': { bg: 'bg-yellow-500/10', text: 'text-yellow-400', border: 'border-yellow-500/20', dot: 'bg-yellow-400' }, 'Education': { bg: 'bg-pink-500/10', text: 'text-pink-400', border: 'border-pink-500/20', dot: 'bg-pink-400' }, 'Other': { bg: 'bg-gray-500/10', text: 'text-gray-400', border: 'border-gray-500/20', dot: 'bg-gray-400' } }; const catColor = categoryColors[tx.category_name] || categoryColors['Other']; // Status icon (completed/pending) const isCompleted = true; // For now, all are completed const statusIcon = isCompleted ? 'check' : 'schedule'; const statusClass = isCompleted ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-400'; const statusTitle = isCompleted ? (window.getTranslation ? window.getTranslation('transactions.completed', 'Completed') : 'Completed') : (window.getTranslation ? window.getTranslation('transactions.pending', 'Pending') : 'Pending'); return `
payments
${tx.description} ${tx.tags.length > 0 ? tx.tags.join(', ') : (window.getTranslation ? window.getTranslation('transactions.expense', 'Expense') : 'Expense')}
${tx.category_name} ${dateStr} ${timeStr}
credit_card •••• ${window.userCurrency || 'RON'}
${formatCurrency(tx.amount, tx.currency || window.userCurrency || 'GBP')} ${statusIcon}
${tx.receipt_path ? ` ` : ''}
`; }).join(''); } // Display pagination function displayPagination(totalPages, current, totalItems = 0) { const container = document.getElementById('pagination'); // Update pagination info const perPage = 10; const start = (current - 1) * perPage + 1; const end = Math.min(current * perPage, totalItems); document.getElementById('page-start').textContent = totalItems > 0 ? start : 0; document.getElementById('page-end').textContent = end; document.getElementById('total-count').textContent = totalItems; if (totalPages <= 1) { container.innerHTML = ''; return; } let html = ''; // Previous button const prevDisabled = current <= 1; const prevText = window.getTranslation ? window.getTranslation('transactions.previous', 'Previous') : 'Previous'; const nextText = window.getTranslation ? window.getTranslation('transactions.next', 'Next') : 'Next'; html += ` `; // Next button const nextDisabled = current >= totalPages; html += ` `; container.innerHTML = html; } // Change page function changePage(page) { currentPage = page; loadTransactions(); } // Edit transaction let currentExpenseId = null; let currentReceiptPath = null; async function editTransaction(id) { try { // Fetch expense details const data = await apiCall(`/api/expenses/?page=1`); const expense = data.expenses.find(e => e.id === id); if (!expense) { showToast(window.getTranslation ? window.getTranslation('transactions.notFound', 'Transaction not found') : 'Transaction not found', 'error'); return; } // Store current expense data currentExpenseId = id; currentReceiptPath = expense.receipt_path; // Update modal title const modalTitle = document.getElementById('expense-modal-title'); modalTitle.textContent = window.getTranslation ? window.getTranslation('modal.edit_expense', 'Edit Expense') : 'Edit Expense'; // Load categories await loadCategoriesForModal(); // Populate form fields const form = document.getElementById('expense-form'); form.querySelector('[name="amount"]').value = expense.amount; form.querySelector('[name="description"]').value = expense.description; form.querySelector('[name="category_id"]').value = expense.category_id; // Format date for input (YYYY-MM-DD) const expenseDate = new Date(expense.date); const dateStr = expenseDate.toISOString().split('T')[0]; form.querySelector('[name="date"]').value = dateStr; // Populate tags if (expense.tags && expense.tags.length > 0) { form.querySelector('[name="tags"]').value = expense.tags.join(', '); } // Show current receipt info if exists const receiptInfo = document.getElementById('current-receipt-info'); const viewReceiptBtn = document.getElementById('view-current-receipt'); if (expense.receipt_path) { receiptInfo.classList.remove('hidden'); viewReceiptBtn.onclick = () => viewReceipt(expense.receipt_path); } else { receiptInfo.classList.add('hidden'); } // Update submit button text const submitBtn = document.getElementById('expense-submit-btn'); submitBtn.textContent = window.getTranslation ? window.getTranslation('actions.update', 'Update Expense') : 'Update Expense'; // Show modal document.getElementById('expense-modal').classList.remove('hidden'); } catch (error) { console.error('Failed to load transaction for editing:', error); showToast(window.getTranslation ? window.getTranslation('common.error', 'An error occurred') : 'An error occurred', 'error'); } } // Make editTransaction global window.editTransaction = editTransaction; // Delete transaction async function deleteTransaction(id) { const confirmMsg = window.getTranslation ? window.getTranslation('transactions.deleteConfirm', 'Are you sure you want to delete this transaction?') : 'Are you sure you want to delete this transaction?'; const successMsg = window.getTranslation ? window.getTranslation('transactions.deleted', 'Transaction deleted') : 'Transaction deleted'; if (!confirm(confirmMsg)) { return; } try { await apiCall(`/api/expenses/${id}`, { method: 'DELETE' }); showToast(successMsg, 'success'); loadTransactions(); } catch (error) { console.error('Failed to delete transaction:', error); } } // Load categories for filter async function loadCategoriesFilter() { try { const data = await apiCall('/api/expenses/categories'); const select = document.getElementById('filter-category'); const categoryText = window.getTranslation ? window.getTranslation('transactions.allCategories', 'Category') : 'Category'; select.innerHTML = `` + data.categories.map(cat => ``).join(''); } catch (error) { console.error('Failed to load categories:', error); } } // Load categories for modal async function loadCategoriesForModal() { try { const data = await apiCall('/api/expenses/categories'); const select = document.querySelector('#expense-form [name="category_id"]'); const selectText = window.getTranslation ? window.getTranslation('dashboard.selectCategory', 'Select category...') : 'Select category...'; // Map category names to translation keys const categoryTranslations = { 'Food & Dining': 'categories.foodDining', 'Transportation': 'categories.transportation', 'Shopping': 'categories.shopping', 'Entertainment': 'categories.entertainment', 'Bills & Utilities': 'categories.billsUtilities', 'Healthcare': 'categories.healthcare', 'Education': 'categories.education', 'Other': 'categories.other' }; select.innerHTML = `` + data.categories.map(cat => { const translationKey = categoryTranslations[cat.name]; const translatedName = translationKey && window.getTranslation ? window.getTranslation(translationKey, cat.name) : cat.name; return ``; }).join(''); } catch (error) { console.error('Failed to load categories:', error); } } // Toggle advanced filters function toggleAdvancedFilters() { const advFilters = document.getElementById('advanced-filters'); advFilters.classList.toggle('hidden'); } // Filter event listeners document.getElementById('filter-category').addEventListener('change', (e) => { filters.category_id = e.target.value; currentPage = 1; loadTransactions(); }); document.getElementById('filter-start-date').addEventListener('change', (e) => { filters.start_date = e.target.value; currentPage = 1; loadTransactions(); }); document.getElementById('filter-end-date').addEventListener('change', (e) => { filters.end_date = e.target.value; currentPage = 1; loadTransactions(); }); document.getElementById('filter-search').addEventListener('input', (e) => { filters.search = e.target.value; currentPage = 1; loadTransactions(); }); // More filters button document.getElementById('more-filters-btn').addEventListener('click', toggleAdvancedFilters); // Date filter button (same as more filters for now) document.getElementById('date-filter-btn').addEventListener('click', toggleAdvancedFilters); // Export CSV document.getElementById('export-csv-btn').addEventListener('click', () => { window.location.href = '/api/expenses/export/csv'; }); // Import CSV document.getElementById('import-csv-btn').addEventListener('click', () => { document.getElementById('csv-file-input').click(); }); document.getElementById('csv-file-input').addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return; const formData = new FormData(); formData.append('file', file); try { const result = await apiCall('/api/expenses/import/csv', { method: 'POST', body: formData }); const importedText = window.getTranslation ? window.getTranslation('transactions.imported', 'Imported') : 'Imported'; const transactionsText = window.getTranslation ? window.getTranslation('transactions.importSuccess', 'transactions') : 'transactions'; showToast(`${importedText} ${result.imported} ${transactionsText}`, 'success'); if (result.errors.length > 0) { console.warn('Import errors:', result.errors); } loadTransactions(); } catch (error) { console.error('Failed to import CSV:', error); } e.target.value = ''; // Reset file input }); // Receipt Viewer const receiptModal = document.getElementById('receipt-modal'); const receiptContent = document.getElementById('receipt-content'); const closeReceiptModal = document.getElementById('close-receipt-modal'); function viewReceipt(receiptPath) { const fileExt = receiptPath.split('.').pop().toLowerCase(); if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(fileExt)) { // Display image receiptContent.innerHTML = `Receipt`; } else if (fileExt === 'pdf') { // Display PDF receiptContent.innerHTML = ``; } else { // Unsupported format - provide download link receiptContent.innerHTML = `
description

Preview not available

${window.getTranslation ? window.getTranslation('transactions.downloadReceipt', 'Download Receipt') : 'Download Receipt'}
`; } receiptModal.classList.remove('hidden'); } closeReceiptModal.addEventListener('click', () => { receiptModal.classList.add('hidden'); receiptContent.innerHTML = ''; }); // Close modal on outside click receiptModal.addEventListener('click', (e) => { if (e.target === receiptModal) { receiptModal.classList.add('hidden'); receiptContent.innerHTML = ''; } }); // Expense Modal Event Listeners const expenseModal = document.getElementById('expense-modal'); const addExpenseBtn = document.getElementById('add-expense-btn'); const closeExpenseModal = document.getElementById('close-expense-modal'); const expenseForm = document.getElementById('expense-form'); // Open modal for adding new expense addExpenseBtn.addEventListener('click', () => { // Reset for add mode currentExpenseId = null; currentReceiptPath = null; expenseForm.reset(); // Update modal title const modalTitle = document.getElementById('expense-modal-title'); modalTitle.textContent = window.getTranslation ? window.getTranslation('modal.add_expense', 'Add Expense') : 'Add Expense'; // Update submit button const submitBtn = document.getElementById('expense-submit-btn'); submitBtn.textContent = window.getTranslation ? window.getTranslation('actions.save', 'Save Expense') : 'Save Expense'; // Hide receipt info document.getElementById('current-receipt-info').classList.add('hidden'); // Load categories and set today's date loadCategoriesForModal(); const dateInput = expenseForm.querySelector('[name="date"]'); dateInput.value = new Date().toISOString().split('T')[0]; // Show modal expenseModal.classList.remove('hidden'); }); // Close modal closeExpenseModal.addEventListener('click', () => { expenseModal.classList.add('hidden'); expenseForm.reset(); currentExpenseId = null; currentReceiptPath = null; }); // Close modal on outside click expenseModal.addEventListener('click', (e) => { if (e.target === expenseModal) { expenseModal.classList.add('hidden'); expenseForm.reset(); currentExpenseId = null; currentReceiptPath = null; } }); // Submit expense form (handles both add and edit) expenseForm.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(expenseForm); // Convert tags to array const tagsString = formData.get('tags'); if (tagsString) { const tags = tagsString.split(',').map(t => t.trim()).filter(t => t); formData.set('tags', JSON.stringify(tags)); } else { formData.set('tags', JSON.stringify([])); } // Convert date to ISO format const date = new Date(formData.get('date')); formData.set('date', date.toISOString()); // If no file selected in edit mode, remove the empty file field const receiptFile = formData.get('receipt'); if (!receiptFile || receiptFile.size === 0) { formData.delete('receipt'); } try { let result; if (currentExpenseId) { // Edit mode - use PUT result = await apiCall(`/api/expenses/${currentExpenseId}`, { method: 'PUT', body: formData }); const successMsg = window.getTranslation ? window.getTranslation('transactions.updated', 'Transaction updated successfully!') : 'Transaction updated successfully!'; showToast(successMsg, 'success'); } else { // Add mode - use POST result = await apiCall('/api/expenses/', { method: 'POST', body: formData }); const successMsg = window.getTranslation ? window.getTranslation('dashboard.expenseAdded', 'Expense added successfully!') : 'Expense added successfully!'; showToast(successMsg, 'success'); } if (result.success) { expenseModal.classList.add('hidden'); expenseForm.reset(); currentExpenseId = null; currentReceiptPath = null; loadTransactions(); } } catch (error) { console.error('Failed to save expense:', error); const errorMsg = window.getTranslation ? window.getTranslation('common.error', 'An error occurred') : 'An error occurred'; showToast(errorMsg, 'error'); } }); // Initialize document.addEventListener('DOMContentLoaded', async () => { await loadUserCurrency(); loadTransactions(); loadCategoriesFilter(); });