// SignalR Real-Time Notifications for LittleShop Admin // Connects to NotificationHub and displays toast notifications class SignalRNotificationManager { constructor() { this.connection = null; this.isConnected = false; this.reconnectAttempts = 0; this.maxReconnectAttempts = 10; this.init(); } async init() { console.log('SignalR Notifications: Initializing...'); try { // Create SignalR connection this.connection = new signalR.HubConnectionBuilder() .withUrl("/notificationHub") .withAutomaticReconnect({ nextRetryDelayInMilliseconds: (retryContext) => { // Exponential backoff: 0s, 2s, 10s, 30s, then 30s if (retryContext.elapsedMilliseconds < 60000) { return Math.min(1000 * Math.pow(2, retryContext.previousRetryCount), 30000); } return 30000; } }) .configureLogging(signalR.LogLevel.Information) .build(); // Set up event handlers this.setupEventHandlers(); // Start connection await this.start(); } catch (error) { console.error('SignalR Notifications: Failed to initialize:', error); } } setupEventHandlers() { // Handle incoming notifications this.connection.on("ReceiveNotification", (notification) => { console.log('SignalR Notifications: Received:', notification); this.showNotification(notification); }); // Connection lifecycle events this.connection.onclose((error) => { this.isConnected = false; console.log('SignalR Notifications: Connection closed', error); this.updateConnectionStatus('disconnected'); }); this.connection.onreconnecting((error) => { console.log('SignalR Notifications: Reconnecting...', error); this.updateConnectionStatus('reconnecting'); }); this.connection.onreconnected((connectionId) => { this.isConnected = true; console.log('SignalR Notifications: Reconnected', connectionId); this.updateConnectionStatus('connected'); this.reconnectAttempts = 0; }); } async start() { try { await this.connection.start(); this.isConnected = true; console.log('SignalR Notifications: Connected successfully'); this.updateConnectionStatus('connected'); } catch (error) { console.error('SignalR Notifications: Failed to connect:', error); this.updateConnectionStatus('disconnected'); // Retry connection setTimeout(() => this.start(), 5000); } } showNotification(notification) { // Create toast notification const toast = document.createElement('div'); toast.className = `alert alert-info alert-dismissible position-fixed`; toast.style.cssText = ` top: 80px; right: 20px; z-index: 9999; min-width: 350px; max-width: 500px; box-shadow: 0 8px 24px rgba(0,0,0,0.2); animation: slideInRight 0.3s ease-out; `; // Notification icon based on type const iconMap = { 'payment': '💰', 'order': 'đŸ“Ļ', 'info': 'â„šī¸', 'warning': 'âš ī¸', 'success': '✅', 'error': '❌' }; const icon = notification.icon || iconMap[notification.type] || '🔔'; const url = notification.url || '#'; toast.innerHTML = `
${icon}
${notification.title}

${notification.message}

