Fixed 89 violations across vendor, admin, and shared JavaScript files: JS-008 (raw fetch → apiClient): - Added postFormData() and getBlob() methods to api-client.js - Updated inventory.js, messages.js to use apiClient.postFormData() - Added noqa for file downloads that need response headers JS-009 (window.showToast → Utils.showToast): - Updated admin/messages.js, notifications.js, vendor/messages.js - Replaced alert() in customers.js JS-006 (async error handling): - Added try/catch to all async init() and reload() methods - Fixed vendor: billing, dashboard, login, messages, onboarding - Fixed shared: feature-store, upgrade-prompts - Fixed admin: all page components JS-005 (init guards): - Added initialization guards to prevent duplicate init() calls - Pattern: if (window._componentInitialized) return; 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
309 lines
11 KiB
JavaScript
309 lines
11 KiB
JavaScript
/**
|
||
* 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 {
|
||
...data(),
|
||
currentPage: 'notifications',
|
||
|
||
// Loading states
|
||
loading: true,
|
||
error: null,
|
||
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() {
|
||
// Guard against multiple initialization
|
||
if (window._adminNotificationsInitialized) return;
|
||
window._adminNotificationsInitialized = true;
|
||
|
||
try {
|
||
notificationsLog.debug('Initializing notifications page');
|
||
await Promise.all([
|
||
this.loadNotifications(),
|
||
this.loadAlertStats()
|
||
]);
|
||
} catch (error) {
|
||
notificationsLog.error('Failed to initialize notifications page:', error);
|
||
} finally {
|
||
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);
|
||
Utils.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);
|
||
|
||
Utils.showToast('Notification marked as read', 'success');
|
||
} catch (error) {
|
||
notificationsLog.error('Failed to mark as read:', error);
|
||
Utils.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;
|
||
|
||
Utils.showToast('All notifications marked as read', 'success');
|
||
} catch (error) {
|
||
notificationsLog.error('Failed to mark all as read:', error);
|
||
Utils.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);
|
||
}
|
||
|
||
Utils.showToast('Notification deleted', 'success');
|
||
} catch (error) {
|
||
notificationsLog.error('Failed to delete notification:', error);
|
||
Utils.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);
|
||
Utils.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++;
|
||
|
||
Utils.showToast('Alert resolved successfully', 'success');
|
||
} catch (error) {
|
||
notificationsLog.error('Failed to resolve alert:', error);
|
||
Utils.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;
|