/** * Admin Email Logs (Audit) Page * * Universal email audit screen covering all emails sent through the platform. * Provides filtering, pagination, stats dashboard, and detail preview. * * Content retention policy: A scheduled Celery task (post-launch) will null out * body_html/body_text on EmailLog records older than 90 days. Metadata (recipient, * subject, status, timestamps, store context) is kept indefinitely for audit/compliance. */ const emailLogsLog = window.LogConfig?.createLogger('EMAIL_LOGS') || console; function emailLogsPage() { return { ...data(), currentPage: 'email-logs', // State loading: true, error: null, logs: [], stats: { by_status: {}, by_template: {}, total: 0 }, // Filters filters: { search: '', status: '', template_code: '', store_id: '', date_from: '', date_to: '', }, // Pagination pagination: { page: 1, per_page: 50, total: 0, pages: 0, }, // Detail modal showDetail: false, selectedLog: null, detailTab: 'html', // Known template types for dropdown templateTypes: [ 'signup_welcome', 'order_confirmation', 'password_reset', 'team_invitation', 'subscription_welcome', 'payment_failed', 'subscription_cancelled', 'trial_ending', ], async init() { if (window._emailLogsInitialized) return; window._emailLogsInitialized = true; emailLogsLog.info('Email logs page initializing'); try { if (window.PlatformSettings) { this.pagination.per_page = await window.PlatformSettings.getRowsPerPage(); } await I18n.loadModule('messaging'); await Promise.all([this.loadLogs(), this.loadStats()]); } catch (err) { emailLogsLog.error('Init failed:', err); this.error = 'Failed to load email logs'; } }, async loadLogs() { this.loading = true; this.error = null; try { const params = new URLSearchParams({ page: this.pagination.page, per_page: this.pagination.per_page, }); if (this.filters.search) params.set('search', this.filters.search); if (this.filters.status) params.set('status', this.filters.status); if (this.filters.template_code) params.set('template_code', this.filters.template_code); if (this.filters.store_id) params.set('store_id', this.filters.store_id); if (this.filters.date_from) params.set('date_from', this.filters.date_from); if (this.filters.date_to) params.set('date_to', this.filters.date_to); const result = await apiClient.get(`/admin/email-logs?${params}`); this.logs = result.items || []; this.pagination.total = result.total || 0; this.pagination.pages = result.total_pages || 0; emailLogsLog.info(`Loaded ${this.logs.length} logs (total: ${this.pagination.total})`); } catch (err) { emailLogsLog.error('Failed to load logs:', err); this.error = err.message || 'Failed to load email logs'; } finally { this.loading = false; } }, async loadStats() { try { this.stats = await apiClient.get('/admin/email-logs/stats'); // Update template types from actual data if (this.stats.by_template) { const serverTypes = Object.keys(this.stats.by_template); const merged = new Set([...this.templateTypes, ...serverTypes]); this.templateTypes = [...merged].sort(); } } catch (err) { emailLogsLog.error('Failed to load stats:', err); } }, async viewDetail(logId) { try { this.selectedLog = await apiClient.get(`/admin/email-logs/${logId}`); this.detailTab = this.selectedLog.body_html ? 'html' : 'text'; this.showDetail = true; // Render HTML in sandboxed iframe after DOM updates this.$nextTick(() => { if (this.selectedLog?.body_html && this.$refs.emailPreview) { const doc = this.$refs.emailPreview.contentDocument; doc.open(); doc.write(this.selectedLog.body_html); doc.close(); } }); } catch (err) { emailLogsLog.error('Failed to load log detail:', err); } }, applyFilters() { this.pagination.page = 1; this.loadLogs(); }, resetFilters() { this.filters = { search: '', status: '', template_code: '', store_id: '', date_from: '', date_to: '', }; this.applyFilters(); }, async refresh() { try { this.error = null; await Promise.all([this.loadLogs(), this.loadStats()]); } catch (err) { emailLogsLog.error('Refresh failed:', err); this.error = err.message || 'Failed to refresh'; } }, // Pagination computed properties and methods required by pagination macro get startIndex() { if (this.pagination.total === 0) return 0; return (this.pagination.page - 1) * this.pagination.per_page + 1; }, get endIndex() { return Math.min( this.pagination.page * this.pagination.per_page, this.pagination.total ); }, get totalPages() { return this.pagination.pages; }, get pageNumbers() { const pages = []; const total = this.pagination.pages; const current = this.pagination.page; if (total <= 7) { for (let i = 1; i <= total; i++) pages.push(i); } else { pages.push(1); if (current > 3) pages.push('...'); for (let i = Math.max(2, current - 1); i <= Math.min(total - 1, current + 1); i++) { pages.push(i); } if (current < total - 2) pages.push('...'); pages.push(total); } return pages; }, previousPage() { if (this.pagination.page > 1) { this.pagination.page--; this.loadLogs(); } }, nextPage() { if (this.pagination.page < this.pagination.pages) { this.pagination.page++; this.loadLogs(); } }, goToPage(page) { if (page === '...' || page === this.pagination.page) return; this.pagination.page = page; this.loadLogs(); }, // Template code to human-readable label templateLabel(code) { if (!code) return 'N/A'; const labels = { signup_welcome: 'Signup Welcome', order_confirmation: 'Order Confirmation', password_reset: 'Password Reset', team_invitation: 'Team Invitation', team_invite: 'Team Invite', subscription_welcome: 'Subscription Welcome', payment_failed: 'Payment Failed', subscription_cancelled: 'Subscription Cancelled', trial_ending: 'Trial Ending', }; return labels[code] || code.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); }, // Status badge CSS classes statusBadgeClass(status) { const classes = { sent: 'text-green-700 bg-green-100 dark:text-green-100 dark:bg-green-700', failed: 'text-red-700 bg-red-100 dark:text-red-100 dark:bg-red-700', pending: 'text-yellow-700 bg-yellow-100 dark:text-yellow-100 dark:bg-yellow-700', delivered: 'text-blue-700 bg-blue-100 dark:text-blue-100 dark:bg-blue-700', bounced: 'text-orange-700 bg-orange-100 dark:text-orange-100 dark:bg-orange-700', opened: 'text-purple-700 bg-purple-100 dark:text-purple-100 dark:bg-purple-700', clicked: 'text-indigo-700 bg-indigo-100 dark:text-indigo-100 dark:bg-indigo-700', }; return classes[status] || 'text-gray-700 bg-gray-100 dark:text-gray-100 dark:bg-gray-700'; }, formatDate(dateStr) { if (!dateStr) return ''; try { return new Date(dateStr).toLocaleString(); } catch { return dateStr; } }, }; }