${url !== '#' ? ` View Order ` : ''} ${this.formatTimestamp(notification.timestamp)}
`; document.body.appendChild(toast); // Play notification sound this.playNotificationSound(); // Auto-remove after 10 seconds setTimeout(() => { if (toast.parentNode) { toast.remove(); } }, 10000); // Update notification badge if it exists this.updateNotificationBadge(); } formatTimestamp(timestamp) { const date = new Date(timestamp); const now = new Date(); const diff = Math.floor((now - date) / 1000); // seconds if (diff < 60) return 'Just now'; if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; return date.toLocaleString(); } playNotificationSound() { try { // Simple beep sound (you can replace with a custom sound file) const audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBShzw+7bjT8JElyv5e2nVhYQTKXi0adkHAYnfcHr4JBNDA9PotTtqV4bCkSa5O/IbCcMKHTH9N+ROwgYarfs5o5MDQ5Pm+Xzr2YeCCF5wOnlkTkKG2a16+2iUxoOSKLe8bp4Kgk0fMnw24k6EBxmsuvsoFMYDk+m4PO2eCQMOXjE7t2OPhkZaK7n7aJbGw1Fn+Hxu3ooDzZ8yO/ciD8PG2+36OybUhoPSpzg8bhwKQo2e8bs3Y5CDQxVqerurF4bCkGc5PG9eywNNHnH9N+PQQ8bZ7Tr7aVVGQ5Fn+Hxtn0qDTZ6x+zelkAMEF216+6kVxwLQqHk8rhvKQs4esr13o9CDhxntujuoFYYDUad4vK9fS0OOX7K9N6OQw0QWK7n7qNYGw5Ip+LxvYAuETl+zPTfkUUNElyy6e6mWBsNRp/j8biCKg88fs/z4I9ADQxbsOXupVYaDkSg4vK3gzULPIHP8N6PTg0NXKvn7aRUFwxNoeXxt4QuCzh+zu/djkAPDVip5+2kVhgMSp/i8bZ8LQo5fc7w3Y5ADAxaqOftpVYZDUmg4/K4gSwKOn3O8N2OQAwMWqjn7aVWGQ1Joezi8biALgo6fc7w3Y5ADA1aqejtpFYYDUmf4/K4gSwJOn7O8N2PQA0MWqro7aRVGg1Jn+Pyu4UuCjp+zfDdjkAPDVqp6O2lVxoOSaHk8rqDLgo4fc3w3I1ADAxZqejto1caDUqh5PG5gjAJPn/N8d6MQQ0NWqrn7aNWGQxKn+Txt4EqCjl+zPHdjkAMDFuq6O2kVhgMTJ/i8rZ8KQo4fc3w3Y5ADA1bq+jtpFYYDU2f4vK2fCkKOH3N8N2OQAwLWqrn7aRWGQ1JoOPyuIArCjl+zPHcj0EPDVqr5+2kVhkLSZ/i8rZ7LQs5fs3w3I1ADAxdqufupFYYDUie4/K4gCwLOX3M8N2OQAwLXKvn7aRWGg1IoOPyuH8rCTl+zfDdjj8MDFuo6O2kVhkLSZ/i8rZ7LQo5fc3w3I1ADAxcq+jtpFYaDUie4/K5gCsJOX7M8d6OQAwLW6ro7aRVGQ1IoOPyt4ArCTl+zfHejT8MDFup6O2jVxkNSaDi8rd9Kgo4fs/w3Y4/DAxcqejvo1YaDUqg4vK3fSoKOH7P8NyNPwsLW6nn7aRXGQ1Jn+LytXwuCzh9zvDcjkAKC1yr5+2jVxkNSJ/i8rZ+Kgo5fs3w3I4/CwxbqefupFcZDEme4vK3fCoJOX7N8N2PQAsMXKnn7aRXGQ1IoOLytX4pCTh+zfDbjj8LC1qq6O2kVxkNSJ/i8rV+Kgo5fs3w3I4/DAtcqefto1caDEie4vK2fioJOX3N8N2OQAsMXKro7aNXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV+Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kwo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kwo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8='); audio.volume = 0.3; audio.play().catch(e => console.log('Cannot play sound:', e)); } catch (error) { console.log('Notification sound error:', error); } } updateConnectionStatus(status) { // Update UI status indicator if exists const statusIndicator = document.getElementById('signalr-connection-status'); if (statusIndicator) { statusIndicator.className = `badge bg-${status === 'connected' ? 'success' : status === 'reconnecting' ? 'warning' : 'danger'}`; statusIndicator.textContent = status.charAt(0).toUpperCase() + status.slice(1); } console.log('SignalR Notifications: Status:', status); } updateNotificationBadge() { const badge = document.getElementById('notification-badge'); if (badge) { const currentCount = parseInt(badge.textContent) || 0; badge.textContent = currentCount + 1; badge.style.display = 'inline'; } } async sendTestNotification() { if (!this.isConnected) { console.error('SignalR Notifications: Not connected'); return; } try { await this.connection.invoke("SendTestNotification"); console.log('SignalR Notifications: Test notification requested'); } catch (error) { console.error('SignalR Notifications: Failed to send test notification:', error); } } } // Initialize on page load document.addEventListener('DOMContentLoaded', () => { window.signalRNotifications = new SignalRNotificationManager(); }); // Export for global access window.SignalRNotificationManager = SignalRNotificationManager;