/** * Custom Recipients Management Module * Handles CRUD operations for custom email recipients in campaigns */ console.log('Custom Recipients module loading...'); const CustomRecipients = (() => { console.log('Custom Recipients module initialized'); let currentCampaignSlug = null; let recipients = []; /** * Initialize the module with a campaign slug */ function init(campaignSlug) { console.log('CustomRecipients.init() called with slug:', campaignSlug); currentCampaignSlug = campaignSlug; // Setup event listeners every time init is called // Use setTimeout to ensure DOM is ready setTimeout(() => { setupEventListeners(); console.log('CustomRecipients event listeners set up'); }, 100); } /** * Setup event listeners for custom recipients UI */ function setupEventListeners() { console.log('Setting up CustomRecipients event listeners'); // Add recipient form submit const addForm = document.getElementById('add-recipient-form'); console.log('Add recipient form found:', addForm); if (addForm) { // Remove any existing listener first addForm.removeEventListener('submit', handleAddRecipient); addForm.addEventListener('submit', handleAddRecipient); console.log('Form submit listener attached'); } // Bulk import button const bulkImportBtn = document.getElementById('bulk-import-recipients-btn'); if (bulkImportBtn) { bulkImportBtn.removeEventListener('click', openBulkImportModal); bulkImportBtn.addEventListener('click', openBulkImportModal); } // Clear all recipients button const clearAllBtn = document.getElementById('clear-all-recipients-btn'); if (clearAllBtn) { clearAllBtn.removeEventListener('click', handleClearAll); clearAllBtn.addEventListener('click', handleClearAll); } // Bulk import modal buttons const importBtn = document.getElementById('import-recipients-btn'); if (importBtn) { importBtn.removeEventListener('click', handleBulkImport); importBtn.addEventListener('click', handleBulkImport); } const cancelBtn = document.querySelector('#bulk-import-modal .cancel'); if (cancelBtn) { cancelBtn.removeEventListener('click', closeBulkImportModal); cancelBtn.addEventListener('click', closeBulkImportModal); } // Close modal on backdrop click const modal = document.getElementById('bulk-import-modal'); if (modal) { modal.addEventListener('click', (e) => { if (e.target === modal) { closeBulkImportModal(); } }); } } /** * Load recipients for the current campaign */ async function loadRecipients(campaignSlug) { console.log('loadRecipients() called with campaignSlug:', campaignSlug); console.log('currentCampaignSlug:', currentCampaignSlug); // Use provided slug or fall back to currentCampaignSlug const slug = campaignSlug || currentCampaignSlug; if (!slug) { console.error('No campaign slug available to load recipients'); showMessage('No campaign selected', 'error'); return []; } try { console.log('Fetching recipients from:', `/campaigns/${slug}/custom-recipients`); const data = await window.apiClient.get(`/campaigns/${slug}/custom-recipients`); console.log('Recipients data received:', data); recipients = data.recipients || []; console.log('Loaded recipients count:', recipients.length); displayRecipients(); return recipients; } catch (error) { console.error('Error loading recipients:', error); showMessage('Failed to load recipients: ' + error.message, 'error'); return []; } } /** * Display recipients list */ function displayRecipients() { console.log('displayRecipients() called, recipients count:', recipients.length); const container = document.getElementById('recipients-list'); console.log('Recipients container found:', container); if (!container) { console.error('Recipients list container not found!'); return; } if (recipients.length === 0) { container.innerHTML = '
No custom recipients added yet. Use the form above to add recipients.
'; console.log('Displayed empty state'); return; } console.log('Rendering', recipients.length, 'recipients'); container.innerHTML = recipients.map(recipient => `
${escapeHtml(recipient.recipient_name)}
${escapeHtml(recipient.recipient_email)}
${recipient.recipient_title ? `
${escapeHtml(recipient.recipient_title)}
` : ''} ${recipient.recipient_organization ? `
${escapeHtml(recipient.recipient_organization)}
` : ''} ${recipient.notes ? `
${escapeHtml(recipient.notes)}
` : ''}
`).join(''); // Add event listeners to edit and delete buttons container.querySelectorAll('.edit-recipient').forEach(btn => { btn.addEventListener('click', (e) => { const id = e.currentTarget.dataset.id; handleEditRecipient(id); }); }); container.querySelectorAll('.delete-recipient').forEach(btn => { btn.addEventListener('click', (e) => { const id = e.currentTarget.dataset.id; handleDeleteRecipient(id); }); }); } /** * Handle add recipient form submission */ async function handleAddRecipient(e) { console.log('handleAddRecipient called, event:', e); e.preventDefault(); console.log('Form submission prevented, currentCampaignSlug:', currentCampaignSlug); const formData = { recipient_name: document.getElementById('recipient-name').value.trim(), recipient_email: document.getElementById('recipient-email').value.trim(), recipient_title: document.getElementById('recipient-title').value.trim(), recipient_organization: document.getElementById('recipient-organization').value.trim(), notes: document.getElementById('recipient-notes').value.trim() }; console.log('Form data collected:', formData); // Validate email if (!validateEmail(formData.recipient_email)) { console.error('Email validation failed'); showMessage('Please enter a valid email address', 'error'); return; } console.log('Email validation passed'); try { const url = `/campaigns/${currentCampaignSlug}/custom-recipients`; console.log('Making POST request to:', url); const data = await window.apiClient.post(url, formData); console.log('Response data:', data); showMessage('Recipient added successfully', 'success'); e.target.reset(); await loadRecipients(currentCampaignSlug); } catch (error) { console.error('Error adding recipient:', error); showMessage('Failed to add recipient: ' + error.message, 'error'); } } /** * Handle edit recipient */ async function handleEditRecipient(recipientId) { const recipient = recipients.find(r => r.id == recipientId); if (!recipient) return; // Populate form with recipient data document.getElementById('recipient-name').value = recipient.recipient_name || ''; document.getElementById('recipient-email').value = recipient.recipient_email || ''; document.getElementById('recipient-title').value = recipient.recipient_title || ''; document.getElementById('recipient-organization').value = recipient.recipient_organization || ''; document.getElementById('recipient-notes').value = recipient.notes || ''; // Change form behavior to update instead of create const form = document.getElementById('add-recipient-form'); const submitBtn = form.querySelector('button[type="submit"]'); // Store the recipient ID for update form.dataset.editingId = recipientId; submitBtn.textContent = 'Update Recipient'; // Add cancel button if it doesn't exist let cancelBtn = form.querySelector('.cancel-edit-btn'); if (!cancelBtn) { cancelBtn = document.createElement('button'); cancelBtn.type = 'button'; cancelBtn.className = 'btn secondary cancel-edit-btn'; cancelBtn.textContent = 'Cancel'; cancelBtn.addEventListener('click', cancelEdit); submitBtn.parentNode.insertBefore(cancelBtn, submitBtn.nextSibling); } // Update form submit handler form.removeEventListener('submit', handleAddRecipient); form.addEventListener('submit', handleUpdateRecipient); // Scroll to form form.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } /** * Handle update recipient */ async function handleUpdateRecipient(e) { e.preventDefault(); const form = e.target; const recipientId = form.dataset.editingId; const formData = { recipient_name: document.getElementById('recipient-name').value.trim(), recipient_email: document.getElementById('recipient-email').value.trim(), recipient_title: document.getElementById('recipient-title').value.trim(), recipient_organization: document.getElementById('recipient-organization').value.trim(), notes: document.getElementById('recipient-notes').value.trim() }; // Validate email if (!validateEmail(formData.recipient_email)) { showMessage('Please enter a valid email address', 'error'); return; } try { const data = await window.apiClient.put(`/campaigns/${currentCampaignSlug}/custom-recipients/${recipientId}`, formData); showMessage('Recipient updated successfully', 'success'); cancelEdit(); await loadRecipients(currentCampaignSlug); } catch (error) { console.error('Error updating recipient:', error); showMessage('Failed to update recipient: ' + error.message, 'error'); } } /** * Cancel edit mode */ function cancelEdit() { const form = document.getElementById('add-recipient-form'); const submitBtn = form.querySelector('button[type="submit"]'); const cancelBtn = form.querySelector('.cancel-edit-btn'); // Reset form form.reset(); delete form.dataset.editingId; submitBtn.textContent = 'Add Recipient'; // Remove cancel button if (cancelBtn) { cancelBtn.remove(); } // Restore original submit handler form.removeEventListener('submit', handleUpdateRecipient); form.addEventListener('submit', handleAddRecipient); } /** * Handle delete recipient */ async function handleDeleteRecipient(recipientId) { if (!confirm('Are you sure you want to delete this recipient?')) { return; } try { const data = await window.apiClient.delete(`/campaigns/${currentCampaignSlug}/custom-recipients/${recipientId}`); showMessage('Recipient deleted successfully', 'success'); await loadRecipients(currentCampaignSlug); } catch (error) { console.error('Error deleting recipient:', error); showMessage('Failed to delete recipient: ' + error.message, 'error'); } } /** * Handle clear all recipients */ async function handleClearAll() { if (!confirm('Are you sure you want to delete ALL custom recipients for this campaign? This cannot be undone.')) { return; } try { const data = await window.apiClient.delete(`/campaigns/${currentCampaignSlug}/custom-recipients`); showMessage(`Successfully deleted ${data.deletedCount} recipient(s)`, 'success'); await loadRecipients(currentCampaignSlug); } catch (error) { console.error('Error deleting all recipients:', error); showMessage('Failed to delete recipients: ' + error.message, 'error'); } } /** * Open bulk import modal */ function openBulkImportModal() { const modal = document.getElementById('bulk-import-modal'); if (modal) { modal.style.display = 'block'; // Clear previous results document.getElementById('import-results').innerHTML = ''; document.getElementById('csv-file-input').value = ''; document.getElementById('csv-paste-input').value = ''; } } /** * Close bulk import modal */ function closeBulkImportModal() { const modal = document.getElementById('bulk-import-modal'); if (modal) { modal.style.display = 'none'; } } /** * Handle bulk import */ async function handleBulkImport() { const fileInput = document.getElementById('csv-file-input'); const pasteInput = document.getElementById('csv-paste-input'); const resultsDiv = document.getElementById('import-results'); let csvText = ''; // Check file input first if (fileInput.files.length > 0) { const file = fileInput.files[0]; csvText = await readFileAsText(file); } else if (pasteInput.value.trim()) { csvText = pasteInput.value.trim(); } else { resultsDiv.innerHTML = '
Please select a CSV file or paste CSV data
'; return; } // Parse CSV const parsedRecipients = parseCsv(csvText); if (parsedRecipients.length === 0) { resultsDiv.innerHTML = '
No valid recipients found in CSV
'; return; } // Show loading resultsDiv.innerHTML = '
Importing recipients...
'; try { const data = await window.apiClient.post(`/campaigns/${currentCampaignSlug}/custom-recipients/bulk`, { recipients: parsedRecipients }); if (data.success) { const { results } = data; let html = `
Successfully imported ${results.success.length} of ${results.total} recipients
`; if (results.failed.length > 0) { html += '
Failed imports:
'; } resultsDiv.innerHTML = html; await loadRecipients(currentCampaignSlug); // Close modal after 3 seconds if all successful if (results.failed.length === 0) { setTimeout(closeBulkImportModal, 3000); } } else { throw new Error(data.error || 'Failed to import recipients'); } } catch (error) { console.error('Error importing recipients:', error); resultsDiv.innerHTML = `
Failed to import recipients: ${escapeHtml(error.message)}
`; } } /** * Parse CSV text into recipients array */ function parseCsv(csvText) { const lines = csvText.split('\n').filter(line => line.trim()); const recipients = []; // Skip header row if it exists const startIndex = lines[0].toLowerCase().includes('recipient_name') ? 1 : 0; for (let i = startIndex; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; // Simple CSV parsing (doesn't handle quoted commas) const parts = line.split(',').map(p => p.trim().replace(/^["']|["']$/g, '')); if (parts.length >= 2) { recipients.push({ recipient_name: parts[0], recipient_email: parts[1], recipient_title: parts[2] || '', recipient_organization: parts[3] || '', notes: parts[4] || '' }); } } return recipients; } /** * Read file as text */ function readFileAsText(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => resolve(e.target.result); reader.onerror = (e) => reject(e); reader.readAsText(file); }); } /** * Validate email format */ function validateEmail(email) { const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return re.test(email); } /** * Escape HTML to prevent XSS */ function escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, m => map[m]); } /** * Show message to user */ function showMessage(message, type = 'info') { // Try to use existing message display system if (typeof window.showMessage === 'function') { window.showMessage(message, type); } else { // Fallback to alert alert(message); } } // Public API return { init, loadRecipients, displayRecipients }; })(); // Make available globally window.CustomRecipients = CustomRecipients;