const nocoDB = require('../services/nocodb'); const emailService = require('../services/email'); const representAPI = require('../services/represent-api'); const { generateSlug, validateSlug } = require('../utils/validators'); class CampaignsController { // Get all campaigns (for admin panel) async getAllCampaigns(req, res, next) { try { const campaigns = await nocoDB.getAllCampaigns(); // Get email counts for each campaign 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 return { id, ...campaign, emailCount }; })); res.json({ success: true, campaigns: campaignsWithCounts }); } catch (error) { console.error('Get campaigns error:', error); res.status(500).json({ success: false, error: 'Failed to retrieve campaigns', message: error.message, details: error.response?.data || null }); } } // Get single campaign by ID (for admin) async getCampaignById(req, res, next) { try { const { id } = req.params; const campaign = await nocoDB.getCampaignById(id); if (!campaign) { return res.status(404).json({ success: false, error: 'Campaign not found' }); } const normalizedId = campaign.Id ?? campaign.id ?? id; const emailCount = await nocoDB.getCampaignEmailCount(normalizedId); res.json({ success: true, campaign: { id: normalizedId, ...campaign, emailCount } }); } catch (error) { console.error('Get campaign error:', error); res.status(500).json({ success: false, error: 'Failed to retrieve campaign', message: error.message, details: error.response?.data || null }); } } // Get campaign by slug (for public access) async getCampaignBySlug(req, res, next) { try { const { slug } = req.params; const campaign = await nocoDB.getCampaignBySlug(slug); if (!campaign) { return res.status(404).json({ success: false, error: 'Campaign not found' }); } if (campaign.status !== 'active') { return res.status(403).json({ success: false, error: 'Campaign is not currently active' }); } // Get email count if enabled let emailCount = null; if (campaign.show_email_count) { const id = campaign.Id ?? campaign.id; if (id != null) { emailCount = await nocoDB.getCampaignEmailCount(id); } } 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()) : []), emailCount } }); } catch (error) { console.error('Get campaign by slug error:', error); res.status(500).json({ success: false, error: 'Failed to retrieve campaign', message: error.message, details: error.response?.data || null }); } } // Create new campaign async createCampaign(req, res, next) { try { const { title, description, email_subject, email_body, call_to_action, allow_smtp_email = true, allow_mailto_link = true, collect_user_info = true, show_email_count = true, target_government_levels = ['Federal', 'Provincial', 'Municipal'] } = req.body; // Generate slug from title let slug = generateSlug(title); // Ensure slug is unique let counter = 1; let originalSlug = slug; while (await nocoDB.getCampaignBySlug(slug)) { slug = `${originalSlug}-${counter}`; counter++; } const campaignData = { slug, title, description, email_subject, email_body, call_to_action, status: 'draft', allow_smtp_email, allow_mailto_link, collect_user_info, show_email_count, // NocoDB MultiSelect expects an array of values target_government_levels: Array.isArray(target_government_levels) ? target_government_levels : (typeof target_government_levels === 'string' && target_government_levels.length > 0 ? target_government_levels.split(',').map(s => s.trim()) : []) }; const campaign = await nocoDB.createCampaign(campaignData); res.status(201).json({ success: true, campaign: { id: campaign.Id ?? campaign.id, ...campaign } }); } catch (error) { console.error('Create campaign error:', error); res.status(500).json({ success: false, error: 'Failed to create campaign', message: error.message, details: error.response?.data || null }); } } // Update campaign async updateCampaign(req, res, next) { try { const { id } = req.params; const updates = req.body; // If title is being updated, regenerate slug if (updates.title) { let slug = generateSlug(updates.title); // Ensure slug is unique (but allow current campaign to keep its slug) const existingCampaign = await nocoDB.getCampaignBySlug(slug); if (existingCampaign && existingCampaign.id !== parseInt(id)) { let counter = 1; let originalSlug = slug; while (await nocoDB.getCampaignBySlug(slug)) { slug = `${originalSlug}-${counter}`; counter++; } } updates.slug = slug; } // Ensure target_government_levels remains an array for MultiSelect if (updates.target_government_levels) { updates.target_government_levels = Array.isArray(updates.target_government_levels) ? updates.target_government_levels : (typeof updates.target_government_levels === 'string' ? updates.target_government_levels.split(',').map(s => s.trim()) : []); } updates.updated_at = new Date().toISOString(); const campaign = await nocoDB.updateCampaign(id, updates); res.json({ success: true, campaign: { id: campaign.Id ?? campaign.id ?? id, ...campaign } }); } catch (error) { console.error('Update campaign error:', error); res.status(500).json({ success: false, error: 'Failed to update campaign', message: error.message, details: error.response?.data || null }); } } // Delete campaign async deleteCampaign(req, res, next) { try { const { id } = req.params; await nocoDB.deleteCampaign(id); res.json({ success: true, message: 'Campaign deleted successfully' }); } catch (error) { console.error('Delete campaign error:', error); res.status(500).json({ success: false, error: 'Failed to delete campaign', message: error.message, details: error.response?.data || null }); } } // Send campaign email async sendCampaignEmail(req, res, next) { try { const { slug } = req.params; const { userEmail, userName, postalCode, recipientEmail, recipientName, recipientTitle, recipientLevel, emailMethod = 'smtp' } = req.body; // Get campaign const campaign = await nocoDB.getCampaignBySlug(slug); if (!campaign) { return res.status(404).json({ success: false, error: 'Campaign not found' }); } if (campaign.status !== 'active') { return res.status(403).json({ success: false, error: 'Campaign is not currently active' }); } // Check if the requested email method is allowed if (emailMethod === 'smtp' && !campaign.allow_smtp_email) { return res.status(403).json({ success: false, error: 'SMTP email sending is not enabled for this campaign' }); } if (emailMethod === 'mailto' && !campaign.allow_mailto_link) { return res.status(403).json({ success: false, error: 'Mailto links are not enabled for this campaign' }); } const subject = campaign.email_subject; const message = campaign.email_body; let emailResult = { success: true }; // Send email if SMTP method if (emailMethod === 'smtp') { emailResult = await emailService.sendEmail({ to: recipientEmail, from: { email: process.env.SMTP_FROM_EMAIL, name: process.env.SMTP_FROM_NAME }, replyTo: userEmail, subject: subject, text: message, html: `

${message.replace(/\n/g, '
')}


This message was sent via the Alberta Influence Campaign Tool by ${userName || 'A constituent'} (${userEmail}) from postal code ${postalCode} as part of the "${campaign.title}" campaign.

` }); } // Log the campaign email await nocoDB.logCampaignEmail({ campaign_id: campaign.Id ?? campaign.id, campaign_slug: slug, user_email: userEmail, user_name: userName, user_postal_code: postalCode, recipient_email: recipientEmail, recipient_name: recipientName, recipient_title: recipientTitle, recipient_level: recipientLevel, email_method: emailMethod, subject: subject, message: message, status: emailMethod === 'mailto' ? 'clicked' : (emailResult.success ? 'sent' : 'failed'), timestamp: new Date().toISOString() }); if (emailMethod === 'smtp') { if (emailResult.success) { res.json({ success: true, message: 'Email sent successfully' }); } else { res.status(500).json({ success: false, error: 'Failed to send email', message: emailResult.error }); } } else { // For mailto, just return success since we're tracking the click res.json({ success: true, message: 'Email action tracked' }); } } catch (error) { console.error('Send campaign email error:', error); res.status(500).json({ success: false, error: 'Failed to process campaign email', message: error.message, details: error.response?.data || null }); } } // Get representatives for postal code (for campaign use) async getRepresentativesForCampaign(req, res, next) { try { const { slug, postalCode } = req.params; // Get campaign to check target levels const campaign = await nocoDB.getCampaignBySlug(slug); if (!campaign) { return res.status(404).json({ success: false, error: 'Campaign not found' }); } if (campaign.status !== 'active') { return res.status(403).json({ success: false, error: 'Campaign is not currently active' }); } // Get representatives const result = await representAPI.getRepresentativesByPostalCode(postalCode); if (!result.success) { return res.status(result.status || 500).json(result); } // 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()) : ['Federal', 'Provincial', 'Municipal']); const filteredRepresentatives = result.representatives.filter(rep => { const repLevel = rep.elected_office ? rep.elected_office.toLowerCase() : 'other'; return targetLevels.some(targetLevel => { const target = targetLevel.toLowerCase(); if (target === 'federal' && (repLevel.includes('mp') || repLevel.includes('member of parliament'))) { return true; } if (target === 'provincial' && (repLevel.includes('mla') || repLevel.includes('legislative assembly'))) { return true; } if (target === 'municipal' && (repLevel.includes('mayor') || repLevel.includes('councillor') || repLevel.includes('council'))) { return true; } if (target === 'school board' && repLevel.includes('school')) { return true; } return false; }); }); res.json({ success: true, representatives: filteredRepresentatives, location: result.location }); } catch (error) { console.error('Get representatives for campaign error:', error); res.status(500).json({ success: false, error: 'Failed to get representatives', message: error.message, details: error.response?.data || null }); } } // Get campaign analytics async getCampaignAnalytics(req, res, next) { try { const { id } = req.params; const analytics = await nocoDB.getCampaignAnalytics(id); res.json({ success: true, analytics }); } catch (error) { console.error('Get campaign analytics error:', error); res.status(500).json({ success: false, error: 'Failed to get campaign analytics', message: error.message, details: error.response?.data || null }); } } } module.exports = new CampaignsController();