837 lines
31 KiB
JavaScript
837 lines
31 KiB
JavaScript
const nocoDB = require('../services/nocodb');
|
|
const emailService = require('../services/email');
|
|
const representAPI = require('../services/represent-api');
|
|
const { generateSlug, validateSlug } = require('../utils/validators');
|
|
|
|
const VALID_CAMPAIGN_STATUSES = ['draft', 'active', 'paused', 'archived'];
|
|
|
|
const normalizeTargetLevels = (rawLevels) => {
|
|
if (Array.isArray(rawLevels)) {
|
|
return rawLevels;
|
|
}
|
|
|
|
if (typeof rawLevels === 'string' && rawLevels.length > 0) {
|
|
return rawLevels.split(',').map(level => level.trim()).filter(Boolean);
|
|
}
|
|
|
|
return [];
|
|
};
|
|
|
|
const normalizeStatus = (rawStatus, fallback = 'draft') => {
|
|
if (!rawStatus) {
|
|
return fallback;
|
|
}
|
|
|
|
const status = String(rawStatus).toLowerCase();
|
|
return VALID_CAMPAIGN_STATUSES.includes(status) ? status : fallback;
|
|
};
|
|
|
|
// Helper function to cache representatives
|
|
async function cacheRepresentatives(postalCode, representatives, representData) {
|
|
try {
|
|
// Cache the postal code info
|
|
await nocoDB.storePostalCodeInfo({
|
|
postal_code: postalCode,
|
|
city: representData.city,
|
|
province: representData.province
|
|
});
|
|
|
|
// Cache representatives using the existing method
|
|
const result = await nocoDB.storeRepresentatives(postalCode, representatives);
|
|
|
|
if (result.success) {
|
|
console.log(`Successfully cached ${result.count} representatives for ${postalCode}`);
|
|
} else {
|
|
console.log(`Partial success caching representatives for ${postalCode}: ${result.error || 'unknown error'}`);
|
|
}
|
|
} catch (error) {
|
|
console.log(`Failed to cache representatives for ${postalCode}:`, error.message);
|
|
// Don't throw - caching is optional and should never break the main flow
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
const rawTargetLevels = campaign['Target Government Levels'] || campaign.target_government_levels;
|
|
const normalizedTargetLevels = normalizeTargetLevels(rawTargetLevels);
|
|
|
|
// 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: normalizeStatus(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,
|
|
allow_email_editing: campaign['Allow Email Editing'] || campaign.allow_email_editing,
|
|
target_government_levels: normalizedTargetLevels,
|
|
created_at: campaign.CreatedAt || campaign.created_at,
|
|
updated_at: campaign.UpdatedAt || campaign.updated_at,
|
|
created_by_user_id: campaign['Created By User ID'] || campaign.created_by_user_id || null,
|
|
created_by_user_email: campaign['Created By User Email'] || campaign.created_by_user_email || null,
|
|
created_by_user_name: campaign['Created By User Name'] || campaign.created_by_user_name || null,
|
|
emailCount
|
|
};
|
|
}));
|
|
|
|
let filteredCampaigns = campaignsWithCounts;
|
|
|
|
if (req.user && !req.user.isAdmin) {
|
|
const sessionUserId = req.user.id != null ? String(req.user.id) : (req.session?.userId != null ? String(req.session.userId) : null);
|
|
const sessionUserEmail = req.user.email ? String(req.user.email).toLowerCase() : (req.session?.userEmail ? String(req.session.userEmail).toLowerCase() : null);
|
|
|
|
filteredCampaigns = campaignsWithCounts.filter((campaign) => {
|
|
const createdById = campaign.created_by_user_id != null ? String(campaign.created_by_user_id) : null;
|
|
const createdByEmail = campaign.created_by_user_email ? String(campaign.created_by_user_email).toLowerCase() : null;
|
|
|
|
return (
|
|
(sessionUserId && createdById && createdById === sessionUserId) ||
|
|
(sessionUserEmail && createdByEmail && createdByEmail === sessionUserEmail)
|
|
);
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
campaigns: filteredCampaigns
|
|
});
|
|
} 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: normalizeStatus(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,
|
|
allow_email_editing: campaign['Allow Email Editing'] || campaign.allow_email_editing,
|
|
target_government_levels: normalizeTargetLevels(campaign['Target Government Levels'] || campaign.target_government_levels),
|
|
created_at: campaign.CreatedAt || campaign.created_at,
|
|
updated_at: campaign.UpdatedAt || campaign.updated_at,
|
|
created_by_user_id: campaign['Created By User ID'] || campaign.created_by_user_id || null,
|
|
created_by_user_email: campaign['Created By User Email'] || campaign.created_by_user_email || null,
|
|
created_by_user_name: campaign['Created By User Name'] || campaign.created_by_user_name || null,
|
|
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 = normalizeStatus(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,
|
|
allow_email_editing: campaign['Allow Email Editing'] || campaign.allow_email_editing,
|
|
target_government_levels: normalizeTargetLevels(campaign['Target Government Levels'] || campaign.target_government_levels),
|
|
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,
|
|
status = 'draft',
|
|
allow_smtp_email = true,
|
|
allow_mailto_link = true,
|
|
collect_user_info = true,
|
|
show_email_count = true,
|
|
allow_email_editing = false,
|
|
target_government_levels = ['Federal', 'Provincial', 'Municipal']
|
|
} = req.body;
|
|
|
|
const ownerUserId = req.user?.id ?? req.session?.userId ?? null;
|
|
const ownerEmail = req.user?.email ?? req.session?.userEmail ?? null;
|
|
const ownerName = req.user?.name ?? req.session?.userName ?? null;
|
|
const normalizedStatus = normalizeStatus(status, 'draft');
|
|
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: normalizedStatus,
|
|
allow_smtp_email,
|
|
allow_mailto_link,
|
|
collect_user_info,
|
|
show_email_count,
|
|
allow_email_editing,
|
|
// NocoDB MultiSelect expects an array of values
|
|
target_government_levels: normalizeTargetLevels(target_government_levels),
|
|
// Add user ownership data
|
|
created_by_user_id: ownerUserId,
|
|
created_by_user_email: ownerEmail,
|
|
created_by_user_name: ownerName
|
|
};
|
|
|
|
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: normalizeStatus(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,
|
|
allow_email_editing: campaign['Allow Email Editing'] || campaign.allow_email_editing,
|
|
target_government_levels: normalizeTargetLevels(campaign['Target Government Levels'] || campaign.target_government_levels),
|
|
created_at: campaign.CreatedAt || campaign.created_at,
|
|
updated_at: campaign.UpdatedAt || campaign.updated_at,
|
|
created_by_user_id: campaign['Created By User ID'] || campaign.created_by_user_id || ownerUserId,
|
|
created_by_user_email: campaign['Created By User Email'] || campaign.created_by_user_email || ownerEmail,
|
|
created_by_user_name: campaign['Created By User Name'] || campaign.created_by_user_name || ownerName
|
|
}
|
|
});
|
|
} 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 };
|
|
|
|
const existingCampaign = await nocoDB.getCampaignById(id);
|
|
if (!existingCampaign) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Campaign not found'
|
|
});
|
|
}
|
|
|
|
const isAdmin = (req.user && req.user.isAdmin) || req.session?.isAdmin || false;
|
|
const sessionUserId = req.user?.id ?? req.session?.userId ?? null;
|
|
const sessionUserEmail = req.user?.email ?? req.session?.userEmail ?? null;
|
|
|
|
if (!isAdmin) {
|
|
const createdById = existingCampaign['Created By User ID'] ?? existingCampaign.created_by_user_id ?? null;
|
|
const createdByEmail = existingCampaign['Created By User Email'] ?? existingCampaign.created_by_user_email ?? null;
|
|
|
|
const ownsCampaign = (
|
|
(createdById != null && sessionUserId != null && String(createdById) === String(sessionUserId)) ||
|
|
(createdByEmail && sessionUserEmail && String(createdByEmail).toLowerCase() === String(sessionUserEmail).toLowerCase())
|
|
);
|
|
|
|
if (!ownsCampaign) {
|
|
return res.status(403).json({
|
|
success: false,
|
|
error: 'Access denied'
|
|
});
|
|
}
|
|
}
|
|
|
|
if (updates.title) {
|
|
let slug = generateSlug(updates.title);
|
|
|
|
const campaignWithSlug = await nocoDB.getCampaignBySlug(slug);
|
|
const existingId = campaignWithSlug ? (campaignWithSlug.ID || campaignWithSlug.Id || campaignWithSlug.id) : null;
|
|
if (campaignWithSlug && String(existingId) !== String(id)) {
|
|
let counter = 1;
|
|
const originalSlug = slug;
|
|
while (await nocoDB.getCampaignBySlug(slug)) {
|
|
slug = `${originalSlug}-${counter}`;
|
|
counter++;
|
|
}
|
|
}
|
|
updates.slug = slug;
|
|
}
|
|
|
|
if (updates.target_government_levels !== undefined) {
|
|
updates.target_government_levels = normalizeTargetLevels(updates.target_government_levels);
|
|
}
|
|
|
|
if (updates.status !== undefined) {
|
|
const sanitizedStatus = normalizeStatus(updates.status, null);
|
|
if (!sanitizedStatus) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Invalid campaign status'
|
|
});
|
|
}
|
|
updates.status = sanitizedStatus;
|
|
}
|
|
|
|
// Remove user ownership fields from updates to prevent unauthorized changes
|
|
delete updates.created_by_user_id;
|
|
delete updates.created_by_user_email;
|
|
delete updates.created_by_user_name;
|
|
// Remove auto-generated fields that NocoDB handles
|
|
delete updates.updated_at;
|
|
delete updates.UpdatedAt;
|
|
delete updates.created_at;
|
|
delete updates.CreatedAt;
|
|
|
|
const campaign = await nocoDB.updateCampaign(id, updates);
|
|
|
|
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: normalizeStatus(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,
|
|
allow_email_editing: campaign['Allow Email Editing'] || campaign.allow_email_editing,
|
|
target_government_levels: normalizeTargetLevels(campaign['Target Government Levels'] || campaign.target_government_levels),
|
|
created_at: campaign.CreatedAt || campaign.created_at,
|
|
updated_at: campaign.UpdatedAt || campaign.updated_at,
|
|
created_by_user_id: campaign['Created By User ID'] || campaign.created_by_user_id || null,
|
|
created_by_user_email: campaign['Created By User Email'] || campaign.created_by_user_email || null,
|
|
created_by_user_name: campaign['Created By User Name'] || campaign.created_by_user_name || null
|
|
}
|
|
});
|
|
} 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',
|
|
customEmailSubject,
|
|
customEmailBody
|
|
} = 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 = normalizeStatus(campaign['Status'] || campaign.status);
|
|
if (campaignStatus !== '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'
|
|
});
|
|
}
|
|
|
|
// Use custom email content if provided (when email editing is enabled), otherwise use campaign defaults
|
|
const allowEmailEditing = campaign['Allow Email Editing'] || campaign.allow_email_editing;
|
|
const subject = (allowEmailEditing && customEmailSubject)
|
|
? customEmailSubject
|
|
: (campaign['Email Subject'] || campaign.email_subject);
|
|
const message = (allowEmailEditing && customEmailBody)
|
|
? customEmailBody
|
|
: (campaign['Email Body'] || campaign.email_body);
|
|
|
|
let emailResult = { success: true };
|
|
|
|
// Send email if SMTP method
|
|
if (emailMethod === 'smtp') {
|
|
console.log('DEBUG: About to send campaign email...');
|
|
emailResult = await emailService.sendCampaignEmail(
|
|
recipientEmail,
|
|
userEmail,
|
|
userName || 'A constituent',
|
|
postalCode,
|
|
subject,
|
|
message,
|
|
campaign['Campaign Title'] || campaign.title,
|
|
recipientName,
|
|
recipientLevel
|
|
);
|
|
console.log('DEBUG: Campaign email service returned:', emailResult);
|
|
}
|
|
|
|
// Log the campaign email
|
|
console.log('DEBUG: About to log campaign email with emailResult:', emailResult);
|
|
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')
|
|
});
|
|
console.log('DEBUG: Campaign email logged with 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 = normalizeStatus(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 = normalizeStatus(campaign['Status'] || campaign.status);
|
|
if (campaignStatus !== 'active') {
|
|
return res.status(403).json({
|
|
success: false,
|
|
error: 'Campaign is not currently active'
|
|
});
|
|
}
|
|
|
|
// First check cache for representatives
|
|
const formattedPostalCode = postalCode.replace(/\s/g, '').toUpperCase();
|
|
let representatives = [];
|
|
let result = null;
|
|
|
|
// Try to check cached data first, but don't fail if NocoDB is down
|
|
let cachedData = [];
|
|
try {
|
|
cachedData = await nocoDB.getRepresentativesByPostalCode(formattedPostalCode);
|
|
console.log(`Cache check for ${formattedPostalCode}: found ${cachedData.length} records`);
|
|
|
|
if (cachedData && cachedData.length > 0) {
|
|
representatives = cachedData;
|
|
console.log(`Using cached representatives for ${formattedPostalCode}`);
|
|
}
|
|
} catch (cacheError) {
|
|
console.log(`Cache unavailable for ${formattedPostalCode}, proceeding with API call:`, cacheError.message);
|
|
}
|
|
|
|
// If not in cache, fetch from Represent API
|
|
if (representatives.length === 0) {
|
|
console.log(`Fetching representatives from Represent API for ${formattedPostalCode}`);
|
|
result = await representAPI.getRepresentativesByPostalCode(postalCode);
|
|
|
|
// Process representatives from both concordance and centroid
|
|
// 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);
|
|
}
|
|
|
|
// Cache the results if we got them from the API
|
|
if (representatives.length > 0 && result) {
|
|
console.log(`Attempting to cache ${representatives.length} representatives for ${formattedPostalCode}`);
|
|
await cacheRepresentatives(formattedPostalCode, representatives, result);
|
|
}
|
|
}
|
|
|
|
if (representatives.length === 0) {
|
|
return res.json({
|
|
success: false,
|
|
message: 'No representatives found for this postal code',
|
|
representatives: [],
|
|
location: {
|
|
city: result?.city || 'Alberta',
|
|
province: result?.province || 'AB'
|
|
}
|
|
});
|
|
}
|
|
|
|
// 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 || cachedData[0]?.city || 'Alberta',
|
|
province: result?.province || cachedData[0]?.province || 'AB'
|
|
}
|
|
});
|
|
|
|
} 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 campaign = await nocoDB.getCampaignById(id);
|
|
|
|
if (!campaign) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Campaign not found'
|
|
});
|
|
}
|
|
|
|
const sessionUserId = req.user?.id ?? req.session?.userId ?? null;
|
|
const sessionUserEmail = req.user?.email ?? req.session?.userEmail ?? null;
|
|
const isAdmin = (req.user && req.user.isAdmin) || req.session?.isAdmin || false;
|
|
|
|
if (!isAdmin) {
|
|
const createdById = campaign['Created By User ID'] ?? campaign.created_by_user_id ?? null;
|
|
const createdByEmail = campaign['Created By User Email'] ?? campaign.created_by_user_email ?? null;
|
|
|
|
const ownsCampaign = (
|
|
(createdById != null && sessionUserId != null && String(createdById) === String(sessionUserId)) ||
|
|
(createdByEmail && sessionUserEmail && String(createdByEmail).toLowerCase() === String(sessionUserEmail).toLowerCase())
|
|
);
|
|
|
|
if (!ownsCampaign) {
|
|
return res.status(403).json({
|
|
success: false,
|
|
error: 'Access denied'
|
|
});
|
|
}
|
|
}
|
|
|
|
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(); |