Files
orion/static/admin/js/notifications.js
Samir Boulahtit 265c71f597 fix: resolve all JS architecture violations (JS-005 through JS-009)
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>
2025-12-31 21:32:19 +01:00

309 lines
11 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;