diff --git a/influence/README.MD b/influence/README.MD index 1b0b472..546c64d 100644 --- a/influence/README.MD +++ b/influence/README.MD @@ -150,7 +150,7 @@ SMTP_SECURE=false SMTP_USER=test SMTP_PASS=test SMTP_FROM_EMAIL=dev@albertainfluence.local -SMTP_FROM_NAME="Alberta Influence Campaign (DEV)" +SMTP_FROM_NAME="BNKops Influence Campaign (DEV)" # Email Testing TEST_EMAIL_RECIPIENT=developer@example.com @@ -182,7 +182,7 @@ SMTP_PORT=587 SMTP_SECURE=false SMTP_USER=your_email@gmail.com SMTP_PASS=your_app_password -SMTP_FROM_NAME=Alberta Influence Campaign +SMTP_FROM_NAME=BNKops Influence Campaign SMTP_FROM_EMAIL=your_email@gmail.com # Rate Limiting diff --git a/influence/app/controllers/authController.js b/influence/app/controllers/authController.js index f94fc7e..05f8c64 100644 --- a/influence/app/controllers/authController.js +++ b/influence/app/controllers/authController.js @@ -50,10 +50,23 @@ class AuthController { // Update last login time try { - const userId = user.Id || user.id; - await nocodbService.updateUser(userId, { - 'Last Login': new Date().toISOString() + // Debug: Log user object structure + console.log('User object keys:', Object.keys(user)); + console.log('User ID candidates:', { + ID: user.ID, + Id: user.Id, + id: user.id }); + + const userId = user.ID || user.Id || user.id; + + if (userId) { + await nocodbService.updateUser(userId, { + 'Last Login': new Date().toISOString() + }); + } else { + console.warn('No valid user ID found for updating last login time'); + } } catch (updateError) { console.warn('Failed to update last login time:', updateError.message); // Don't fail the login @@ -61,7 +74,7 @@ class AuthController { // Set session req.session.authenticated = true; - req.session.userId = user.Id || user.id; + req.session.userId = user.ID || user.Id || user.id; req.session.userEmail = user.Email || user.email; req.session.userName = user.Name || user.name; req.session.isAdmin = user.Admin || user.admin || false; diff --git a/influence/app/controllers/campaigns.js b/influence/app/controllers/campaigns.js index ce169df..06eb5d1 100644 --- a/influence/app/controllers/campaigns.js +++ b/influence/app/controllers/campaigns.js @@ -3,6 +3,30 @@ const emailService = require('../services/email'); const representAPI = require('../services/represent-api'); const { generateSlug, validateSlug } = require('../utils/validators'); +// 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) { @@ -31,6 +55,7 @@ class CampaignsController { 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: campaign['Target Government Levels'] || campaign.target_government_levels, created_at: campaign.CreatedAt || campaign.created_at, updated_at: campaign.UpdatedAt || campaign.updated_at, @@ -91,6 +116,7 @@ class CampaignsController { 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: campaign['Target Government Levels'] || campaign.target_government_levels, created_at: campaign.CreatedAt || campaign.created_at, updated_at: campaign.UpdatedAt || campaign.updated_at, @@ -153,6 +179,7 @@ class CampaignsController { 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: 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 @@ -181,10 +208,12 @@ class CampaignsController { 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; @@ -206,11 +235,12 @@ class CampaignsController { email_subject, email_body, call_to_action, - status: 'draft', + status, 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: Array.isArray(target_government_levels) ? target_government_levels @@ -237,6 +267,7 @@ class CampaignsController { 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: campaign['Target Government Levels'] || campaign.target_government_levels, created_at: campaign.CreatedAt || campaign.created_at, updated_at: campaign.UpdatedAt || campaign.updated_at @@ -356,7 +387,9 @@ class CampaignsController { recipientName, recipientTitle, recipientLevel, - emailMethod = 'smtp' + emailMethod = 'smtp', + customEmailSubject, + customEmailBody } = req.body; // Get campaign @@ -392,8 +425,14 @@ class CampaignsController { }); } - const subject = campaign['Email Subject'] || campaign.email_subject; - const message = campaign['Email Body'] || campaign.email_body; + // 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 }; @@ -539,20 +578,46 @@ class CampaignsController { }); } - // Get representatives - const result = await representAPI.getRepresentativesByPostalCode(postalCode); - - // Process representatives from both concordance and centroid + // First check cache for representatives + const formattedPostalCode = postalCode.replace(/\s/g, '').toUpperCase(); let representatives = []; + let result = null; - // Add concordance representatives (if any) - if (result.representatives_concordance && result.representatives_concordance.length > 0) { - representatives = representatives.concat(result.representatives_concordance); + // 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); } - - // 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 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) { @@ -561,8 +626,8 @@ class CampaignsController { message: 'No representatives found for this postal code', representatives: [], location: { - city: result.city, - province: result.province + city: result?.city || 'Alberta', + province: result?.province || 'AB' } }); } @@ -602,8 +667,8 @@ class CampaignsController { success: true, representatives: filteredRepresentatives, location: { - city: result.city, - province: result.province + city: result?.city || cachedData[0]?.city || 'Alberta', + province: result?.province || cachedData[0]?.province || 'AB' } }); diff --git a/influence/app/controllers/emails.js b/influence/app/controllers/emails.js index caf2af7..84ac4b2 100644 --- a/influence/app/controllers/emails.js +++ b/influence/app/controllers/emails.js @@ -4,7 +4,7 @@ const nocoDB = require('../services/nocodb'); class EmailsController { async sendEmail(req, res, next) { try { - const { recipientEmail, senderName, senderEmail, subject, message, postalCode } = req.body; + const { recipientEmail, senderName, senderEmail, subject, message, postalCode, recipientName } = req.body; // Send the email using template system const emailResult = await emailService.sendRepresentativeEmail( @@ -13,7 +13,8 @@ class EmailsController { senderEmail, subject, message, - postalCode + postalCode, + recipientName ); // Log the email send event @@ -22,6 +23,7 @@ class EmailsController { senderName, senderEmail, subject, + message, postalCode, status: emailResult.success ? 'sent' : 'failed', timestamp: new Date().toISOString(), @@ -53,13 +55,14 @@ class EmailsController { async previewEmail(req, res, next) { try { - const { recipientEmail, subject, message, senderName, senderEmail, postalCode } = req.body; + const { recipientEmail, subject, message, senderName, senderEmail, postalCode, recipientName } = req.body; const templateVariables = { MESSAGE: message, SENDER_NAME: senderName || 'Anonymous', SENDER_EMAIL: senderEmail || 'unknown@example.com', - POSTAL_CODE: postalCode || 'Unknown' + POSTAL_CODE: postalCode || 'Unknown', + RECIPIENT_NAME: recipientName || 'Representative' }; const emailOptions = { @@ -74,6 +77,23 @@ class EmailsController { const preview = await emailService.previewTemplatedEmail('representative-contact', templateVariables, emailOptions); + // Log the email preview event (non-blocking) + try { + await nocoDB.logEmailPreview({ + recipientEmail, + senderName, + senderEmail, + subject, + message, + postalCode, + timestamp: new Date().toISOString(), + senderIP: req.ip || req.connection.remoteAddress + }); + } catch (loggingError) { + console.error('Failed to log email preview:', loggingError); + // Don't fail the preview if logging fails + } + res.json({ success: true, preview: preview, diff --git a/influence/app/controllers/representatives.js b/influence/app/controllers/representatives.js index a977ba9..675cd37 100644 --- a/influence/app/controllers/representatives.js +++ b/influence/app/controllers/representatives.js @@ -12,12 +12,16 @@ async function cacheRepresentatives(postalCode, representatives, representData) }); // Cache representatives using the existing method - await nocoDB.storeRepresentatives(postalCode, representatives); + const result = await nocoDB.storeRepresentatives(postalCode, representatives); - console.log(`Successfully cached representatives for ${postalCode}`); + 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 + // Don't throw - caching is optional and should never break the main flow } } @@ -48,9 +52,16 @@ class RepresentativesController { if (cachedData && cachedData.length > 0) { return res.json({ - source: 'cache', - postalCode: formattedPostalCode, - representatives: cachedData + success: true, + source: 'Local Cache', + data: { + postalCode: formattedPostalCode, + location: { + city: cachedData[0]?.city || 'Alberta', + province: 'AB' + }, + representatives: cachedData + } }); } } catch (cacheError) { @@ -85,6 +96,9 @@ class RepresentativesController { if (representData.representatives_centroid && representData.representatives_centroid.length > 0) { representatives = representatives.concat(representData.representatives_centroid); } + + // Representatives already include office information, no need for additional API calls + console.log('Using representatives data with existing office information'); console.log(`Representatives concordance count: ${representData.boundaries_concordance ? representData.boundaries_concordance.length : 0}`); console.log(`Representatives centroid count: ${representData.representatives_centroid ? representData.representatives_centroid.length : 0}`); @@ -111,6 +125,7 @@ class RepresentativesController { res.json({ success: true, + source: 'Open North', data: { postalCode, location: { @@ -151,7 +166,7 @@ class RepresentativesController { await nocoDB.storeRepresentatives(formattedPostalCode, representData.representatives_concordance); res.json({ - source: 'refreshed', + source: 'Open North', postalCode: formattedPostalCode, representatives: representData.representatives_concordance, city: representData.city, diff --git a/influence/app/public/admin.html b/influence/app/public/admin.html index fca4970..d2ccae9 100644 --- a/influence/app/public/admin.html +++ b/influence/app/public/admin.html @@ -588,6 +588,10 @@ Sincerely, +