/**
* 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();
});
}