500 lines
15 KiB
JavaScript
500 lines
15 KiB
JavaScript
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: `
|
|
<p>${message.replace(/\n/g, '<br>')}</p>
|
|
<hr>
|
|
<p><small>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.</small></p>
|
|
`
|
|
});
|
|
}
|
|
|
|
// 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(); |