const nocoDB = require('../services/nocodb'); const emailService = require('../services/email'); const representAPI = require('../services/represent-api'); const { generateSlug, validateSlug } = require('../utils/validators'); const multer = require('multer'); const path = require('path'); const fs = require('fs'); // Configure multer for file uploads const storage = multer.diskStorage({ destination: function (req, file, cb) { const uploadDir = path.join(__dirname, '../public/uploads'); // Ensure the upload directory exists if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } cb(null, uploadDir); }, filename: function (req, file, cb) { // Generate unique filename with timestamp const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); cb(null, 'cover-' + uniqueSuffix + path.extname(file.originalname)); } }); const upload = multer({ storage: storage, limits: { fileSize: 5 * 1024 * 1024 // 5MB limit }, fileFilter: function (req, file, cb) { // Accept only image files const allowedTypes = /jpeg|jpg|png|gif|webp/; const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase()); const mimetype = allowedTypes.test(file.mimetype); if (mimetype && extname) { return cb(null, true); } else { cb(new Error('Only image files are allowed (jpeg, jpg, png, gif, webp)')); } } }); 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 public campaigns (no authentication required) async getPublicCampaigns(req, res, next) { try { const campaigns = await nocoDB.getAllCampaigns(); // Filter to only active campaigns and normalize data structure const activeCampaigns = await Promise.all( campaigns .filter(campaign => { const status = normalizeStatus(campaign['Status'] || campaign.status); return status === 'active'; }) .map(async (campaign) => { const id = campaign.ID || campaign.Id || campaign.id; // Debug: Log specific fields we're looking for console.log(`Campaign ${id}:`, { 'Show Call Count': campaign['Show Call Count'], 'show_call_count': campaign.show_call_count, 'Show Email Count': campaign['Show Email Count'], 'show_email_count': campaign.show_email_count }); // Get email count if show_email_count is enabled let emailCount = null; const showEmailCount = campaign['Show Email Count'] || campaign.show_email_count; console.log(`Getting email count for campaign ID: ${id}, showEmailCount: ${showEmailCount}`); if (showEmailCount && id != null) { emailCount = await nocoDB.getCampaignEmailCount(id); console.log(`Email count result: ${emailCount}`); } // Get call count if show_call_count is enabled let callCount = null; const showCallCount = campaign['Show Call Count'] || campaign.show_call_count; console.log(`Getting call count for campaign ID: ${id}, showCallCount: ${showCallCount}`); if (showCallCount && id != null) { callCount = await nocoDB.getCampaignCallCount(id); console.log(`Call count result: ${callCount}`); } const rawTargetLevels = campaign['Target Government Levels'] || campaign.target_government_levels; const normalizedTargetLevels = normalizeTargetLevels(rawTargetLevels); // Return only public-facing information return { 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, cover_photo: campaign['Cover Photo'] || campaign.cover_photo, show_email_count: showEmailCount, show_call_count: showCallCount, target_government_levels: normalizedTargetLevels, created_at: campaign.CreatedAt || campaign.created_at, emailCount, callCount }; }) ); res.json({ success: true, campaigns: activeCampaigns }); } catch (error) { console.error('Get public campaigns error:', error); res.status(500).json({ success: false, error: 'Failed to retrieve campaigns', message: error.message }); } } // 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, cover_photo: campaign['Cover Photo'] || campaign.cover_photo, 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, cover_photo: campaign['Cover Photo'] || campaign.cover_photo, 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 || campaign.id; console.log('Getting email count for campaign ID:', id); if (id != null) { emailCount = await nocoDB.getCampaignEmailCount(id); console.log('Email count result:', emailCount); } } // Get call count if enabled let callCount = null; const showCallCount = campaign['Show Call Count'] || campaign.show_call_count; if (showCallCount) { const id = campaign.ID || campaign.Id || campaign.id; console.log('Getting call count for campaign ID:', id); if (id != null) { callCount = await nocoDB.getCampaignCallCount(id); console.log('Call count result:', callCount); } } // Debug cover photo value const coverPhoto = campaign['Cover Photo'] || campaign.cover_photo; console.log('Raw cover_photo from NocoDB:', coverPhoto, 'Type:', typeof coverPhoto); 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, cover_photo: coverPhoto || null, 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, show_call_count: campaign['Show Call Count'] || campaign.show_call_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, callCount } }); } 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, // Add cover photo if uploaded cover_photo: req.file ? req.file.filename : null }; 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, cover_photo: campaign['Cover Photo'] || campaign.cover_photo, 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' }); } } // Track old slug for cascade updates const oldSlug = existingCampaign['Campaign Slug'] || existingCampaign.slug; let newSlug = oldSlug; 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; newSlug = slug; } if (updates.target_government_levels !== undefined) { updates.target_government_levels = normalizeTargetLevels(updates.target_government_levels); } // Handle cover photo upload if (req.file) { console.log('Cover photo file received:', req.file.filename); updates.cover_photo = req.file.filename; } else { console.log('No cover photo file in request'); } console.log('Updates object before saving:', updates); 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); // If slug changed, update references in related tables if (oldSlug && newSlug && oldSlug !== newSlug) { console.log(`Campaign slug changed from '${oldSlug}' to '${newSlug}', updating references...`); const cascadeResult = await nocoDB.updateCampaignSlugReferences(id, oldSlug, newSlug); if (cascadeResult.success) { console.log(`Successfully updated slug references: ${cascadeResult.updatedCampaignEmails} campaign emails, ${cascadeResult.updatedCallLogs} call logs`); } else { console.warn(`Failed to update some slug references:`, cascadeResult.error); // Don't fail the main update - cascade is a best-effort operation } } 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, cover_photo: campaign['Cover Photo'] || campaign.cover_photo, 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 }); } } // Track campaign phone call async trackCampaignCall(req, res, next) { try { const { slug } = req.params; const { representativeName, representativeTitle, phoneNumber, officeType, userEmail, userName, postalCode } = req.body; // Validate required fields if (!representativeName || !phoneNumber) { return res.status(400).json({ success: false, error: 'Representative name and phone number are required' }); } // 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 the call await nocoDB.logCall({ representativeName, representativeTitle: representativeTitle || null, phoneNumber, officeType: officeType || null, callerName: userName || null, callerEmail: userEmail || null, postalCode: postalCode || null, campaignId: campaign.ID || campaign.Id || campaign.id, campaignSlug: slug, callerIP: req.ip || req.connection?.remoteAddress || null, timestamp: new Date().toISOString() }); res.json({ success: true, message: 'Call tracked successfully' }); } catch (error) { console.error('Track campaign call error:', error); res.status(500).json({ success: false, error: 'Failed to track call', message: error.message }); } } } // Export controller instance and upload middleware const controller = new CampaignsController(); controller.upload = upload; module.exports = controller;