feat: add admin notification system
- Add AdminNotificationService for notification operations - Enhance notifications API with mark-read, mark-all-read endpoints - Add notifications.html page template - Add notifications.js Alpine component - Add implementation documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
295
static/admin/js/notifications.js
Normal file
295
static/admin/js/notifications.js
Normal file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* Admin Notifications Page
|
||||
*
|
||||
* Handles the notifications management interface including:
|
||||
* - Notifications list with filtering and pagination
|
||||
* - Platform alerts management
|
||||
* - Mark as read, delete, and bulk operations
|
||||
*/
|
||||
|
||||
const notificationsLog = window.LogConfig?.createLogger('NOTIFICATIONS') || console;
|
||||
|
||||
/**
|
||||
* Admin Notifications Component
|
||||
*/
|
||||
function adminNotifications() {
|
||||
return {
|
||||
// Loading states
|
||||
loading: true,
|
||||
loadingNotifications: false,
|
||||
loadingAlerts: false,
|
||||
|
||||
// Tab state
|
||||
activeTab: 'notifications',
|
||||
|
||||
// Notifications state
|
||||
notifications: [],
|
||||
page: 1,
|
||||
skip: 0,
|
||||
limit: 10,
|
||||
stats: {
|
||||
total: 0,
|
||||
unread_count: 0
|
||||
},
|
||||
|
||||
// Notifications filters
|
||||
filters: {
|
||||
priority: '',
|
||||
is_read: ''
|
||||
},
|
||||
|
||||
// Alerts state
|
||||
alerts: [],
|
||||
alertPage: 1,
|
||||
alertSkip: 0,
|
||||
alertLimit: 10,
|
||||
alertStats: {
|
||||
total: 0,
|
||||
active_alerts: 0,
|
||||
critical_alerts: 0,
|
||||
resolved_today: 0,
|
||||
by_type: {},
|
||||
by_severity: {}
|
||||
},
|
||||
|
||||
// Alerts filters
|
||||
alertFilters: {
|
||||
severity: '',
|
||||
is_resolved: ''
|
||||
},
|
||||
|
||||
// Resolve modal state
|
||||
showResolveModal: false,
|
||||
resolvingAlert: null,
|
||||
resolutionNotes: '',
|
||||
|
||||
/**
|
||||
* Initialize component
|
||||
*/
|
||||
async init() {
|
||||
notificationsLog.debug('Initializing notifications page');
|
||||
await Promise.all([
|
||||
this.loadNotifications(),
|
||||
this.loadAlertStats()
|
||||
]);
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// NOTIFICATIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Load notifications with current filters
|
||||
*/
|
||||
async loadNotifications() {
|
||||
this.loadingNotifications = true;
|
||||
try {
|
||||
this.skip = (this.page - 1) * this.limit;
|
||||
const params = new URLSearchParams();
|
||||
params.append('skip', this.skip);
|
||||
params.append('limit', this.limit);
|
||||
|
||||
if (this.filters.priority) params.append('priority', this.filters.priority);
|
||||
if (this.filters.is_read !== '') params.append('is_read', this.filters.is_read);
|
||||
|
||||
const response = await apiClient.get(`/admin/notifications?${params}`);
|
||||
this.notifications = response.notifications || [];
|
||||
this.stats.total = response.total || 0;
|
||||
this.stats.unread_count = response.unread_count || 0;
|
||||
|
||||
notificationsLog.debug(`Loaded ${this.notifications.length} notifications`);
|
||||
} catch (error) {
|
||||
notificationsLog.error('Failed to load notifications:', error);
|
||||
window.showToast?.('Failed to load notifications', 'error');
|
||||
} finally {
|
||||
this.loadingNotifications = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark notification as read
|
||||
*/
|
||||
async markAsRead(notification) {
|
||||
try {
|
||||
await apiClient.put(`/admin/notifications/${notification.id}/read`);
|
||||
|
||||
// Update local state
|
||||
notification.is_read = true;
|
||||
this.stats.unread_count = Math.max(0, this.stats.unread_count - 1);
|
||||
|
||||
window.showToast?.('Notification marked as read', 'success');
|
||||
} catch (error) {
|
||||
notificationsLog.error('Failed to mark as read:', error);
|
||||
window.showToast?.('Failed to mark notification as read', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark all notifications as read
|
||||
*/
|
||||
async markAllAsRead() {
|
||||
try {
|
||||
await apiClient.put('/admin/notifications/mark-all-read');
|
||||
|
||||
// Update local state
|
||||
this.notifications.forEach(n => n.is_read = true);
|
||||
this.stats.unread_count = 0;
|
||||
|
||||
window.showToast?.('All notifications marked as read', 'success');
|
||||
} catch (error) {
|
||||
notificationsLog.error('Failed to mark all as read:', error);
|
||||
window.showToast?.('Failed to mark all as read', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete notification
|
||||
*/
|
||||
async deleteNotification(notificationId) {
|
||||
if (!confirm('Are you sure you want to delete this notification?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.delete(`/admin/notifications/${notificationId}`);
|
||||
|
||||
// Remove from local state
|
||||
const wasUnread = this.notifications.find(n => n.id === notificationId && !n.is_read);
|
||||
this.notifications = this.notifications.filter(n => n.id !== notificationId);
|
||||
this.stats.total = Math.max(0, this.stats.total - 1);
|
||||
if (wasUnread) {
|
||||
this.stats.unread_count = Math.max(0, this.stats.unread_count - 1);
|
||||
}
|
||||
|
||||
window.showToast?.('Notification deleted', 'success');
|
||||
} catch (error) {
|
||||
notificationsLog.error('Failed to delete notification:', error);
|
||||
window.showToast?.('Failed to delete notification', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get notification icon based on type
|
||||
*/
|
||||
getNotificationIcon(type) {
|
||||
const icons = {
|
||||
'import_failure': window.$icon?.('x-circle', 'w-5 h-5') || '❌',
|
||||
'sync_issue': window.$icon?.('refresh', 'w-5 h-5') || '🔄',
|
||||
'vendor_alert': window.$icon?.('exclamation-triangle', 'w-5 h-5') || '⚠️',
|
||||
'system_health': window.$icon?.('heart', 'w-5 h-5') || '💓',
|
||||
'security': window.$icon?.('shield-exclamation', 'w-5 h-5') || '🛡️',
|
||||
'performance': window.$icon?.('chart-bar', 'w-5 h-5') || '📊',
|
||||
'info': window.$icon?.('information-circle', 'w-5 h-5') || 'ℹ️'
|
||||
};
|
||||
return icons[type] || window.$icon?.('bell', 'w-5 h-5') || '🔔';
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// PLATFORM ALERTS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Load platform alerts
|
||||
*/
|
||||
async loadAlerts() {
|
||||
this.loadingAlerts = true;
|
||||
try {
|
||||
this.alertSkip = (this.alertPage - 1) * this.alertLimit;
|
||||
const params = new URLSearchParams();
|
||||
params.append('skip', this.alertSkip);
|
||||
params.append('limit', this.alertLimit);
|
||||
|
||||
if (this.alertFilters.severity) params.append('severity', this.alertFilters.severity);
|
||||
if (this.alertFilters.is_resolved !== '') params.append('is_resolved', this.alertFilters.is_resolved);
|
||||
|
||||
const response = await apiClient.get(`/admin/notifications/alerts?${params}`);
|
||||
this.alerts = response.alerts || [];
|
||||
this.alertStats.total = response.total || 0;
|
||||
this.alertStats.active_alerts = response.active_count || 0;
|
||||
this.alertStats.critical_alerts = response.critical_count || 0;
|
||||
|
||||
notificationsLog.debug(`Loaded ${this.alerts.length} alerts`);
|
||||
} catch (error) {
|
||||
notificationsLog.error('Failed to load alerts:', error);
|
||||
window.showToast?.('Failed to load alerts', 'error');
|
||||
} finally {
|
||||
this.loadingAlerts = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load alert statistics
|
||||
*/
|
||||
async loadAlertStats() {
|
||||
try {
|
||||
const response = await apiClient.get('/admin/notifications/alerts/stats');
|
||||
this.alertStats = {
|
||||
...this.alertStats,
|
||||
total: response.total || 0,
|
||||
active_alerts: response.active || 0,
|
||||
critical_alerts: response.critical || 0,
|
||||
resolved_today: response.resolved_today || 0,
|
||||
by_type: response.by_type || {},
|
||||
by_severity: response.by_severity || {}
|
||||
};
|
||||
} catch (error) {
|
||||
notificationsLog.error('Failed to load alert stats:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolve alert
|
||||
*/
|
||||
async resolveAlert(alert) {
|
||||
const notes = prompt('Resolution notes (optional):');
|
||||
if (notes === null) return; // User cancelled
|
||||
|
||||
try {
|
||||
await apiClient.put(`/admin/notifications/alerts/${alert.id}/resolve`, {
|
||||
resolution_notes: notes
|
||||
});
|
||||
|
||||
// Update local state
|
||||
alert.is_resolved = true;
|
||||
alert.resolution_notes = notes;
|
||||
this.alertStats.active_alerts = Math.max(0, this.alertStats.active_alerts - 1);
|
||||
if (alert.severity === 'critical') {
|
||||
this.alertStats.critical_alerts = Math.max(0, this.alertStats.critical_alerts - 1);
|
||||
}
|
||||
this.alertStats.resolved_today++;
|
||||
|
||||
window.showToast?.('Alert resolved successfully', 'success');
|
||||
} catch (error) {
|
||||
notificationsLog.error('Failed to resolve alert:', error);
|
||||
window.showToast?.('Failed to resolve alert', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
// ============================================================================
|
||||
// HELPERS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Format date for display
|
||||
*/
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diff = Math.floor((now - date) / 1000);
|
||||
|
||||
// Show relative time for recent dates
|
||||
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`;
|
||||
if (diff < 172800) return 'Yesterday';
|
||||
|
||||
// Show full date for older dates
|
||||
return date.toLocaleString();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Make available globally
|
||||
window.adminNotifications = adminNotifications;
|
||||
Reference in New Issue
Block a user