/**
* 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:';
results.failed.forEach(failure => {
html += `- ${escapeHtml(failure.recipient.recipient_name || 'Unknown')} (${escapeHtml(failure.recipient.recipient_email || 'No email')}): ${escapeHtml(failure.error)}
`;
});
html += '
';
}
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;