/** * Activity Feed Component * Real-time activity feed with filtering and auto-refresh */ class ActivityFeed { constructor(containerId, options = {}) { this.container = document.getElementById(containerId); if (!this.container) { console.error(`Activity feed container not found: ${containerId}`); return; } this.options = { limit: options.limit || 50, autoRefresh: options.autoRefresh !== false, refreshInterval: options.refreshInterval || 30000, // 30 seconds filters: options.filters || {}, ...options }; this.activities = []; this.page = 1; this.hasMore = true; this.loading = false; this.refreshTimer = null; this.init(); } init() { this.render(); this.loadActivities(); this.setupAutoRefresh(); this.setupWebSocket(); } async loadActivities(page = 1, append = false) { if (this.loading) return; this.loading = true; this.showLoading(); try { const params = new URLSearchParams({ page: page.toString(), limit: this.options.limit.toString(), ...this.options.filters }); const response = await fetch(`/api/activity?${params}`); const data = await response.json(); if (append) { this.activities = [...this.activities, ...data.activities]; } else { this.activities = data.activities; } this.hasMore = data.pagination.has_next; this.page = data.pagination.page; this.render(); } catch (error) { console.error('Error loading activities:', error); this.showError('Failed to load activities'); } finally { this.loading = false; this.hideLoading(); } } render() { if (!this.container) return; if (this.activities.length === 0 && !this.loading) { this.container.innerHTML = `

No activities found

`; return; } const activitiesHtml = this.activities.map(activity => this.renderActivity(activity)).join(''); this.container.innerHTML = `
${activitiesHtml}
${this.hasMore ? '
' : ''} `; // Setup load more button const loadMoreBtn = this.container.querySelector('.load-more-btn'); if (loadMoreBtn) { loadMoreBtn.addEventListener('click', () => { this.loadActivities(this.page + 1, true); }); } } renderActivity(activity) { const icon = this.getActivityIcon(activity); const timeAgo = this.formatTimeAgo(activity.created_at); const userDisplay = activity.display_name || activity.username || 'Unknown'; return `
${userDisplay} ${activity.description || this.formatActivityDescription(activity)} ${timeAgo}
${activity.extra_data ? `
${this.formatExtraData(activity.extra_data)}
` : ''}
`; } getActivityIcon(activity) { const icons = { 'created': 'fas fa-plus-circle text-green-500', 'updated': 'fas fa-edit text-blue-500', 'deleted': 'fas fa-trash text-red-500', 'started': 'fas fa-play text-green-500', 'stopped': 'fas fa-stop text-red-500', 'completed': 'fas fa-check-circle text-green-500', 'assigned': 'fas fa-user-plus text-blue-500', 'commented': 'fas fa-comment text-gray-500', 'sent': 'fas fa-paper-plane text-blue-500', 'paid': 'fas fa-dollar-sign text-green-500', }; return icons[activity.action] || 'fas fa-circle text-gray-500'; } formatActivityDescription(activity) { const entityType = activity.entity_type.replace('_', ' '); return `${activity.action} ${entityType} ${activity.entity_name || ''}`; } formatTimeAgo(timestamp) { if (!timestamp) return ''; const date = new Date(timestamp); const now = new Date(); const diffMs = now - date; const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return 'just now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString(); } formatExtraData(extraData) { if (typeof extraData !== 'object') return ''; return Object.entries(extraData).map(([key, value]) => `${key}: ${value}`).join(', '); } setupAutoRefresh() { if (!this.options.autoRefresh) return; this.refreshTimer = setInterval(() => { this.loadActivities(1, false); }, this.options.refreshInterval); } setupWebSocket() { // Listen for real-time activity updates via WebSocket if (typeof io !== 'undefined') { io.on('activity_created', (data) => { if (data.activity) { this.activities.unshift(data.activity); if (this.activities.length > this.options.limit) { this.activities.pop(); } this.render(); } }); } } showLoading() { const loadingEl = document.createElement('div'); loadingEl.className = 'activity-loading text-center py-4'; loadingEl.innerHTML = ' Loading...'; this.container.appendChild(loadingEl); } hideLoading() { const loadingEl = this.container.querySelector('.activity-loading'); if (loadingEl) { loadingEl.remove(); } } showError(message) { const errorEl = document.createElement('div'); errorEl.className = 'activity-error bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded'; errorEl.textContent = message; this.container.appendChild(errorEl); } setFilters(filters) { this.options.filters = { ...this.options.filters, ...filters }; this.page = 1; this.loadActivities(1, false); } destroy() { if (this.refreshTimer) { clearInterval(this.refreshTimer); } if (typeof io !== 'undefined') { io.off('activity_created'); } } } // Auto-initialize if container exists document.addEventListener('DOMContentLoaded', () => { const container = document.getElementById('activity-feed-container'); if (container) { window.activityFeed = new ActivityFeed('activity-feed-container', { autoRefresh: true, refreshInterval: 30000 }); } });