Initial commit
This commit is contained in:
commit
983cee0320
322 changed files with 57174 additions and 0 deletions
264
app/static/js/notifications.js
Normal file
264
app/static/js/notifications.js
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
/**
|
||||
* 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);
|
||||
Loading…
Add table
Add a link
Reference in a new issue