/** * Budget Alerts Dashboard Module * Displays budget warnings, progress bars, and alerts */ class BudgetDashboard { constructor() { this.budgetData = null; this.refreshInterval = null; } /** * Initialize budget dashboard */ async init() { await this.loadBudgetStatus(); this.renderBudgetBanner(); this.attachEventListeners(); // Refresh every 5 minutes this.refreshInterval = setInterval(() => { this.loadBudgetStatus(); }, 5 * 60 * 1000); } /** * Load budget status from API */ async loadBudgetStatus() { try { this.budgetData = await window.apiCall('/api/budget/status', 'GET'); this.renderBudgetBanner(); this.updateCategoryBudgets(); } catch (error) { console.error('Error loading budget status:', error); } } /** * Render budget alert banner at top of dashboard */ renderBudgetBanner() { const existingBanner = document.getElementById('budgetAlertBanner'); if (existingBanner) { existingBanner.remove(); } if (!this.budgetData || !this.budgetData.active_alerts || this.budgetData.active_alerts.length === 0) { return; } const mostSevere = this.budgetData.active_alerts[0]; const banner = document.createElement('div'); banner.id = 'budgetAlertBanner'; banner.className = `mb-6 rounded-lg p-4 ${this.getBannerClass(mostSevere.level)}`; let message = ''; let icon = ''; switch (mostSevere.level) { case 'warning': icon = 'warning'; break; case 'danger': case 'exceeded': icon = 'error'; break; } if (mostSevere.type === 'overall') { message = window.getTranslation('budget.overallWarning') .replace('{percentage}', mostSevere.percentage.toFixed(0)) .replace('{spent}', window.formatCurrency(mostSevere.spent)) .replace('{budget}', window.formatCurrency(mostSevere.budget)); } else if (mostSevere.type === 'category') { message = window.getTranslation('budget.categoryWarning') .replace('{category}', mostSevere.category_name) .replace('{percentage}', mostSevere.percentage.toFixed(0)) .replace('{spent}', window.formatCurrency(mostSevere.spent)) .replace('{budget}', window.formatCurrency(mostSevere.budget)); } banner.innerHTML = `
${icon}

${window.getTranslation('budget.alert')}

${message}

${this.budgetData.active_alerts.length > 1 ? ` ` : ''}
`; // Insert at the top of main content const mainContent = document.querySelector('main') || document.querySelector('.container'); if (mainContent && mainContent.firstChild) { mainContent.insertBefore(banner, mainContent.firstChild); } } /** * Get banner CSS classes based on alert level */ getBannerClass(level) { switch (level) { case 'warning': return 'bg-yellow-100 text-yellow-800 border border-yellow-300'; case 'danger': return 'bg-orange-100 text-orange-800 border border-orange-300'; case 'exceeded': return 'bg-red-100 text-red-800 border border-red-300'; default: return 'bg-blue-100 text-blue-800 border border-blue-300'; } } /** * Dismiss budget banner (hide for 1 hour) */ dismissBanner() { const banner = document.getElementById('budgetAlertBanner'); if (banner) { banner.remove(); } // Store dismissal timestamp localStorage.setItem('budgetBannerDismissed', Date.now().toString()); } /** * Check if banner should be shown (not dismissed in last hour) */ shouldShowBanner() { const dismissed = localStorage.getItem('budgetBannerDismissed'); if (!dismissed) return true; const dismissedTime = parseInt(dismissed); const oneHour = 60 * 60 * 1000; return Date.now() - dismissedTime > oneHour; } /** * Show modal with all active alerts */ showAllAlerts() { if (!this.budgetData || !this.budgetData.active_alerts) return; const modal = document.createElement('div'); modal.id = 'allAlertsModal'; modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4'; const alertsList = this.budgetData.active_alerts.map(alert => { let message = ''; if (alert.type === 'overall') { message = window.getTranslation('budget.overallWarning') .replace('{percentage}', alert.percentage.toFixed(0)) .replace('{spent}', window.formatCurrency(alert.spent)) .replace('{budget}', window.formatCurrency(alert.budget)); } else { message = window.getTranslation('budget.categoryWarning') .replace('{category}', alert.category_name) .replace('{percentage}', alert.percentage.toFixed(0)) .replace('{spent}', window.formatCurrency(alert.spent)) .replace('{budget}', window.formatCurrency(alert.budget)); } return `
${alert.category_name || window.getTranslation('budget.monthlyBudget')}
${message}
${this.renderProgressBar(alert.percentage, alert.level)}
`; }).join(''); modal.innerHTML = `

${window.getTranslation('budget.activeAlerts')}

${alertsList}
`; document.body.appendChild(modal); // Close on backdrop click modal.addEventListener('click', (e) => { if (e.target === modal) { modal.remove(); } }); } /** * Render a progress bar for budget percentage */ renderProgressBar(percentage, level) { const cappedPercentage = Math.min(percentage, 100); let colorClass = 'bg-green-500'; switch (level) { case 'warning': colorClass = 'bg-yellow-500'; break; case 'danger': colorClass = 'bg-orange-500'; break; case 'exceeded': colorClass = 'bg-red-500'; break; } return `
${percentage.toFixed(0)}%
`; } /** * Update category cards with budget information */ updateCategoryBudgets() { if (!this.budgetData || !this.budgetData.categories) return; this.budgetData.categories.forEach(category => { const categoryCard = document.querySelector(`[data-category-id="${category.id}"]`); if (!categoryCard) return; // Check if budget info already exists let budgetInfo = categoryCard.querySelector('.budget-info'); if (!budgetInfo) { budgetInfo = document.createElement('div'); budgetInfo.className = 'budget-info mt-2'; categoryCard.appendChild(budgetInfo); } if (category.budget_status && category.budget_status.budget) { const status = category.budget_status; budgetInfo.innerHTML = `
${window.formatCurrency(status.spent)} / ${window.formatCurrency(status.budget)}
${this.renderProgressBar(status.percentage, status.alert_level)} `; } else { budgetInfo.innerHTML = ''; } }); } /** * Attach event listeners */ attachEventListeners() { // Listen for expense changes to refresh budget document.addEventListener('expenseCreated', () => { this.loadBudgetStatus(); }); document.addEventListener('expenseUpdated', () => { this.loadBudgetStatus(); }); document.addEventListener('expenseDeleted', () => { this.loadBudgetStatus(); }); } /** * Cleanup on destroy */ destroy() { if (this.refreshInterval) { clearInterval(this.refreshInterval); } } } // Create global instance window.budgetDashboard = new BudgetDashboard(); // Initialize on dashboard page if (window.location.pathname === '/dashboard' || window.location.pathname === '/') { document.addEventListener('DOMContentLoaded', () => { window.budgetDashboard.init(); }); }