// App Logic for Control de Gastos // Toast notification system function showToast(message, type = 'info') { const container = document.getElementById('toast-container'); if (!container) return; const toast = document.createElement('div'); toast.className = 'toast'; if (type === 'success') { toast.style.borderLeftColor = 'var(--accent-secondary)'; } else if (type === 'danger') { toast.style.borderLeftColor = 'var(--danger)'; } toast.innerHTML = `
${message}
`; container.appendChild(toast); // Trigger animation setTimeout(() => { toast.classList.add('show'); }, 10); // Remove toast after 5 seconds setTimeout(() => { toast.classList.remove('show'); setTimeout(() => { if (container.contains(toast)) { container.removeChild(toast); } }, 300); }, 5000); } // WebSocket connection for real-time updates let wsReconnectAttempts = 0; const maxWsReconnectAttempts = 5; function connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws/expenses/`; // We only connect if we are not on the login page if (document.querySelector('.login-page')) return; const ws = new WebSocket(wsUrl); ws.onopen = function(e) { console.log('WebSocket connected'); wsReconnectAttempts = 0; }; ws.onmessage = function(e) { const data = JSON.parse(e.data); const action = data.action; const expense = data.expense; const tbody = document.getElementById('expenses-table-body'); if (!tbody || !expense) return; if (action === 'created') { const tr = document.createElement('tr'); tr.id = `expense-row-${expense.id}`; tr.innerHTML = ` ${expense.date} ${expense.project} ${expense.category} ${document.querySelector('th:nth-child(4)') && document.querySelector('th:nth-child(4)').textContent === 'Descripción' ? `${expense.description || ''}` : ''} $${expense.amount} ${expense.user} ${document.querySelector('th:nth-child(7)') ? `
Recarga para ver acciones
` : ''} `; // Add animation class tr.style.backgroundColor = 'rgba(0, 212, 170, 0.2)'; tr.style.transition = 'background-color 2s ease'; tbody.insertBefore(tr, tbody.firstChild); // Remove highlight after animation setTimeout(() => { tr.style.backgroundColor = ''; }, 2000); showToast('Nuevo gasto registrado', 'success'); } else if (action === 'updated') { const row = document.getElementById(`expense-row-${expense.id}`); if (row) { // Flash the row row.style.backgroundColor = 'rgba(108, 99, 255, 0.2)'; row.style.transition = 'background-color 2s ease'; setTimeout(() => { row.style.backgroundColor = ''; }, 2000); showToast('Gasto actualizado', 'info'); } } else if (action === 'deleted') { const row = document.getElementById(`expense-row-${expense.id}`); if (row) { row.style.transform = 'translateX(100%)'; row.style.opacity = '0'; row.style.transition = 'all 0.3s ease'; setTimeout(() => { if (row.parentNode) { row.parentNode.removeChild(row); } }, 300); showToast('Gasto eliminado', 'danger'); } } }; ws.onclose = function(e) { console.error('WebSocket closed unexpectedly'); if (wsReconnectAttempts < maxWsReconnectAttempts) { wsReconnectAttempts++; const timeout = Math.min(1000 * Math.pow(2, wsReconnectAttempts), 30000); console.log(`Attempting reconnect in ${timeout}ms...`); setTimeout(connectWebSocket, timeout); } }; } document.addEventListener('DOMContentLoaded', () => { connectWebSocket(); // Convert CSRF token for fetch requests if needed later const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); });