648 lines
23 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 and normalize data structure
const campaignsWithCounts = await Promise.all(campaigns.map(async (campaign) => {
const id = campaign.ID || campaign.Id || campaign.id;
let emailCount = 0;
if (id != null) {
emailCount = await nocoDB.getCampaignEmailCount(id);
}
// Normalize campaign data structure for frontend
return {
id,
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
};
}));
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'
});
}
// Debug logging
console.log('Campaign object keys:', Object.keys(campaign));
console.log('Campaign ID field:', campaign.ID, campaign.Id, campaign.id);
const normalizedId = campaign.ID || campaign.Id || campaign.id;
console.log('Using normalized ID:', normalizedId);
const emailCount = await nocoDB.getCampaignEmailCount(normalizedId);
// Normalize campaign data structure for frontend
res.json({
success: true,
campaign: {
id: normalizedId,
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
}
});
} 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'
});
}
const campaignStatus = campaign['Status'] || campaign.status;
if (campaignStatus !== 'active') {
return res.status(403).json({
success: false,
error: 'Campaign is not currently active'
});
}
// Get email count if enabled
let emailCount = null;
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);
}
}
res.json({
success: true,
campaign: {
id: campaign.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
}
});
} 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);
// Normalize the created campaign data
res.status(201).json({
success: true,
campaign: {
id: campaign.ID || campaign.Id || campaign.id,
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) {
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);
const existingId = existingCampaign ? (existingCampaign.ID || existingCampaign.Id || existingCampaign.id) : null;
if (existingCampaign && String(existingId) !== String(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);
// Normalize the updated campaign data
res.json({
success: true,
campaign: {
id: campaign.ID || campaign.Id || campaign.id,
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) {
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'] || 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'] || 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'] || 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'] || campaign.email_subject;
const message = campaign['Email Body'] || 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 BNKops Influence Campaign Tool by ${userName || 'A constituent'} (${userEmail}) from postal code ${postalCode} as part of the "${campaign['Campaign Title'] || campaign.title}" campaign.</small></p>
`
});
}
// Log the campaign email
await nocoDB.logCampaignEmail({
campaign_id: 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')
});
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
});
}
}
// Track user info when they find representatives
async trackUserInfo(req, res, next) {
try {
const { slug } = req.params;
const { userEmail, userName, postalCode } = req.body;
// Get campaign
const campaign = await nocoDB.getCampaignBySlug(slug);
if (!campaign) {
return res.status(404).json({
success: false,
error: 'Campaign not found'
});
}
const campaignStatus = campaign['Status'] || campaign.status;
if (campaignStatus !== 'active') {
return res.status(403).json({
success: false,
error: 'Campaign is not currently active'
});
}
// Log user interaction - finding representatives
await nocoDB.logCampaignEmail({
campaign_id: campaign.ID || campaign.Id || campaign.id,
campaign_slug: slug,
user_email: userEmail || '',
user_name: userName || '',
user_postal_code: postalCode,
recipient_email: '',
recipient_name: '',
recipient_title: '',
recipient_level: 'Other',
email_method: 'smtp', // Use valid option but distinguish by status
subject: 'User Info Capture',
message: 'User searched for representatives',
status: 'user_info_captured'
});
res.json({
success: true,
message: 'User info tracked successfully'
});
} catch (error) {
console.error('Track user info error:', error);
res.status(500).json({
success: false,
error: 'Failed to track user info',
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'
});
}
const campaignStatus = campaign['Status'] || campaign.status;
if (campaignStatus !== 'active') {
return res.status(403).json({
success: false,
error: 'Campaign is not currently active'
});
}
// Get representatives
const result = await representAPI.getRepresentativesByPostalCode(postalCode);
// 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 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 = 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: {
city: result.city,
province: result.province
}
});
} 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();