fina/app/static/js/notifications.js

265 lines
8.7 KiB
JavaScript
Raw Normal View History

2025-12-26 00:52:56 +00:00
/**
* 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);