Add: SignalR real-time notifications for admin panel
- Created NotificationHub for instant browser notifications - Updated CryptoPaymentService to broadcast via SignalR - Added JavaScript client with toast notifications - Works with custom SSL certificates (no FCM dependency) - Automatic reconnection with exponential backoff - Notification sound and visual indicators - Bypasses all Web Push SSL certificate issues
This commit is contained in:
parent
b8390162d9
commit
be91b3efd7
@ -159,6 +159,9 @@
|
|||||||
<script src="/js/blazor-integration.js"></script>
|
<script src="/js/blazor-integration.js"></script>
|
||||||
<script src="/js/pwa.js"></script>
|
<script src="/js/pwa.js"></script>
|
||||||
<script src="/js/notifications.js"></script>
|
<script src="/js/notifications.js"></script>
|
||||||
|
<!-- SignalR Real-Time Notifications -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@@microsoft/signalr@@latest/dist/browser/signalr.min.js"></script>
|
||||||
|
<script src="/js/signalr-notifications.js"></script>
|
||||||
<script src="/js/modern-mobile.js"></script>
|
<script src="/js/modern-mobile.js"></script>
|
||||||
@await RenderSectionAsync("Scripts", required: false)
|
@await RenderSectionAsync("Scripts", required: false)
|
||||||
<!-- Mobile Bottom Navigation -->
|
<!-- Mobile Bottom Navigation -->
|
||||||
|
|||||||
39
LittleShop/Hubs/NotificationHub.cs
Normal file
39
LittleShop/Hubs/NotificationHub.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace LittleShop.Hubs;
|
||||||
|
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
|
public class NotificationHub : Hub
|
||||||
|
{
|
||||||
|
private readonly ILogger<NotificationHub> _logger;
|
||||||
|
|
||||||
|
public NotificationHub(ILogger<NotificationHub> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task OnConnectedAsync()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Admin user connected to notification hub: {ConnectionId}", Context.ConnectionId);
|
||||||
|
await base.OnConnectedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Admin user disconnected from notification hub: {ConnectionId}", Context.ConnectionId);
|
||||||
|
await base.OnDisconnectedAsync(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client can call this to test notifications
|
||||||
|
public async Task SendTestNotification()
|
||||||
|
{
|
||||||
|
await Clients.Caller.SendAsync("ReceiveNotification", new
|
||||||
|
{
|
||||||
|
title = "Test Notification",
|
||||||
|
message = "This is a test notification from SignalR!",
|
||||||
|
type = "info",
|
||||||
|
timestamp = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -397,8 +397,9 @@ app.MapBlazorHub(); // Map Blazor Server hub
|
|||||||
app.MapRazorPages(); // Enable Razor Pages for Blazor
|
app.MapRazorPages(); // Enable Razor Pages for Blazor
|
||||||
app.MapFallbackToPage("/blazor/{*path}", "/_Host"); // Fallback for all Blazor routes
|
app.MapFallbackToPage("/blazor/{*path}", "/_Host"); // Fallback for all Blazor routes
|
||||||
|
|
||||||
// Map SignalR hub
|
// Map SignalR hubs
|
||||||
app.MapHub<LittleShop.Hubs.ActivityHub>("/activityHub");
|
app.MapHub<LittleShop.Hubs.ActivityHub>("/activityHub");
|
||||||
|
app.MapHub<LittleShop.Hubs.NotificationHub>("/notificationHub");
|
||||||
|
|
||||||
// Health check endpoint
|
// Health check endpoint
|
||||||
app.MapHealthChecks("/health");
|
app.MapHealthChecks("/health");
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using LittleShop.Data;
|
using LittleShop.Data;
|
||||||
using LittleShop.Models;
|
using LittleShop.Models;
|
||||||
using LittleShop.DTOs;
|
using LittleShop.DTOs;
|
||||||
using LittleShop.Enums;
|
using LittleShop.Enums;
|
||||||
|
using LittleShop.Hubs;
|
||||||
|
|
||||||
namespace LittleShop.Services;
|
namespace LittleShop.Services;
|
||||||
|
|
||||||
@ -14,6 +16,7 @@ public class CryptoPaymentService : ICryptoPaymentService
|
|||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly IPushNotificationService _pushNotificationService;
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
private readonly ITeleBotMessagingService _teleBotMessagingService;
|
private readonly ITeleBotMessagingService _teleBotMessagingService;
|
||||||
|
private readonly IHubContext<NotificationHub> _notificationHub;
|
||||||
|
|
||||||
public CryptoPaymentService(
|
public CryptoPaymentService(
|
||||||
LittleShopContext context,
|
LittleShopContext context,
|
||||||
@ -21,7 +24,8 @@ public class CryptoPaymentService : ICryptoPaymentService
|
|||||||
ILogger<CryptoPaymentService> logger,
|
ILogger<CryptoPaymentService> logger,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IPushNotificationService pushNotificationService,
|
IPushNotificationService pushNotificationService,
|
||||||
ITeleBotMessagingService teleBotMessagingService)
|
ITeleBotMessagingService teleBotMessagingService,
|
||||||
|
IHubContext<NotificationHub> notificationHub)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_silverPayService = silverPayService;
|
_silverPayService = silverPayService;
|
||||||
@ -29,8 +33,9 @@ public class CryptoPaymentService : ICryptoPaymentService
|
|||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_pushNotificationService = pushNotificationService;
|
_pushNotificationService = pushNotificationService;
|
||||||
_teleBotMessagingService = teleBotMessagingService;
|
_teleBotMessagingService = teleBotMessagingService;
|
||||||
|
_notificationHub = notificationHub;
|
||||||
|
|
||||||
_logger.LogInformation("CryptoPaymentService initialized with SilverPAY");
|
_logger.LogInformation("CryptoPaymentService initialized with SilverPAY and SignalR notifications");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CryptoPaymentDto> CreatePaymentAsync(Guid orderId, CryptoCurrency currency)
|
public async Task<CryptoPaymentDto> CreatePaymentAsync(Guid orderId, CryptoCurrency currency)
|
||||||
@ -215,13 +220,26 @@ public class CryptoPaymentService : ICryptoPaymentService
|
|||||||
var title = "💰 Payment Confirmed";
|
var title = "💰 Payment Confirmed";
|
||||||
var body = $"Order #{orderId.ToString()[..8]} payment of £{amount:F2} confirmed. Ready for acceptance.";
|
var body = $"Order #{orderId.ToString()[..8]} payment of £{amount:F2} confirmed. Ready for acceptance.";
|
||||||
|
|
||||||
// Send push notification to admin users
|
// Send SignalR real-time notification to connected admin users
|
||||||
|
await _notificationHub.Clients.All.SendAsync("ReceiveNotification", new
|
||||||
|
{
|
||||||
|
title = title,
|
||||||
|
message = body,
|
||||||
|
type = "payment",
|
||||||
|
orderId = orderId,
|
||||||
|
amount = amount,
|
||||||
|
timestamp = DateTime.UtcNow,
|
||||||
|
icon = "💰",
|
||||||
|
url = $"/Admin/Orders/Details/{orderId}"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send push notification to admin users (may not work with custom CA)
|
||||||
await _pushNotificationService.SendOrderNotificationAsync(orderId, title, body);
|
await _pushNotificationService.SendOrderNotificationAsync(orderId, title, body);
|
||||||
|
|
||||||
// Send TeleBot message to customer
|
// Send TeleBot message to customer
|
||||||
await _teleBotMessagingService.SendPaymentConfirmedAsync(orderId);
|
await _teleBotMessagingService.SendPaymentConfirmedAsync(orderId);
|
||||||
|
|
||||||
_logger.LogInformation("Sent payment confirmation notifications for order {OrderId} (Admin + Customer)", orderId);
|
_logger.LogInformation("Sent payment confirmation notifications for order {OrderId} (SignalR + Push + Telegram)", orderId);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
206
LittleShop/wwwroot/js/signalr-notifications.js
Normal file
206
LittleShop/wwwroot/js/signalr-notifications.js
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
// 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 = `
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<div class="fs-2 me-3">${icon}</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="alert-heading mb-1">${notification.title}</h6>
|
||||||
|
<p class="mb-2">${notification.message}</p>
|
||||||
|
${url !== '#' ? `
|
||||||
|
<a href="${url}" class="btn btn-sm btn-primary">View Order</a>
|
||||||
|
` : ''}
|
||||||
|
<small class="text-muted d-block mt-1">${this.formatTimestamp(notification.timestamp)}</small>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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;
|
||||||
Loading…
Reference in New Issue
Block a user