diff --git a/influence/app/controllers/campaigns.js b/influence/app/controllers/campaigns.js index c88791b..8848819 100644 --- a/influence/app/controllers/campaigns.js +++ b/influence/app/controllers/campaigns.js @@ -9,17 +9,31 @@ class CampaignsController { try { const campaigns = await nocoDB.getAllCampaigns(); - // Get email counts for each campaign + // Get email counts for each campaign and normalize data structure const campaignsWithCounts = await Promise.all(campaigns.map(async (campaign) => { const id = campaign.Id ?? campaign.id; let emailCount = 0; if (id != null) { emailCount = await nocoDB.getCampaignEmailCount(id); } - // Normalize id property for frontend + + // Normalize campaign data structure for frontend return { id, - ...campaign, + slug: campaign['Campaign Slug'] || campaign.slug, + title: campaign['Campaign Title'] || campaign.title, + description: campaign['Description'] || campaign.description, + email_subject: campaign['Email Subject'] || campaign.email_subject, + email_body: campaign['Email Body'] || campaign.email_body, + call_to_action: campaign['Call to Action'] || campaign.call_to_action, + status: campaign['Status'] || campaign.status, + allow_smtp_email: campaign['Allow SMTP Email'] || campaign.allow_smtp_email, + allow_mailto_link: campaign['Allow Mailto Link'] || campaign.allow_mailto_link, + collect_user_info: campaign['Collect User Info'] || campaign.collect_user_info, + show_email_count: campaign['Show Email Count'] || campaign.show_email_count, + target_government_levels: campaign['Target Government Levels'] || campaign.target_government_levels, + created_at: campaign.CreatedAt || campaign.created_at, + updated_at: campaign.UpdatedAt || campaign.updated_at, emailCount }; })); @@ -55,11 +69,25 @@ class CampaignsController { const normalizedId = campaign.Id ?? campaign.id ?? id; const emailCount = await nocoDB.getCampaignEmailCount(normalizedId); + // Normalize campaign data structure for frontend res.json({ success: true, campaign: { id: normalizedId, - ...campaign, + slug: campaign['Campaign Slug'] || campaign.slug, + title: campaign['Campaign Title'] || campaign.title, + description: campaign['Description'] || campaign.description, + email_subject: campaign['Email Subject'] || campaign.email_subject, + email_body: campaign['Email Body'] || campaign.email_body, + call_to_action: campaign['Call to Action'] || campaign.call_to_action, + status: campaign['Status'] || campaign.status, + allow_smtp_email: campaign['Allow SMTP Email'] || campaign.allow_smtp_email, + allow_mailto_link: campaign['Allow Mailto Link'] || campaign.allow_mailto_link, + collect_user_info: campaign['Collect User Info'] || campaign.collect_user_info, + show_email_count: campaign['Show Email Count'] || campaign.show_email_count, + target_government_levels: campaign['Target Government Levels'] || campaign.target_government_levels, + created_at: campaign.CreatedAt || campaign.created_at, + updated_at: campaign.UpdatedAt || campaign.updated_at, emailCount } }); @@ -87,7 +115,8 @@ class CampaignsController { }); } - if (campaign.status !== 'active') { + const campaignStatus = campaign['Status'] || campaign.status; + if (campaignStatus !== 'active') { return res.status(403).json({ success: false, error: 'Campaign is not currently active' @@ -96,7 +125,8 @@ class CampaignsController { // Get email count if enabled let emailCount = null; - if (campaign.show_email_count) { + const showEmailCount = campaign['Show Email Count'] || campaign.show_email_count; + if (showEmailCount) { const id = campaign.Id ?? campaign.id; if (id != null) { emailCount = await nocoDB.getCampaignEmailCount(id); @@ -106,21 +136,21 @@ class CampaignsController { res.json({ success: true, campaign: { - id: campaign.id, - slug: campaign.slug, - title: campaign.title, - description: campaign.description, - call_to_action: campaign.call_to_action, - email_subject: campaign.email_subject, - email_body: campaign.email_body, - allow_smtp_email: campaign.allow_smtp_email, - allow_mailto_link: campaign.allow_mailto_link, - collect_user_info: campaign.collect_user_info, - show_email_count: campaign.show_email_count, - target_government_levels: Array.isArray(campaign.target_government_levels) - ? campaign.target_government_levels - : (typeof campaign.target_government_levels === 'string' && campaign.target_government_levels.length > 0 - ? campaign.target_government_levels.split(',').map(s => s.trim()) + id: campaign.Id || campaign.id, + slug: campaign['Campaign Slug'] || campaign.slug, + title: campaign['Campaign Title'] || campaign.title, + description: campaign['Description'] || campaign.description, + call_to_action: campaign['Call to Action'] || campaign.call_to_action, + email_subject: campaign['Email Subject'] || campaign.email_subject, + email_body: campaign['Email Body'] || campaign.email_body, + allow_smtp_email: campaign['Allow SMTP Email'] || campaign.allow_smtp_email, + allow_mailto_link: campaign['Allow Mailto Link'] || campaign.allow_mailto_link, + collect_user_info: campaign['Collect User Info'] || campaign.collect_user_info, + show_email_count: campaign['Show Email Count'] || campaign.show_email_count, + target_government_levels: Array.isArray(campaign['Target Government Levels'] || campaign.target_government_levels) + ? (campaign['Target Government Levels'] || campaign.target_government_levels) + : (typeof (campaign['Target Government Levels'] || campaign.target_government_levels) === 'string' && (campaign['Target Government Levels'] || campaign.target_government_levels).length > 0 + ? (campaign['Target Government Levels'] || campaign.target_government_levels).split(',').map(s => s.trim()) : []), emailCount } @@ -185,11 +215,25 @@ class CampaignsController { const campaign = await nocoDB.createCampaign(campaignData); + // Normalize the created campaign data res.status(201).json({ success: true, campaign: { id: campaign.Id ?? campaign.id, - ...campaign + slug: campaign['Campaign Slug'] || campaign.slug, + title: campaign['Campaign Title'] || campaign.title, + description: campaign['Description'] || campaign.description, + email_subject: campaign['Email Subject'] || campaign.email_subject, + email_body: campaign['Email Body'] || campaign.email_body, + call_to_action: campaign['Call to Action'] || campaign.call_to_action, + status: campaign['Status'] || campaign.status, + allow_smtp_email: campaign['Allow SMTP Email'] || campaign.allow_smtp_email, + allow_mailto_link: campaign['Allow Mailto Link'] || campaign.allow_mailto_link, + collect_user_info: campaign['Collect User Info'] || campaign.collect_user_info, + show_email_count: campaign['Show Email Count'] || campaign.show_email_count, + target_government_levels: campaign['Target Government Levels'] || campaign.target_government_levels, + created_at: campaign.CreatedAt || campaign.created_at, + updated_at: campaign.UpdatedAt || campaign.updated_at } }); } catch (error) { @@ -215,7 +259,8 @@ class CampaignsController { // Ensure slug is unique (but allow current campaign to keep its slug) const existingCampaign = await nocoDB.getCampaignBySlug(slug); - if (existingCampaign && existingCampaign.id !== parseInt(id)) { + const existingId = existingCampaign ? (existingCampaign.Id || existingCampaign.id) : null; + if (existingCampaign && String(existingId) !== String(id)) { let counter = 1; let originalSlug = slug; while (await nocoDB.getCampaignBySlug(slug)) { @@ -239,11 +284,25 @@ class CampaignsController { const campaign = await nocoDB.updateCampaign(id, updates); + // Normalize the updated campaign data res.json({ success: true, campaign: { id: campaign.Id ?? campaign.id ?? id, - ...campaign + slug: campaign['Campaign Slug'] || campaign.slug, + title: campaign['Campaign Title'] || campaign.title, + description: campaign['Description'] || campaign.description, + email_subject: campaign['Email Subject'] || campaign.email_subject, + email_body: campaign['Email Body'] || campaign.email_body, + call_to_action: campaign['Call to Action'] || campaign.call_to_action, + status: campaign['Status'] || campaign.status, + allow_smtp_email: campaign['Allow SMTP Email'] || campaign.allow_smtp_email, + allow_mailto_link: campaign['Allow Mailto Link'] || campaign.allow_mailto_link, + collect_user_info: campaign['Collect User Info'] || campaign.collect_user_info, + show_email_count: campaign['Show Email Count'] || campaign.show_email_count, + target_government_levels: campaign['Target Government Levels'] || campaign.target_government_levels, + created_at: campaign.CreatedAt || campaign.created_at, + updated_at: campaign.UpdatedAt || campaign.updated_at } }); } catch (error) { @@ -413,7 +472,8 @@ class CampaignsController { }); } - if (campaign.status !== 'active') { + const campaignStatus = campaign['Status'] || campaign.status; + if (campaignStatus !== 'active') { return res.status(403).json({ success: false, error: 'Campaign is not currently active' @@ -423,18 +483,40 @@ class CampaignsController { // Get representatives const result = await representAPI.getRepresentativesByPostalCode(postalCode); - if (!result.success) { - return res.status(result.status || 500).json(result); + // Process representatives from both concordance and centroid + let representatives = []; + + // Add concordance representatives (if any) + if (result.representatives_concordance && result.representatives_concordance.length > 0) { + representatives = representatives.concat(result.representatives_concordance); + } + + // Add centroid representatives (if any) - these are the actual elected officials + if (result.representatives_centroid && result.representatives_centroid.length > 0) { + representatives = representatives.concat(result.representatives_centroid); + } + + if (representatives.length === 0) { + return res.json({ + success: false, + message: 'No representatives found for this postal code', + representatives: [], + location: { + city: result.city, + province: result.province + } + }); } // Filter representatives by target government levels - const targetLevels = Array.isArray(campaign.target_government_levels) - ? campaign.target_government_levels - : (typeof campaign.target_government_levels === 'string' && campaign.target_government_levels.length > 0 - ? campaign.target_government_levels.split(',').map(level => level.trim()) + const targetGovernmentLevels = campaign['Target Government Levels'] || campaign.target_government_levels; + const targetLevels = Array.isArray(targetGovernmentLevels) + ? targetGovernmentLevels + : (typeof targetGovernmentLevels === 'string' && targetGovernmentLevels.length > 0 + ? targetGovernmentLevels.split(',').map(level => level.trim()) : ['Federal', 'Provincial', 'Municipal']); - const filteredRepresentatives = result.representatives.filter(rep => { + const filteredRepresentatives = representatives.filter(rep => { const repLevel = rep.elected_office ? rep.elected_office.toLowerCase() : 'other'; return targetLevels.some(targetLevel => { @@ -460,7 +542,10 @@ class CampaignsController { res.json({ success: true, representatives: filteredRepresentatives, - location: result.location + location: { + city: result.city, + province: result.province + } }); } catch (error) { diff --git a/influence/app/public/admin.html.broken b/influence/app/public/admin.html.broken deleted file mode 100644 index 33d9a63..0000000 --- a/influence/app/public/admin.html.broken +++ /dev/null @@ -1,636 +0,0 @@ - - - - - - Campaign Admin Panel - Alberta Influence Tool - - - - -
-

Campaign Admin Panel

-

Manage your influence campaigns and track engagement

-
- -
- - - - - - -
-
-

Active Campaigns

- -
- - - -
- -
-
- - -
-

Create New Campaign

- -
- - -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- -
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- -
-
- - -
-
- -
- - - -
-
-
- - -
-

Campaign Analytics

- - - -
- -
-
-
- - - - - - - - - constructor() { - this.currentCampaign = null; - this.campaigns = []; - this.init(); - } - - init() { - // Tab navigation - document.querySelectorAll('.nav-btn').forEach(btn => { - btn.addEventListener('click', (e) => { - const tab = e.target.dataset.tab; - this.switchTab(tab); - }); - }); - - // Form submission - document.getElementById('campaign-form').addEventListener('submit', (e) => { - this.handleFormSubmit(e); - }); - - // Load campaigns on init - this.loadCampaigns(); - } - - switchTab(tab) { - // Update nav buttons - document.querySelectorAll('.nav-btn').forEach(btn => { - btn.classList.remove('active'); - }); - document.querySelector(`[data-tab="${tab}"]`).classList.add('active'); - - // Show/hide tab content - document.querySelectorAll('.tab-content').forEach(content => { - content.classList.remove('active'); - }); - document.getElementById(`${tab}-tab`).classList.add('active'); - - // Load data for specific tabs - if (tab === 'campaigns') { - this.loadCampaigns(); - } else if (tab === 'analytics') { - this.loadOverallAnalytics(); - } - } - - async loadCampaigns() { - const loading = document.getElementById('campaigns-loading'); - const list = document.getElementById('campaigns-list'); - - loading.classList.remove('hidden'); - - try { - const response = await window.apiClient.get('/admin/campaigns'); - this.campaigns = response.campaigns || []; - this.renderCampaigns(); - } catch (error) { - this.showMessage('Failed to load campaigns: ' + error.message, 'error'); - } finally { - loading.classList.add('hidden'); - } - } - - renderCampaigns() { - const list = document.getElementById('campaigns-list'); - - if (this.campaigns.length === 0) { - list.innerHTML = '

No campaigns found. Create your first campaign

'; - return; - } - - list.innerHTML = this.campaigns.map(campaign => ` -
-
-
-

${campaign.title}

-

${campaign.description || 'No description'}

-
- ${campaign.status} - - 📧 ${campaign.emailCount || 0} emails sent - -
-

- Campaign URL: /campaign/${campaign.slug} -

-
-
-
- - - - -
-
- `).join(''); - } - - async editCampaign(id) { - try { - const response = await window.apiClient.get(`/admin/campaigns/${id}`); - const campaign = response.campaign; - - this.currentCampaign = campaign; - this.populateForm(campaign); - this.switchTab('create'); - document.getElementById('form-title').textContent = 'Edit Campaign'; - } catch (error) { - this.showMessage('Failed to load campaign: ' + error.message, 'error'); - } - } - - populateForm(campaign) { - document.getElementById('campaign-id').value = campaign.id; - document.getElementById('title').value = campaign.title; - document.getElementById('description').value = campaign.description || ''; - document.getElementById('call_to_action').value = campaign.call_to_action || ''; - document.getElementById('email_subject').value = campaign.email_subject; - document.getElementById('email_body').value = campaign.email_body; - document.getElementById('status').value = campaign.status; - - // Handle checkboxes - document.getElementById('allow_smtp_email').checked = campaign.allow_smtp_email; - document.getElementById('allow_mailto_link').checked = campaign.allow_mailto_link; - document.getElementById('collect_user_info').checked = campaign.collect_user_info; - document.getElementById('show_email_count').checked = campaign.show_email_count; - - // Handle target levels - document.querySelectorAll('input[name="target_government_levels"]').forEach(cb => cb.checked = false); - if (campaign.target_government_levels) { - const levels = campaign.target_government_levels.split(','); - levels.forEach(level => { - const checkbox = document.querySelector(`input[name="target_government_levels"][value="${level.trim()}"]`); - if (checkbox) checkbox.checked = true; - }); - } - } - - async handleFormSubmit(e) { - e.preventDefault(); - - const formData = new FormData(e.target); - const data = {}; - - // Handle regular fields - for (let [key, value] of formData.entries()) { - if (key !== 'target_government_levels') { - data[key] = value; - } - } - - // Handle checkboxes - data.allow_smtp_email = document.getElementById('allow_smtp_email').checked; - data.allow_mailto_link = document.getElementById('allow_mailto_link').checked; - data.collect_user_info = document.getElementById('collect_user_info').checked; - data.show_email_count = document.getElementById('show_email_count').checked; - - // Handle target government levels - const selectedLevels = []; - document.querySelectorAll('input[name="target_government_levels"]:checked').forEach(cb => { - selectedLevels.push(cb.value); - }); - data.target_government_levels = selectedLevels; - - try { - const campaignId = document.getElementById('campaign-id').value; - let response; - - if (campaignId) { - // Update existing campaign - response = await window.apiClient.makeRequest(`/api/admin/campaigns/${campaignId}`, { - method: 'PUT', - body: JSON.stringify(data) - }); - } else { - // Create new campaign - response = await window.apiClient.post('/admin/campaigns', data); - } - - this.showMessage('Campaign saved successfully!', 'success'); - this.resetForm(); - this.switchTab('campaigns'); - } catch (error) { - this.showMessage('Failed to save campaign: ' + error.message, 'error'); - } - } - - resetForm() { - document.getElementById('campaign-form').reset(); - document.getElementById('campaign-id').value = ''; - document.getElementById('form-title').textContent = 'Create New Campaign'; - this.currentCampaign = null; - } - - async deleteCampaign(id) { - if (!confirm('Are you sure you want to delete this campaign? This action cannot be undone.')) { - return; - } - - try { - await window.apiClient.makeRequest(`/api/admin/campaigns/${id}`, { - method: 'DELETE' - }); - this.showMessage('Campaign deleted successfully!', 'success'); - this.loadCampaigns(); - } catch (error) { - this.showMessage('Failed to delete campaign: ' + error.message, 'error'); - } - } - - async showCampaignAnalytics(id) { - try { - const response = await window.apiClient.get(`/admin/campaigns/${id}/analytics`); - const analytics = response.analytics; - const campaign = this.campaigns.find(c => c.id === id); - - document.getElementById('analytics-modal-title').textContent = `Analytics: ${campaign.title}`; - document.getElementById('analytics-modal-content').innerHTML = this.renderAnalytics(analytics); - document.getElementById('analytics-modal').style.display = 'block'; - } catch (error) { - this.showMessage('Failed to load analytics: ' + error.message, 'error'); - } - } - - renderAnalytics(analytics) { - return ` -
-
-
${analytics.totalEmails}
-
Total Emails
-
-
-
${analytics.smtpEmails}
-
SMTP Sent
-
-
-
${analytics.mailtoClicks}
-
Mailto Clicks
-
-
-
${analytics.successfulEmails}
-
Successful
-
-
- -

By Government Level

-
- ${Object.entries(analytics.byLevel).map(([level, count]) => ` -
-
${count}
-
${level}
-
- `).join('')} -
- -

Recent Activity

-
- ${analytics.recentEmails.map(email => ` -
- ${email.user_name || 'Anonymous'} → - ${email.recipient_name} (${email.recipient_level}) -
- ${email.timestamp} • ${email.email_method} • ${email.status} -
- `).join('')} -
- `; - } - - showMessage(message, type) { - const container = document.getElementById('message-container'); - container.innerHTML = `
${message}
`; - container.classList.remove('hidden'); - - setTimeout(() => { - container.classList.add('hidden'); - }, 5000); - } - } - - // Global functions - function switchTab(tab) { - window.adminPanel.switchTab(tab); - } - - function resetForm() { - window.adminPanel.resetForm(); - } - - function closeAnalyticsModal() { - document.getElementById('analytics-modal').style.display = 'none'; - } - - // Initialize admin panel - document.addEventListener('DOMContentLoaded', () => { - window.adminPanel = new AdminPanel(); - }); - - - \ No newline at end of file diff --git a/influence/app/public/campaign.html b/influence/app/public/campaign.html index 12ae42b..7e767d5 100644 --- a/influence/app/public/campaign.html +++ b/influence/app/public/campaign.html @@ -321,351 +321,6 @@ - + \ No newline at end of file diff --git a/influence/app/public/js/campaign.js b/influence/app/public/js/campaign.js new file mode 100644 index 0000000..72494e8 --- /dev/null +++ b/influence/app/public/js/campaign.js @@ -0,0 +1,371 @@ +// Campaign Page Management Module +class CampaignPage { + constructor() { + this.campaign = null; + this.representatives = []; + this.userInfo = {}; + this.currentStep = 1; + this.init(); + } + + init() { + // Get campaign slug from URL + const pathParts = window.location.pathname.split('/'); + this.campaignSlug = pathParts[pathParts.length - 1]; + + // Set up form handlers + document.getElementById('user-info-form').addEventListener('submit', (e) => { + this.handleUserInfoSubmit(e); + }); + + // Postal code formatting + document.getElementById('user-postal-code').addEventListener('input', (e) => { + this.formatPostalCode(e); + }); + + // Load campaign data + this.loadCampaign(); + } + + async loadCampaign() { + this.showLoading('Loading campaign...'); + + try { + const response = await fetch(`/api/campaigns/${this.campaignSlug}`); + const data = await response.json(); + + if (!data.success) { + throw new Error(data.error || 'Failed to load campaign'); + } + + this.campaign = data.campaign; + this.renderCampaign(); + } catch (error) { + this.showError('Failed to load campaign: ' + error.message); + } finally { + this.hideLoading(); + } + } + + renderCampaign() { + // Update page title and header + document.title = `${this.campaign.title} - Alberta Influence Tool`; + document.getElementById('page-title').textContent = `${this.campaign.title} - Alberta Influence Tool`; + document.getElementById('campaign-title').textContent = this.campaign.title; + document.getElementById('campaign-description').textContent = this.campaign.description; + + // Show email count if enabled + if (this.campaign.show_email_count && this.campaign.emailCount !== null) { + document.getElementById('email-count').textContent = this.campaign.emailCount; + document.getElementById('campaign-stats').style.display = 'block'; + } + + // Show call to action + if (this.campaign.call_to_action) { + document.getElementById('call-to-action').innerHTML = `

${this.campaign.call_to_action}

`; + document.getElementById('call-to-action').style.display = 'block'; + } + + // Show email preview + document.getElementById('preview-subject').textContent = this.campaign.email_subject; + document.getElementById('preview-body').textContent = this.campaign.email_body; + document.getElementById('email-preview').style.display = 'block'; + + // Set up email method options + this.setupEmailMethodOptions(); + + // Set initial step + this.setStep(1); + } + + setupEmailMethodOptions() { + const emailMethodSection = document.getElementById('email-method-selection'); + const allowSMTP = this.campaign.allow_smtp_email; + const allowMailto = this.campaign.allow_mailto_link; + + if (!emailMethodSection) { + console.warn('Email method selection element not found'); + return; + } + + // Configure existing radio buttons instead of replacing HTML + const smtpRadio = document.getElementById('method-smtp'); + const mailtoRadio = document.getElementById('method-mailto'); + + if (allowSMTP && allowMailto) { + // Both methods allowed - keep default setup + smtpRadio.disabled = false; + mailtoRadio.disabled = false; + smtpRadio.checked = true; + } else if (allowSMTP && !allowMailto) { + // Only SMTP allowed + smtpRadio.disabled = false; + mailtoRadio.disabled = true; + smtpRadio.checked = true; + } else if (!allowSMTP && allowMailto) { + // Only mailto allowed + smtpRadio.disabled = true; + mailtoRadio.disabled = false; + mailtoRadio.checked = true; + } else { + // Neither allowed - hide the section + emailMethodSection.style.display = 'none'; + } + } + + formatPostalCode(e) { + let value = e.target.value.replace(/\s/g, '').toUpperCase(); + if (value.length > 3) { + value = value.substring(0, 3) + ' ' + value.substring(3, 6); + } + e.target.value = value; + } + + async handleUserInfoSubmit(e) { + e.preventDefault(); + + const formData = new FormData(e.target); + this.userInfo = { + postalCode: formData.get('postalCode').replace(/\s/g, '').toUpperCase(), + userName: formData.get('userName') || '', + userEmail: formData.get('userEmail') || '' + }; + + await this.loadRepresentatives(); + } + + async loadRepresentatives() { + this.showLoading('Finding your representatives...'); + + try { + const response = await fetch(`/api/campaigns/${this.campaignSlug}/representatives/${this.userInfo.postalCode}`); + const data = await response.json(); + + if (!data.success) { + throw new Error(data.error || 'Failed to load representatives'); + } + + this.representatives = data.representatives; + this.renderRepresentatives(); + this.setStep(2); + + // Scroll to representatives section + document.getElementById('representatives-section').scrollIntoView({ + behavior: 'smooth' + }); + } catch (error) { + this.showError('Failed to load representatives: ' + error.message); + } finally { + this.hideLoading(); + } + } + + renderRepresentatives() { + const list = document.getElementById('representatives-list'); + + if (this.representatives.length === 0) { + list.innerHTML = '

No representatives found for your area. Please check your postal code.

'; + return; + } + + list.innerHTML = this.representatives.map(rep => ` +
+
+ ${rep.photo_url ? + `${rep.name}` : + `
` + } +
+

${rep.name}

+

${rep.elected_office || 'Representative'}

+

${rep.party_name || ''}

+ ${rep.email ? `

📧 ${rep.email}

` : ''} +
+
+ ${rep.email ? ` +
+ +
+ ` : '

No email available

'} +
+ `).join(''); + + // Attach event listeners to send email buttons + this.attachEmailButtonListeners(); + + document.getElementById('representatives-section').style.display = 'block'; + } + + attachEmailButtonListeners() { + // Send email buttons + document.querySelectorAll('[data-action="send-email"]').forEach(btn => { + btn.addEventListener('click', (e) => { + const email = e.target.dataset.email; + const name = e.target.dataset.name; + const title = e.target.dataset.title; + const level = e.target.dataset.level; + this.sendEmail(email, name, title, level); + }); + }); + + // Reload page button + const reloadBtn = document.querySelector('[data-action="reload-page"]'); + if (reloadBtn) { + reloadBtn.addEventListener('click', () => { + location.reload(); + }); + } + } + + getGovernmentLevel(rep) { + const office = (rep.elected_office || '').toLowerCase(); + if (office.includes('mp') || office.includes('member of parliament')) return 'Federal'; + if (office.includes('mla') || office.includes('legislative assembly')) return 'Provincial'; + if (office.includes('mayor') || office.includes('councillor')) return 'Municipal'; + if (office.includes('school')) return 'School Board'; + return 'Other'; + } + + async sendEmail(recipientEmail, recipientName, recipientTitle, recipientLevel) { + const emailMethod = document.querySelector('input[name="emailMethod"]:checked').value; + + if (emailMethod === 'mailto') { + this.openMailtoLink(recipientEmail); + } else { + await this.sendSMTPEmail(recipientEmail, recipientName, recipientTitle, recipientLevel); + } + } + + openMailtoLink(recipientEmail) { + const subject = encodeURIComponent(this.campaign.email_subject); + const body = encodeURIComponent(this.campaign.email_body); + const mailtoUrl = `mailto:${recipientEmail}?subject=${subject}&body=${body}`; + + // Track the mailto click + this.trackEmail(recipientEmail, '', '', '', 'mailto'); + + window.open(mailtoUrl); + } + + async sendSMTPEmail(recipientEmail, recipientName, recipientTitle, recipientLevel) { + this.showLoading('Sending email...'); + + try { + const response = await fetch(`/api/campaigns/${this.campaignSlug}/send-email`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + userEmail: this.userInfo.userEmail, + userName: this.userInfo.userName, + postalCode: this.userInfo.postalCode, + recipientEmail, + recipientName, + recipientTitle, + recipientLevel, + emailMethod: 'smtp' + }) + }); + + const data = await response.json(); + + if (data.success) { + this.showSuccess('Email sent successfully!'); + } else { + throw new Error(data.error || 'Failed to send email'); + } + } catch (error) { + this.showError('Failed to send email: ' + error.message); + } finally { + this.hideLoading(); + } + } + + async trackEmail(recipientEmail, recipientName, recipientTitle, recipientLevel, emailMethod) { + try { + await fetch(`/api/campaigns/${this.campaignSlug}/send-email`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + userEmail: this.userInfo.userEmail, + userName: this.userInfo.userName, + postalCode: this.userInfo.postalCode, + recipientEmail, + recipientName, + recipientTitle, + recipientLevel, + emailMethod + }) + }); + } catch (error) { + console.error('Failed to track email:', error); + } + } + + setStep(step) { + // Reset all steps + document.querySelectorAll('.step').forEach(s => { + s.classList.remove('active', 'completed'); + }); + + // Mark completed steps + for (let i = 1; i < step; i++) { + document.getElementById(`step-${this.getStepName(i)}`).classList.add('completed'); + } + + // Mark current step + document.getElementById(`step-${this.getStepName(step)}`).classList.add('active'); + + this.currentStep = step; + } + + getStepName(step) { + const steps = ['', 'info', 'postal', 'send']; + return steps[step] || 'info'; + } + + showLoading(message) { + document.getElementById('loading-message').textContent = message; + document.getElementById('loading-overlay').style.display = 'flex'; + } + + hideLoading() { + document.getElementById('loading-overlay').style.display = 'none'; + } + + showError(message) { + const errorDiv = document.getElementById('error-message'); + errorDiv.textContent = message; + errorDiv.style.display = 'block'; + + setTimeout(() => { + errorDiv.style.display = 'none'; + }, 5000); + } + + showSuccess(message) { + // Update email count if enabled + if (this.campaign.show_email_count) { + const countElement = document.getElementById('email-count'); + const currentCount = parseInt(countElement.textContent) || 0; + countElement.textContent = currentCount + 1; + } + + // You could show a toast or update UI to indicate success + alert(message); // Simple for now, could be improved with better UI + } +} + +// Initialize the campaign page when DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + window.campaignPage = new CampaignPage(); +}); \ No newline at end of file diff --git a/influence/app/services/nocodb.js b/influence/app/services/nocodb.js index 0793231..f14e798 100644 --- a/influence/app/services/nocodb.js +++ b/influence/app/services/nocodb.js @@ -307,9 +307,25 @@ class NocoDBService { async updateCampaign(id, updates) { try { - // NocoDB update using direct API call + // Map field names to NocoDB column titles + const mappedUpdates = {}; + + if (updates.slug !== undefined) mappedUpdates['Campaign Slug'] = updates.slug; + if (updates.title !== undefined) mappedUpdates['Campaign Title'] = updates.title; + if (updates.description !== undefined) mappedUpdates['Description'] = updates.description; + if (updates.email_subject !== undefined) mappedUpdates['Email Subject'] = updates.email_subject; + if (updates.email_body !== undefined) mappedUpdates['Email Body'] = updates.email_body; + if (updates.call_to_action !== undefined) mappedUpdates['Call to Action'] = updates.call_to_action; + if (updates.status !== undefined) mappedUpdates['Status'] = updates.status; + if (updates.allow_smtp_email !== undefined) mappedUpdates['Allow SMTP Email'] = updates.allow_smtp_email; + if (updates.allow_mailto_link !== undefined) mappedUpdates['Allow Mailto Link'] = updates.allow_mailto_link; + if (updates.collect_user_info !== undefined) mappedUpdates['Collect User Info'] = updates.collect_user_info; + if (updates.show_email_count !== undefined) mappedUpdates['Show Email Count'] = updates.show_email_count; + if (updates.target_government_levels !== undefined) mappedUpdates['Target Government Levels'] = updates.target_government_levels; + if (updates.updated_at !== undefined) mappedUpdates['UpdatedAt'] = updates.updated_at; + const url = `${this.getTableUrl(this.tableIds.campaigns)}/${id}`; - const response = await this.client.patch(url, updates); + const response = await this.client.patch(url, mappedUpdates); return response.data; } catch (error) { console.error('Update campaign failed:', error); diff --git a/influence/app/services/nocodb.js.backup b/influence/app/services/nocodb.js.backup deleted file mode 100644 index 31e01eb..0000000 --- a/influence/app/services/nocodb.js.backup +++ /dev/null @@ -1,375 +0,0 @@ -const axios = require('axios'); - -class NocoDBService { - constructor() { - this.apiUrl = process.env.NOCODB_API_URL; - this.apiToken = process.env.NOCODB_API_TOKEN; - this.projectId = process.env.NOCODB_PROJECT_ID; - this.timeout = 10000; - - // Table mapping with actual table IDs from NocoDB - this.tableIds = { - representatives: 'm3slxjt2t9fspvn', - emails: 'mclckn23dlsiuvj', - postalCodes: 'mfsefv20htd6jy1', - campaigns: 'mrbky41y7nahz98', - campaignEmails: 'mlij85ls403d7c2' - }; - - // Create axios instance with defaults like the map service - this.client = axios.create({ - baseURL: this.apiUrl, - timeout: this.timeout, - headers: { - 'xc-token': this.apiToken, - 'Content-Type': 'application/json' - } - }); - - // Add response interceptor for error handling - this.client.interceptors.response.use( - response => response, - error => { - console.error('NocoDB API Error:', { - message: error.message, - url: error.config?.url, - method: error.config?.method, - status: error.response?.status, - data: error.response?.data - }); - throw error; - } - ); - } - - // Build table URL using table ID - getTableUrl(tableId) { - return `/db/data/v1/${this.projectId}/${tableId}`; - } - - // Get all records from a table - async getAll(tableId, params = {}) { - const url = this.getTableUrl(tableId); - const response = await this.client.get(url, { params }); - return response.data; - } - - // Create record - async create(tableId, data) { - try { - // Clean data to prevent ID conflicts - const cleanData = { ...data }; - delete cleanData.ID; - delete cleanData.id; - delete cleanData.Id; - - // Remove undefined values - Object.keys(cleanData).forEach(key => { - if (cleanData[key] === undefined) { - delete cleanData[key]; - } - }); - - const url = this.getTableUrl(tableId); - const response = await this.client.post(url, cleanData); - return response.data; - } catch (error) { - console.error('Error creating record:', error); - throw error; - } - } - - - - async storeRepresentatives(postalCode, representatives) { - try { - const stored = []; - - for (const rep of representatives) { - const record = { - postal_code: postalCode, - name: rep.name || '', - email: rep.email || '', - district_name: rep.district_name || '', - elected_office: rep.elected_office || '', - party_name: rep.party_name || '', - representative_set_name: rep.representative_set_name || '', - url: rep.url || '', - photo_url: rep.photo_url || '', - cached_at: new Date().toISOString() - }; - - const result = await this.create(this.tableIds.representatives, record); - stored.push(result); - } - - return { success: true, count: stored.length }; - } catch (error) { - // If we get a server error, don't throw - just log and return failure - if (error.response && error.response.status >= 500) { - console.log('NocoDB server unavailable, cannot cache representatives'); - return { success: false, error: 'Server unavailable' }; - } - - console.log('Error storing representatives:', error.response?.data?.msg || error.message); - return { success: false, error: error.message }; - } - } - - async getRepresentativesByPostalCode(postalCode) { - try { - // Try to query with the most likely column name - const response = await this.getAll(this.tableIds.representatives, { - where: `(postal_code,eq,${postalCode})` - }); - - return response.list || []; - } catch (error) { - // If we get a 502 or other server error, just return empty array - if (error.response && (error.response.status === 502 || error.response.status >= 500)) { - console.log('NocoDB server unavailable (502/5xx error), returning empty cache result'); - return []; - } - - // For other errors like column not found, also return empty array - console.log('NocoDB cache error, returning empty array:', error.response?.data?.msg || error.message); - return []; - } - } - - async clearRepresentativesByPostalCode(postalCode) { - try { - // Get existing records - const existing = await this.getRepresentativesByPostalCode(postalCode); - - // Delete each record using client - for (const record of existing) { - const url = `${this.getTableUrl(this.tableIds.representatives)}/${record.Id}`; - await this.client.delete(url); - } - - return { success: true, deleted: existing.length }; - } catch (error) { - console.error('Error clearing representatives:', error); - throw error; - } - } - - async logEmailSend(emailData) { - try { - const record = { - recipient_email: emailData.recipientEmail, - sender_name: emailData.senderName, - sender_email: emailData.senderEmail, - subject: emailData.subject, - postal_code: emailData.postalCode, - status: emailData.status, - sent_at: emailData.timestamp, - created_at: new Date().toISOString() - }; - - await this.create(this.tableIds.emails, record); - return { success: true }; - } catch (error) { - console.error('Error logging email:', error); - throw error; - } - } - - async getEmailLogs(filters = {}) { - try { - let whereClause = ''; - const conditions = []; - - if (filters.postalCode) { - conditions.push(`(postal_code,eq,${filters.postalCode})`); - } - if (filters.senderEmail) { - conditions.push(`(sender_email,eq,${filters.senderEmail})`); - } - if (filters.status) { - conditions.push(`(status,eq,${filters.status})`); - } - - if (conditions.length > 0) { - whereClause = `?where=${conditions.join('~and')}`; - } - - const params = {}; - if (conditions.length > 0) { - params.where = conditions.join('~and'); - } - params.sort = '-created_at'; - - const response = await this.getAll(this.tableIds.emails, params); - return response.list || []; - } catch (error) { - console.error('Error getting email logs:', error); - return []; - } - } - - async storePostalCodeInfo(postalCodeData) { - try { - const response = await this.create(this.tableIds.postalCodes, postalCodeData); - return response; - } catch (error) { - // Don't throw error for postal code caching failures - console.log('Postal code info storage failed:', error.message); - return null; - } - } - - // Campaign management methods - async getAllCampaigns() { - try { - const response = await this.getAll(this.tableIds.campaigns, { - sort: '-created_at' - }); - return response.list || []; - } catch (error) { - console.error('Get all campaigns failed:', error); - throw error; - } - } - - async getCampaignById(id) { - try { - const response = await this.getAll(this.tableIds.campaigns, { - where: `(id,eq,${id})` - }); - return response.list && response.list.length > 0 ? response.list[0] : null; - } catch (error) { - console.error('Get campaign by ID failed:', error); - throw error; - } - } - - async getCampaignBySlug(slug) { - try { - const response = await this.getAll(this.tableIds.campaigns, { - where: `(slug,eq,${slug})` - }); - return response.list && response.list.length > 0 ? response.list[0] : null; - } catch (error) { - console.error('Get campaign by slug failed:', error); - throw error; - } - } - - async createCampaign(campaignData) { - try { - const response = await this.create(this.tableIds.campaigns, campaignData); - return response; - } catch (error) { - console.error('Create campaign failed:', error); - throw error; - } - } - - async updateCampaign(id, updates) { - try { - // NocoDB update using direct API call - const url = `${this.getTableUrl(this.tableIds.campaigns)}/${id}`; - const response = await this.client.patch(url, updates); - return response.data; - } catch (error) { - console.error('Update campaign failed:', error); - throw error; - } - } - - async deleteCampaign(id) { - try { - const url = `${this.getTableUrl(this.tableIds.campaigns)}/${id}`; - const response = await this.client.delete(url); - return response.data; - } catch (error) { - console.error('Delete campaign failed:', error); - throw error; - } - } - - // Campaign email tracking methods - async logCampaignEmail(emailData) { - try { - const response = await this.create(this.tableIds.campaignEmails, emailData); - return response; - } catch (error) { - console.error('Log campaign email failed:', error); - throw error; - } - } - - async getCampaignEmailCount(campaignId) { - try { - const response = await this.getAll(this.tableIds.campaignEmails, { - where: `(campaign_id,eq,${campaignId})`, - limit: 1000 // Get enough to count - }); - return response.pageInfo ? response.pageInfo.totalRows : (response.list ? response.list.length : 0); - } catch (error) { - console.error('Get campaign email count failed:', error); - return 0; - } - } - - async getCampaignAnalytics(campaignId) { - try { - const response = await this.getAll(this.tableIds.campaignEmails, { - where: `(campaign_id,eq,${campaignId})`, - limit: 1000 - }); - - const emails = response.list || []; - - const analytics = { - totalEmails: emails.length, - smtpEmails: emails.filter(e => e.email_method === 'smtp').length, - mailtoClicks: emails.filter(e => e.email_method === 'mailto').length, - successfulEmails: emails.filter(e => e.status === 'sent' || e.status === 'clicked').length, - failedEmails: emails.filter(e => e.status === 'failed').length, - byLevel: {}, - byDate: {}, - recentEmails: emails.slice(0, 10).map(email => ({ - timestamp: email.timestamp, - user_name: email.user_name, - recipient_name: email.recipient_name, - recipient_level: email.recipient_level, - email_method: email.email_method, - status: email.status - })) - }; - - // Group by government level - emails.forEach(email => { - const level = email.recipient_level || 'Other'; - analytics.byLevel[level] = (analytics.byLevel[level] || 0) + 1; - }); - - // Group by date - emails.forEach(email => { - if (email.timestamp) { - const date = email.timestamp.split('T')[0]; // Get date part - analytics.byDate[date] = (analytics.byDate[date] || 0) + 1; - } - }); - - return analytics; - } catch (error) { - console.error('Get campaign analytics failed:', error); - return { - totalEmails: 0, - smtpEmails: 0, - mailtoClicks: 0, - successfulEmails: 0, - failedEmails: 0, - byLevel: {}, - byDate: {}, - recentEmails: [] - }; - } - } -} - -module.exports = new NocoDBService(); \ No newline at end of file