/** * Newsletter Management - Admin Interface */ let currentPage = 1; const perPage = 50; let currentFilters = { status: 'active', verified: 'all' }; /** * Initialize page */ async function init() { // Event listeners (navbar handles admin name and logout now) document.getElementById('refresh-btn').addEventListener('click', () => loadAll()); document.getElementById('export-btn').addEventListener('click', exportSubscribers); document.getElementById('filter-status').addEventListener('change', handleFilterChange); document.getElementById('filter-verified').addEventListener('change', handleFilterChange); document.getElementById('prev-page').addEventListener('click', () => changePage(-1)); document.getElementById('next-page').addEventListener('click', () => changePage(1)); // Newsletter sending form listeners document.getElementById('send-newsletter-form').addEventListener('submit', handleSendNewsletter); document.getElementById('preview-newsletter-btn').addEventListener('click', handlePreviewNewsletter); document.getElementById('test-newsletter-btn').addEventListener('click', handleTestNewsletter); // Load data await loadAll(); } /** * Load all data */ async function loadAll() { await Promise.all([ loadStats(), loadSubscribers() ]); } /** * Load statistics */ async function loadStats() { try { const token = localStorage.getItem('admin_token'); const response = await fetch('/api/newsletter/admin/stats', { headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) throw new Error('Failed to load stats'); const data = await response.json(); const stats = data.stats; document.getElementById('stat-total').textContent = stats.total || 0; document.getElementById('stat-active').textContent = stats.active || 0; document.getElementById('stat-verified').textContent = stats.verified || 0; document.getElementById('stat-recent').textContent = stats.recent_30_days || 0; } catch (error) { console.error('Error loading stats:', error); } } /** * Load subscribers list */ async function loadSubscribers() { try { const token = localStorage.getItem('admin_token'); const skip = (currentPage - 1) * perPage; const params = new URLSearchParams({ limit: perPage, skip, active: currentFilters.status === 'all' ? null : currentFilters.status === 'active', verified: currentFilters.verified === 'all' ? null : currentFilters.verified === 'verified' }); // Remove null values for (const [key, value] of [...params.entries()]) { if (value === 'null' || value === null) { params.delete(key); } } const response = await fetch(`/api/newsletter/admin/subscriptions?${params}`, { headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) throw new Error('Failed to load subscribers'); const data = await response.json(); renderSubscribers(data.subscriptions); updatePagination(data.pagination); } catch (error) { console.error('Error loading subscribers:', error); document.getElementById('subscribers-table').innerHTML = ` Error loading subscribers. Please refresh the page. `; } } /** * Render subscribers table */ function renderSubscribers(subscriptions) { const tbody = document.getElementById('subscribers-table'); if (!subscriptions || subscriptions.length === 0) { tbody.innerHTML = ` No subscribers found `; return; } tbody.innerHTML = subscriptions.map(sub => ` ${escapeHtml(sub.email)} ${escapeHtml(sub.name) || '-'} ${escapeHtml(sub.source) || 'unknown'} ${sub.active ? ` ${sub.verified ? '' : ''} ${sub.verified ? 'Active ✓' : 'Active'} ` : 'Inactive' } ${formatDate(sub.subscribed_at)} `).join(''); // Add event listeners to buttons tbody.querySelectorAll('.view-details-btn').forEach(btn => { btn.addEventListener('click', function() { const id = this.getAttribute('data-id'); viewDetails(id); }); }); tbody.querySelectorAll('.delete-subscriber-btn').forEach(btn => { btn.addEventListener('click', function() { const id = this.getAttribute('data-id'); const email = this.getAttribute('data-email'); deleteSubscriber(id, email); }); }); } /** * Update pagination UI */ function updatePagination(pagination) { document.getElementById('showing-from').textContent = pagination.skip + 1; document.getElementById('showing-to').textContent = Math.min(pagination.skip + pagination.limit, pagination.total); document.getElementById('total-count').textContent = pagination.total; document.getElementById('prev-page').disabled = currentPage === 1; document.getElementById('next-page').disabled = !pagination.has_more; } /** * Handle filter change */ function handleFilterChange() { currentFilters.status = document.getElementById('filter-status').value; currentFilters.verified = document.getElementById('filter-verified').value; currentPage = 1; loadSubscribers(); } /** * Change page */ function changePage(direction) { currentPage += direction; loadSubscribers(); } /** * View subscriber details */ async function viewDetails(id) { alert(`Subscriber details for ID: ${id}\n(Full implementation would show a modal with complete subscriber information)`); } /** * Delete subscriber */ async function deleteSubscriber(id, email) { if (!confirm(`Are you sure you want to delete subscription for ${email}?\n\nThis action cannot be undone.`)) { return; } try { const token = localStorage.getItem('admin_token'); const response = await fetch(`/api/newsletter/admin/subscriptions/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) throw new Error('Failed to delete subscriber'); alert('Subscriber deleted successfully'); await loadAll(); } catch (error) { console.error('Error deleting subscriber:', error); alert('Failed to delete subscriber. Please try again.'); } } /** * Export subscribers as CSV */ async function exportSubscribers() { try { const token = localStorage.getItem('admin_token'); const active = currentFilters.status === 'all' ? 'all' : 'true'; const response = await fetch(`/api/newsletter/admin/export?active=${active}`, { headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) throw new Error('Failed to export subscribers'); const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `newsletter-subscribers-${Date.now()}.csv`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); } catch (error) { console.error('Error exporting subscribers:', error); alert('Failed to export subscribers. Please try again.'); } } // Logout handled by navbar component /** * Format date */ function formatDate(dateString) { const date = new Date(dateString); return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); } /** * Escape HTML */ function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Get CSRF token from cookie */ function getCsrfToken() { const cookies = document.cookie.split('; '); const csrfCookie = cookies.find(row => row.startsWith('csrf-token=')); return csrfCookie ? csrfCookie.split('=')[1] : null; } /** * Validate newsletter form */ function validateNewsletterForm() { const tier = document.getElementById('newsletter-tier').value; const subject = document.getElementById('newsletter-subject').value; if (!tier) { showStatus('error', 'Please select a newsletter tier'); return null; } if (!subject) { showStatus('error', 'Please enter a subject line'); return null; } // Collect content from individual form fields const variables = { highlight_1_title: document.getElementById('highlight_1_title').value, highlight_1_summary: document.getElementById('highlight_1_summary').value, highlight_1_link: document.getElementById('highlight_1_link').value, finding_1: document.getElementById('finding_1').value, question_1: document.getElementById('question_1').value, feedback_link: document.getElementById('feedback_link').value, blog_link: document.getElementById('blog_link').value }; // Basic validation - at least one content field should be filled const hasContent = Object.values(variables).some(val => val && val.trim()); if (!hasContent) { showStatus('error', 'Please fill in at least one content field'); return null; } return { tier, subject, previewText: document.getElementById('newsletter-preview').value, variables }; } /** * Show status message */ function showStatus(type, message) { const statusDiv = document.getElementById('send-status'); statusDiv.className = `px-4 py-3 rounded ${ type === 'success' ? 'bg-green-50 border border-green-200 text-green-800' : type === 'error' ? 'bg-red-50 border border-red-200 text-red-800' : 'bg-blue-50 border border-blue-200 text-blue-800' }`; statusDiv.textContent = message; statusDiv.classList.remove('hidden'); } /** * Handle newsletter preview */ async function handlePreviewNewsletter(e) { e.preventDefault(); const formData = validateNewsletterForm(); if (!formData) return; try { showStatus('info', 'Generating preview...'); const token = localStorage.getItem('admin_token'); const csrfToken = getCsrfToken(); const response = await fetch('/api/newsletter/admin/preview', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ tier: formData.tier, variables: formData.variables }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Preview failed'); } const html = await response.text(); // Open preview in new window const previewWindow = window.open('', '_blank', 'width=800,height=600'); previewWindow.document.write(html); previewWindow.document.close(); showStatus('success', 'Preview opened in new window'); } catch (error) { console.error('Preview error:', error); showStatus('error', `Preview failed: ${error.message}`); } } /** * Handle test newsletter send */ async function handleTestNewsletter(e) { e.preventDefault(); const formData = validateNewsletterForm(); if (!formData) return; const testEmail = prompt('Enter your email address for test send:'); if (!testEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(testEmail)) { showStatus('error', 'Valid email address is required for test send'); return; } try { showStatus('info', 'Sending test email...'); const token = localStorage.getItem('admin_token'); const csrfToken = getCsrfToken(); const response = await fetch('/api/newsletter/admin/send', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ ...formData, testMode: true, testEmail }) }); const result = await response.json(); if (response.ok && result.success) { showStatus('success', `Test email sent to ${testEmail}`); // Clear form on successful test send document.getElementById('send-newsletter-form').reset(); } else { throw new Error(result.error || 'Test send failed'); } } catch (error) { console.error('Test send error:', error); showStatus('error', `Test send failed: ${error.message}`); } } /** * Handle newsletter send to all subscribers */ async function handleSendNewsletter(e) { e.preventDefault(); const formData = validateNewsletterForm(); if (!formData) return; const confirmation = confirm( `Are you sure you want to send this newsletter to all subscribers of the "${formData.tier}" tier?\n\n` + `Subject: ${formData.subject}\n\n` + `This action cannot be undone.` ); if (!confirmation) { return; } try { showStatus('info', 'Sending newsletter to subscribers...'); const token = localStorage.getItem('admin_token'); const csrfToken = getCsrfToken(); const response = await fetch('/api/newsletter/admin/send', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ ...formData, testMode: false }) }); const result = await response.json(); if (response.ok && result.success) { showStatus('success', `${result.message}\n\nSent: ${result.sent}, Failed: ${result.failed}`); // Clear form on success if (result.failed === 0) { document.getElementById('send-newsletter-form').reset(); } } else { throw new Error(result.error || 'Send failed'); } } catch (error) { console.error('Send error:', error); showStatus('error', `Send failed: ${error.message}`); } } // Initialize on page load document.addEventListener('DOMContentLoaded', init);