Hoghoghi / frontend /js /notifications.js
Really-amin's picture
Upload 143 files
c636ebf verified
raw
history blame
21.6 kB
/**
* Notification System for Legal Dashboard
* ======================================
*
* Handles real-time notifications, WebSocket connections, and notification management.
*/
class NotificationManager {
constructor() {
this.websocket = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
this.notifications = [];
this.unreadCount = 0;
this.isConnected = false;
this.userId = null;
// Initialize notification elements
this.initElements();
// Start WebSocket connection
this.connectWebSocket();
// Set up periodic cleanup
setInterval(() => this.cleanupExpiredNotifications(), 60000); // Every minute
}
initElements() {
// Create notification container if it doesn't exist
if (!document.getElementById('notification-container')) {
const container = document.createElement('div');
container.id = 'notification-container';
container.className = 'notification-container';
document.body.appendChild(container);
}
// Create notification bell if it doesn't exist
if (!document.getElementById('notification-bell')) {
const bell = document.createElement('div');
bell.id = 'notification-bell';
bell.className = 'notification-bell';
bell.innerHTML = `
<i class="fas fa-bell"></i>
<span class="notification-badge" id="notification-badge">0</span>
`;
bell.addEventListener('click', () => this.toggleNotificationPanel());
document.body.appendChild(bell);
}
// Create notification panel if it doesn't exist
if (!document.getElementById('notification-panel')) {
const panel = document.createElement('div');
panel.id = 'notification-panel';
panel.className = 'notification-panel hidden';
panel.innerHTML = `
<div class="notification-header">
<h3>Notifications</h3>
<button class="close-btn" onclick="notificationManager.closeNotificationPanel()">×</button>
</div>
<div class="notification-list" id="notification-list"></div>
<div class="notification-footer">
<button onclick="notificationManager.markAllAsRead()">Mark All as Read</button>
<button onclick="notificationManager.clearAll()">Clear All</button>
</div>
`;
document.body.appendChild(panel);
}
// Add CSS styles
this.addStyles();
}
addStyles() {
if (!document.getElementById('notification-styles')) {
const styles = document.createElement('style');
styles.id = 'notification-styles';
styles.textContent = `
.notification-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
max-width: 400px;
}
.notification-bell {
position: fixed;
top: 20px;
right: 20px;
background: #007bff;
color: white;
padding: 10px;
border-radius: 50%;
cursor: pointer;
z-index: 10001;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
transition: all 0.3s ease;
}
.notification-bell:hover {
transform: scale(1.1);
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
}
.notification-badge {
position: absolute;
top: -5px;
right: -5px;
background: #dc3545;
color: white;
border-radius: 50%;
padding: 2px 6px;
font-size: 12px;
min-width: 18px;
text-align: center;
}
.notification-panel {
position: fixed;
top: 70px;
right: 20px;
width: 350px;
max-height: 500px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
z-index: 10002;
overflow: hidden;
transition: all 0.3s ease;
}
.notification-panel.hidden {
transform: translateX(100%);
opacity: 0;
}
.notification-header {
padding: 15px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.notification-header h3 {
margin: 0;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #666;
}
.notification-list {
max-height: 350px;
overflow-y: auto;
}
.notification-item {
padding: 15px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background-color 0.2s ease;
}
.notification-item:hover {
background-color: #f8f9fa;
}
.notification-item.unread {
background-color: #e3f2fd;
border-left: 4px solid #2196f3;
}
.notification-title {
font-weight: bold;
margin-bottom: 5px;
color: #333;
}
.notification-message {
color: #666;
font-size: 14px;
margin-bottom: 5px;
}
.notification-meta {
font-size: 12px;
color: #999;
display: flex;
justify-content: space-between;
}
.notification-footer {
padding: 15px;
border-top: 1px solid #eee;
display: flex;
gap: 10px;
}
.notification-footer button {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.notification-footer button:hover {
background: #f8f9fa;
}
.notification-toast {
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
margin-bottom: 10px;
padding: 15px;
border-left: 4px solid #007bff;
animation: slideIn 0.3s ease;
}
.notification-toast.success {
border-left-color: #28a745;
}
.notification-toast.warning {
border-left-color: #ffc107;
}
.notification-toast.error {
border-left-color: #dc3545;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.notification-type-icon {
margin-right: 8px;
}
.notification-type-icon.info { color: #007bff; }
.notification-type-icon.success { color: #28a745; }
.notification-type-icon.warning { color: #ffc107; }
.notification-type-icon.error { color: #dc3545; }
`;
document.head.appendChild(styles);
}
}
connectWebSocket() {
try {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/api/notifications/ws`;
this.websocket = new WebSocket(wsUrl);
this.websocket.onopen = () => {
console.log('WebSocket connected');
this.isConnected = true;
this.reconnectAttempts = 0;
// Send user authentication if available
const token = localStorage.getItem('auth_token');
if (token) {
this.websocket.send(JSON.stringify({
type: 'auth',
token: token
}));
}
};
this.websocket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleNotification(data);
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
this.websocket.onclose = () => {
console.log('WebSocket disconnected');
this.isConnected = false;
this.handleReconnect();
};
this.websocket.onerror = (error) => {
console.error('WebSocket error:', error);
this.isConnected = false;
};
} catch (error) {
console.error('Error connecting to WebSocket:', error);
this.handleReconnect();
}
}
handleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
setTimeout(() => {
this.connectWebSocket();
}, this.reconnectDelay * this.reconnectAttempts);
} else {
console.error('Max reconnection attempts reached');
this.showToast('Connection lost. Please refresh the page.', 'error');
}
}
handleNotification(data) {
if (data.type === 'connection_established') {
console.log('Notification service connected');
this.userId = data.user_id;
return;
}
// Add notification to list
const notification = {
id: data.id || Date.now(),
type: data.type || 'info',
title: data.title || 'Notification',
message: data.message || '',
priority: data.priority || 'medium',
created_at: data.created_at || new Date().toISOString(),
metadata: data.metadata || {},
read: false
};
this.notifications.unshift(notification);
this.unreadCount++;
this.updateBadge();
this.showToast(notification);
this.updateNotificationPanel();
// Play notification sound if enabled
this.playNotificationSound();
}
showToast(notification) {
const container = document.getElementById('notification-container');
const toast = document.createElement('div');
toast.className = `notification-toast ${notification.type}`;
const icon = this.getNotificationIcon(notification.type);
toast.innerHTML = `
<div class="notification-title">
<i class="fas ${icon} notification-type-icon ${notification.type}"></i>
${notification.title}
</div>
<div class="notification-message">${notification.message}</div>
<div class="notification-meta">
<span>${this.formatTime(notification.created_at)}</span>
<span>${notification.priority}</span>
</div>
`;
container.appendChild(toast);
// Auto-remove after 5 seconds
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 5000);
}
getNotificationIcon(type) {
const icons = {
'info': 'fa-info-circle',
'success': 'fa-check-circle',
'warning': 'fa-exclamation-triangle',
'error': 'fa-times-circle',
'upload_complete': 'fa-upload',
'ocr_complete': 'fa-file-text',
'scraping_complete': 'fa-spider',
'system_error': 'fa-exclamation-circle',
'user_activity': 'fa-user'
};
return icons[type] || 'fa-bell';
}
formatTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) { // Less than 1 minute
return 'Just now';
} else if (diff < 3600000) { // Less than 1 hour
const minutes = Math.floor(diff / 60000);
return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
} else if (diff < 86400000) { // Less than 1 day
const hours = Math.floor(diff / 3600000);
return `${hours} hour${hours > 1 ? 's' : ''} ago`;
} else {
return date.toLocaleDateString();
}
}
updateBadge() {
const badge = document.getElementById('notification-badge');
if (badge) {
badge.textContent = this.unreadCount;
badge.style.display = this.unreadCount > 0 ? 'block' : 'none';
}
}
toggleNotificationPanel() {
const panel = document.getElementById('notification-panel');
if (panel) {
panel.classList.toggle('hidden');
if (!panel.classList.contains('hidden')) {
this.updateNotificationPanel();
}
}
}
closeNotificationPanel() {
const panel = document.getElementById('notification-panel');
if (panel) {
panel.classList.add('hidden');
}
}
updateNotificationPanel() {
const list = document.getElementById('notification-list');
if (!list) return;
list.innerHTML = '';
this.notifications.slice(0, 20).forEach(notification => {
const item = document.createElement('div');
item.className = `notification-item ${notification.read ? '' : 'unread'}`;
item.onclick = () => this.markAsRead(notification.id);
const icon = this.getNotificationIcon(notification.type);
item.innerHTML = `
<div class="notification-title">
<i class="fas ${icon} notification-type-icon ${notification.type}"></i>
${notification.title}
</div>
<div class="notification-message">${notification.message}</div>
<div class="notification-meta">
<span>${this.formatTime(notification.created_at)}</span>
<span>${notification.priority}</span>
</div>
`;
list.appendChild(item);
});
}
markAsRead(notificationId) {
const notification = this.notifications.find(n => n.id === notificationId);
if (notification && !notification.read) {
notification.read = true;
this.unreadCount = Math.max(0, this.unreadCount - 1);
this.updateBadge();
this.updateNotificationPanel();
// Send to server
this.sendToServer('mark_read', { notification_id: notificationId });
}
}
markAllAsRead() {
this.notifications.forEach(notification => {
notification.read = true;
});
this.unreadCount = 0;
this.updateBadge();
this.updateNotificationPanel();
// Send to server
this.sendToServer('mark_all_read', {});
}
clearAll() {
this.notifications = [];
this.unreadCount = 0;
this.updateBadge();
this.updateNotificationPanel();
// Send to server
this.sendToServer('clear_all', {});
}
sendToServer(action, data) {
if (this.websocket && this.isConnected) {
this.websocket.send(JSON.stringify({
action: action,
...data
}));
}
}
playNotificationSound() {
// Create audio context for notification sound
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
oscillator.frequency.setValueAtTime(600, audioContext.currentTime + 0.1);
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.2);
} catch (error) {
console.log('Could not play notification sound:', error);
}
}
cleanupExpiredNotifications() {
const now = new Date();
const expired = this.notifications.filter(notification => {
const created = new Date(notification.created_at);
const diff = now - created;
return diff > 24 * 60 * 60 * 1000; // 24 hours
});
expired.forEach(notification => {
const index = this.notifications.indexOf(notification);
if (index > -1) {
this.notifications.splice(index, 1);
}
});
this.updateNotificationPanel();
}
// Public methods for external use
showInfo(title, message, duration = 5000) {
this.showCustomNotification('info', title, message, duration);
}
showSuccess(title, message, duration = 5000) {
this.showCustomNotification('success', title, message, duration);
}
showWarning(title, message, duration = 5000) {
this.showCustomNotification('warning', title, message, duration);
}
showError(title, message, duration = 5000) {
this.showCustomNotification('error', title, message, duration);
}
showCustomNotification(type, title, message, duration) {
const notification = {
id: Date.now(),
type: type,
title: title,
message: message,
priority: 'medium',
created_at: new Date().toISOString(),
metadata: {},
read: false
};
this.notifications.unshift(notification);
this.unreadCount++;
this.updateBadge();
this.showToast(notification);
this.updateNotificationPanel();
if (duration > 0) {
setTimeout(() => {
const index = this.notifications.indexOf(notification);
if (index > -1) {
this.notifications.splice(index, 1);
this.updateNotificationPanel();
}
}, duration);
}
}
}
// Initialize notification manager when DOM is loaded
let notificationManager;
document.addEventListener('DOMContentLoaded', () => {
notificationManager = new NotificationManager();
});
// Global function for external use
window.showNotification = (type, title, message) => {
if (notificationManager) {
notificationManager.showCustomNotification(type, title, message);
}
};