freealberta/influence/app/public/js/custom-recipients.js
admin 4d8b9effd0 feat(blog): add detailed update on Influence and Map app developments since August
A bunch of udpates to the listmonk sync to add influence to it
2025-10-25 12:45:35 -06:00

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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
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;