265 lines
8.7 KiB
JavaScript
265 lines
8.7 KiB
JavaScript
|
|
/**
|
||
|
|
* Budget Notifications Module
|
||
|
|
* Handles PWA push notifications for budget alerts
|
||
|
|
*/
|
||
|
|
|
||
|
|
class BudgetNotifications {
|
||
|
|
constructor() {
|
||
|
|
this.notificationPermission = 'default';
|
||
|
|
this.checkPermission();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check current notification permission status
|
||
|
|
*/
|
||
|
|
checkPermission() {
|
||
|
|
if ('Notification' in window) {
|
||
|
|
this.notificationPermission = Notification.permission;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Request notification permission from user
|
||
|
|
*/
|
||
|
|
async requestPermission() {
|
||
|
|
if (!('Notification' in window)) {
|
||
|
|
console.warn('This browser does not support notifications');
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.notificationPermission === 'granted') {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const permission = await Notification.requestPermission();
|
||
|
|
this.notificationPermission = permission;
|
||
|
|
|
||
|
|
if (permission === 'granted') {
|
||
|
|
// Store permission preference
|
||
|
|
localStorage.setItem('budgetNotificationsEnabled', 'true');
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error requesting notification permission:', error);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Show a budget alert notification
|
||
|
|
*/
|
||
|
|
async showBudgetAlert(alert) {
|
||
|
|
if (this.notificationPermission !== 'granted') {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const icon = '/static/icons/icon-192x192.png';
|
||
|
|
const badge = '/static/icons/icon-72x72.png';
|
||
|
|
|
||
|
|
let title = '';
|
||
|
|
let body = '';
|
||
|
|
let tag = `budget-alert-${alert.type}`;
|
||
|
|
|
||
|
|
switch (alert.type) {
|
||
|
|
case 'category':
|
||
|
|
title = window.getTranslation('budget.categoryAlert');
|
||
|
|
body = window.getTranslation('budget.categoryAlertMessage')
|
||
|
|
.replace('{category}', alert.category_name)
|
||
|
|
.replace('{percentage}', alert.percentage.toFixed(0));
|
||
|
|
tag = `budget-category-${alert.category_id}`;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'overall':
|
||
|
|
title = window.getTranslation('budget.overallAlert');
|
||
|
|
body = window.getTranslation('budget.overallAlertMessage')
|
||
|
|
.replace('{percentage}', alert.percentage.toFixed(0));
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'exceeded':
|
||
|
|
title = window.getTranslation('budget.exceededAlert');
|
||
|
|
body = window.getTranslation('budget.exceededAlertMessage')
|
||
|
|
.replace('{category}', alert.category_name);
|
||
|
|
tag = `budget-exceeded-${alert.category_id}`;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
const options = {
|
||
|
|
body: body,
|
||
|
|
icon: icon,
|
||
|
|
badge: badge,
|
||
|
|
tag: tag, // Prevents duplicate notifications
|
||
|
|
renotify: true,
|
||
|
|
requireInteraction: alert.level === 'danger' || alert.level === 'exceeded',
|
||
|
|
data: {
|
||
|
|
url: alert.type === 'overall' ? '/dashboard' : '/transactions',
|
||
|
|
categoryId: alert.category_id
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Use service worker for better notification handling
|
||
|
|
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
|
||
|
|
navigator.serviceWorker.ready.then(registration => {
|
||
|
|
registration.showNotification(title, options);
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
// Fallback to regular notification
|
||
|
|
const notification = new Notification(title, options);
|
||
|
|
|
||
|
|
notification.onclick = function(event) {
|
||
|
|
event.preventDefault();
|
||
|
|
window.focus();
|
||
|
|
if (options.data.url) {
|
||
|
|
window.location.href = options.data.url;
|
||
|
|
}
|
||
|
|
notification.close();
|
||
|
|
};
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error showing notification:', error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Show weekly spending summary notification
|
||
|
|
*/
|
||
|
|
async showWeeklySummary(summary) {
|
||
|
|
if (this.notificationPermission !== 'granted') {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const icon = '/static/icons/icon-192x192.png';
|
||
|
|
const badge = '/static/icons/icon-72x72.png';
|
||
|
|
|
||
|
|
const title = window.getTranslation('budget.weeklySummary');
|
||
|
|
const spent = window.formatCurrency(summary.current_week_spent);
|
||
|
|
const change = summary.percentage_change > 0 ? '+' : '';
|
||
|
|
const changeText = `${change}${summary.percentage_change.toFixed(0)}%`;
|
||
|
|
|
||
|
|
const body = window.getTranslation('budget.weeklySummaryMessage')
|
||
|
|
.replace('{spent}', spent)
|
||
|
|
.replace('{change}', changeText)
|
||
|
|
.replace('{category}', summary.top_category);
|
||
|
|
|
||
|
|
const options = {
|
||
|
|
body: body,
|
||
|
|
icon: icon,
|
||
|
|
badge: badge,
|
||
|
|
tag: 'weekly-summary',
|
||
|
|
data: {
|
||
|
|
url: '/reports'
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
|
||
|
|
navigator.serviceWorker.ready.then(registration => {
|
||
|
|
registration.showNotification(title, options);
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
const notification = new Notification(title, options);
|
||
|
|
|
||
|
|
notification.onclick = function(event) {
|
||
|
|
event.preventDefault();
|
||
|
|
window.focus();
|
||
|
|
window.location.href = '/reports';
|
||
|
|
notification.close();
|
||
|
|
};
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error showing weekly summary:', error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if notifications are enabled in settings
|
||
|
|
*/
|
||
|
|
isEnabled() {
|
||
|
|
return localStorage.getItem('budgetNotificationsEnabled') === 'true';
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Enable/disable budget notifications
|
||
|
|
*/
|
||
|
|
async setEnabled(enabled) {
|
||
|
|
if (enabled) {
|
||
|
|
const granted = await this.requestPermission();
|
||
|
|
if (granted) {
|
||
|
|
localStorage.setItem('budgetNotificationsEnabled', 'true');
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
} else {
|
||
|
|
localStorage.setItem('budgetNotificationsEnabled', 'false');
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create global instance
|
||
|
|
window.budgetNotifications = new BudgetNotifications();
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check budget status and show alerts if needed
|
||
|
|
*/
|
||
|
|
async function checkBudgetAlerts() {
|
||
|
|
if (!window.budgetNotifications.isEnabled()) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const data = await window.apiCall('/api/budget/status', 'GET');
|
||
|
|
|
||
|
|
if (data.active_alerts && data.active_alerts.length > 0) {
|
||
|
|
// Show only the most severe alert to avoid spam
|
||
|
|
const mostSevereAlert = data.active_alerts[0];
|
||
|
|
await window.budgetNotifications.showBudgetAlert(mostSevereAlert);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error checking budget alerts:', error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if it's time to show weekly summary
|
||
|
|
* Shows on Monday morning if not shown this week
|
||
|
|
*/
|
||
|
|
async function checkWeeklySummary() {
|
||
|
|
if (!window.budgetNotifications.isEnabled()) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const lastShown = localStorage.getItem('lastWeeklySummaryShown');
|
||
|
|
const now = new Date();
|
||
|
|
const dayOfWeek = now.getDay(); // 0 = Sunday, 1 = Monday
|
||
|
|
|
||
|
|
// Show on Monday (1) between 9 AM and 11 AM
|
||
|
|
if (dayOfWeek === 1 && now.getHours() >= 9 && now.getHours() < 11) {
|
||
|
|
const today = now.toDateString();
|
||
|
|
|
||
|
|
if (lastShown !== today) {
|
||
|
|
try {
|
||
|
|
const data = await window.apiCall('/api/budget/weekly-summary', 'GET');
|
||
|
|
await window.budgetNotifications.showWeeklySummary(data);
|
||
|
|
localStorage.setItem('lastWeeklySummaryShown', today);
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error showing weekly summary:', error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check budget alerts every 30 minutes
|
||
|
|
if (window.budgetNotifications.isEnabled()) {
|
||
|
|
setInterval(checkBudgetAlerts, 30 * 60 * 1000);
|
||
|
|
|
||
|
|
// Check immediately on load
|
||
|
|
setTimeout(checkBudgetAlerts, 5000);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check weekly summary once per hour
|
||
|
|
setInterval(checkWeeklySummary, 60 * 60 * 1000);
|
||
|
|
setTimeout(checkWeeklySummary, 10000);
|