526 lines
18 KiB
JavaScript
526 lines
18 KiB
JavaScript
/**
|
|
* 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 = '<div class="empty-state">No custom recipients added yet. Use the form above to add recipients.</div>';
|
|
console.log('Displayed empty state');
|
|
return;
|
|
}
|
|
|
|
console.log('Rendering', recipients.length, 'recipients');
|
|
container.innerHTML = recipients.map(recipient => `
|
|
<div class="recipient-card" data-id="${recipient.id}">
|
|
<div class="recipient-info">
|
|
<div class="recipient-name">${escapeHtml(recipient.recipient_name)}</div>
|
|
<div class="recipient-email">${escapeHtml(recipient.recipient_email)}</div>
|
|
${recipient.recipient_title ? `<div class="recipient-meta">${escapeHtml(recipient.recipient_title)}</div>` : ''}
|
|
${recipient.recipient_organization ? `<div class="recipient-meta">${escapeHtml(recipient.recipient_organization)}</div>` : ''}
|
|
${recipient.notes ? `<div class="recipient-meta"><em>${escapeHtml(recipient.notes)}</em></div>` : ''}
|
|
</div>
|
|
<div class="recipient-actions">
|
|
<button class="btn-icon edit-recipient" data-id="${recipient.id}" title="Edit recipient">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
|
|
</svg>
|
|
</button>
|
|
<button class="btn-icon delete-recipient" data-id="${recipient.id}" title="Delete recipient">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
|
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`).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 = '<div class="error">Please select a CSV file or paste CSV data</div>';
|
|
return;
|
|
}
|
|
|
|
// Parse CSV
|
|
const parsedRecipients = parseCsv(csvText);
|
|
|
|
if (parsedRecipients.length === 0) {
|
|
resultsDiv.innerHTML = '<div class="error">No valid recipients found in CSV</div>';
|
|
return;
|
|
}
|
|
|
|
// Show loading
|
|
resultsDiv.innerHTML = '<div class="loading">Importing recipients...</div>';
|
|
|
|
try {
|
|
const data = await window.apiClient.post(`/campaigns/${currentCampaignSlug}/custom-recipients/bulk`, { recipients: parsedRecipients });
|
|
|
|
if (data.success) {
|
|
const { results } = data;
|
|
let html = `<div class="success">Successfully imported ${results.success.length} of ${results.total} recipients</div>`;
|
|
|
|
if (results.failed.length > 0) {
|
|
html += '<div class="failed-imports"><strong>Failed imports:</strong><ul>';
|
|
results.failed.forEach(failure => {
|
|
html += `<li>${escapeHtml(failure.recipient.recipient_name || 'Unknown')} (${escapeHtml(failure.recipient.recipient_email || 'No email')}): ${escapeHtml(failure.error)}</li>`;
|
|
});
|
|
html += '</ul></div>';
|
|
}
|
|
|
|
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 = `<div class="error">Failed to import recipients: ${escapeHtml(error.message)}</div>`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|