New pages:
- Notifications center: view, mark read, delete, settings modal
- Analytics: period selector, stats overview, feature-gated advanced metrics
Changes:
- Add routes for /vendor/{code}/notifications and /analytics
- Create notifications.js and analytics.js with full functionality
- Create notifications.html and analytics.html templates
- Update sidebar with Analytics link and Notifications in Customers section
- Update vendor-frontend-parity-plan.md to mark Phase 3 complete
Vendor frontend now at ~95% parity with admin.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
275 lines
9.2 KiB
JavaScript
275 lines
9.2 KiB
JavaScript
// static/vendor/js/notifications.js
|
|
/**
|
|
* Vendor notifications center page logic
|
|
* View and manage notifications
|
|
*/
|
|
|
|
const vendorNotificationsLog = window.LogConfig.loggers.vendorNotifications ||
|
|
window.LogConfig.createLogger('vendorNotifications', false);
|
|
|
|
vendorNotificationsLog.info('Loading...');
|
|
|
|
function vendorNotifications() {
|
|
vendorNotificationsLog.info('vendorNotifications() called');
|
|
|
|
return {
|
|
// Inherit base layout state
|
|
...data(),
|
|
|
|
// Set page identifier
|
|
currentPage: 'notifications',
|
|
|
|
// Loading states
|
|
loading: true,
|
|
error: '',
|
|
loadingNotifications: false,
|
|
|
|
// Notifications data
|
|
notifications: [],
|
|
stats: {
|
|
total: 0,
|
|
unread_count: 0
|
|
},
|
|
|
|
// Pagination
|
|
page: 1,
|
|
limit: 20,
|
|
skip: 0,
|
|
|
|
// Filters
|
|
filters: {
|
|
priority: '',
|
|
is_read: ''
|
|
},
|
|
|
|
// Settings
|
|
settings: null,
|
|
showSettingsModal: false,
|
|
settingsForm: {
|
|
email_notifications: true,
|
|
in_app_notifications: true
|
|
},
|
|
|
|
async init() {
|
|
vendorNotificationsLog.info('Notifications init() called');
|
|
|
|
// Guard against multiple initialization
|
|
if (window._vendorNotificationsInitialized) {
|
|
vendorNotificationsLog.warn('Already initialized, skipping');
|
|
return;
|
|
}
|
|
window._vendorNotificationsInitialized = true;
|
|
|
|
try {
|
|
await this.loadNotifications();
|
|
} catch (error) {
|
|
vendorNotificationsLog.error('Init failed:', error);
|
|
this.error = 'Failed to initialize notifications page';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
|
|
vendorNotificationsLog.info('Notifications initialization complete');
|
|
},
|
|
|
|
/**
|
|
* Load notifications with current filters
|
|
*/
|
|
async loadNotifications() {
|
|
this.loadingNotifications = true;
|
|
this.error = '';
|
|
|
|
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.is_read === 'false') {
|
|
params.append('unread_only', 'true');
|
|
}
|
|
|
|
const response = await apiClient.get(`/vendor/${this.vendorCode}/notifications?${params}`);
|
|
|
|
this.notifications = response.notifications || [];
|
|
this.stats.total = response.total || 0;
|
|
this.stats.unread_count = response.unread_count || 0;
|
|
|
|
vendorNotificationsLog.info(`Loaded ${this.notifications.length} notifications`);
|
|
} catch (error) {
|
|
vendorNotificationsLog.error('Failed to load notifications:', error);
|
|
this.error = error.message || 'Failed to load notifications';
|
|
} finally {
|
|
this.loadingNotifications = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Mark notification as read
|
|
*/
|
|
async markAsRead(notification) {
|
|
try {
|
|
await apiClient.put(`/vendor/${this.vendorCode}/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) {
|
|
vendorNotificationsLog.error('Failed to mark as read:', error);
|
|
Utils.showToast(error.message || 'Failed to mark notification as read', 'error');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Mark all notifications as read
|
|
*/
|
|
async markAllAsRead() {
|
|
try {
|
|
await apiClient.put(`/vendor/${this.vendorCode}/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) {
|
|
vendorNotificationsLog.error('Failed to mark all as read:', error);
|
|
Utils.showToast(error.message || '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(`/vendor/${this.vendorCode}/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) {
|
|
vendorNotificationsLog.error('Failed to delete notification:', error);
|
|
Utils.showToast(error.message || 'Failed to delete notification', 'error');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Open settings modal
|
|
*/
|
|
async openSettingsModal() {
|
|
try {
|
|
const response = await apiClient.get(`/vendor/${this.vendorCode}/notifications/settings`);
|
|
this.settingsForm = {
|
|
email_notifications: response.email_notifications !== false,
|
|
in_app_notifications: response.in_app_notifications !== false
|
|
};
|
|
this.showSettingsModal = true;
|
|
} catch (error) {
|
|
vendorNotificationsLog.error('Failed to load settings:', error);
|
|
Utils.showToast(error.message || 'Failed to load notification settings', 'error');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Save notification settings
|
|
*/
|
|
async saveSettings() {
|
|
try {
|
|
await apiClient.put(`/vendor/${this.vendorCode}/notifications/settings`, this.settingsForm);
|
|
Utils.showToast('Notification settings saved', 'success');
|
|
this.showSettingsModal = false;
|
|
} catch (error) {
|
|
vendorNotificationsLog.error('Failed to save settings:', error);
|
|
Utils.showToast(error.message || 'Failed to save settings', 'error');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get notification icon based on type
|
|
*/
|
|
getNotificationIcon(type) {
|
|
const icons = {
|
|
'order_received': 'shopping-cart',
|
|
'order_shipped': 'truck',
|
|
'order_delivered': 'check-circle',
|
|
'low_stock': 'exclamation-triangle',
|
|
'import_complete': 'cloud-download',
|
|
'import_failed': 'x-circle',
|
|
'team_invite': 'user-plus',
|
|
'payment_received': 'credit-card',
|
|
'system': 'cog'
|
|
};
|
|
return icons[type] || 'bell';
|
|
},
|
|
|
|
/**
|
|
* Get priority color class
|
|
*/
|
|
getPriorityClass(priority) {
|
|
const classes = {
|
|
'critical': 'bg-red-100 text-red-600 dark:bg-red-900 dark:text-red-300',
|
|
'high': 'bg-orange-100 text-orange-600 dark:bg-orange-900 dark:text-orange-300',
|
|
'normal': 'bg-blue-100 text-blue-600 dark:bg-blue-900 dark:text-blue-300',
|
|
'low': 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300'
|
|
};
|
|
return classes[priority] || classes['normal'];
|
|
},
|
|
|
|
/**
|
|
* 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.toLocaleDateString();
|
|
},
|
|
|
|
// Pagination methods
|
|
get totalPages() {
|
|
return Math.ceil(this.stats.total / this.limit);
|
|
},
|
|
|
|
prevPage() {
|
|
if (this.page > 1) {
|
|
this.page--;
|
|
this.loadNotifications();
|
|
}
|
|
},
|
|
|
|
nextPage() {
|
|
if (this.page < this.totalPages) {
|
|
this.page++;
|
|
this.loadNotifications();
|
|
}
|
|
},
|
|
|
|
goToPage(pageNum) {
|
|
this.page = pageNum;
|
|
this.loadNotifications();
|
|
}
|
|
};
|
|
}
|