tonne of imporvements, debugs, new ui

This commit is contained in:
2025-09-20 15:58:55 -06:00
parent f49306a96f
commit b6960e45e8
13 changed files with 673 additions and 87 deletions

View File

@@ -80,18 +80,13 @@ class NocoDBService {
// Create record
async create(tableId, data) {
try {
// Clean data to prevent ID conflicts
const cleanData = { ...data };
delete cleanData.ID;
delete cleanData.id;
delete cleanData.Id;
// Remove undefined values
Object.keys(cleanData).forEach(key => {
if (cleanData[key] === undefined) {
delete cleanData[key];
// Clean the data to remove any null values which can cause NocoDB issues
const cleanData = Object.keys(data).reduce((clean, key) => {
if (data[key] !== null && data[key] !== undefined) {
clean[key] = data[key];
}
});
return clean;
}, {});
const url = this.getTableUrl(tableId);
const response = await this.client.post(url, cleanData);
@@ -102,6 +97,26 @@ class NocoDBService {
}
}
// Update record
async update(tableId, recordId, data) {
try {
// Clean the data to remove any null values which can cause NocoDB issues
const cleanData = Object.keys(data).reduce((clean, key) => {
if (data[key] !== null && data[key] !== undefined) {
clean[key] = data[key];
}
return clean;
}, {});
const url = `${this.getTableUrl(tableId)}/${recordId}`;
const response = await this.client.patch(url, cleanData);
return response.data;
} catch (error) {
console.error('Error updating record:', error);
throw error;
}
}
async storeRepresentatives(postalCode, representatives) {
@@ -110,16 +125,16 @@ class NocoDBService {
for (const rep of representatives) {
const record = {
postal_code: postalCode,
name: rep.name || '',
email: rep.email || '',
district_name: rep.district_name || '',
elected_office: rep.elected_office || '',
party_name: rep.party_name || '',
representative_set_name: rep.representative_set_name || '',
url: rep.url || '',
photo_url: rep.photo_url || '',
cached_at: new Date().toISOString()
'Postal Code': postalCode,
'Name': rep.name || '',
'Email': rep.email || '',
'District Name': rep.district_name || '',
'Elected Office': rep.elected_office || '',
'Party Name': rep.party_name || '',
'Representative Set Name': rep.representative_set_name || '',
'Profile URL': rep.url || '',
'Photo URL': rep.photo_url || '',
'Cached At': new Date().toISOString()
};
const result = await this.create(this.tableIds.representatives, record);
@@ -181,13 +196,13 @@ class NocoDBService {
async logEmailSend(emailData) {
try {
const record = {
recipient_email: emailData.recipientEmail,
sender_name: emailData.senderName,
sender_email: emailData.senderEmail,
subject: emailData.subject,
postal_code: emailData.postalCode,
status: emailData.status,
sent_at: emailData.timestamp
'Recipient Email': emailData.recipientEmail,
'Sender Name': emailData.senderName,
'Sender Email': emailData.senderEmail,
'Subject': emailData.subject,
'Postal Code': emailData.postalCode,
'Status': emailData.status,
'Sent At': emailData.timestamp
};
await this.create(this.tableIds.emails, record);
@@ -233,7 +248,13 @@ class NocoDBService {
async storePostalCodeInfo(postalCodeData) {
try {
const response = await this.create(this.tableIds.postalCodes, postalCodeData);
// Map fields to NocoDB column titles
const mappedData = {
'Postal Code': postalCodeData.postal_code,
'City': postalCodeData.city,
'Province': postalCodeData.province
};
const response = await this.create(this.tableIds.postalCodes, mappedData);
return response;
} catch (error) {
// Don't throw error for postal code caching failures
@@ -347,7 +368,25 @@ class NocoDBService {
// Campaign email tracking methods
async logCampaignEmail(emailData) {
try {
const response = await this.create(this.tableIds.campaignEmails, emailData);
// Map fields to NocoDB column titles
const mappedData = {
'Campaign ID': emailData.campaign_id,
'Campaign Slug': emailData.campaign_slug,
'User Email': emailData.user_email,
'User Name': emailData.user_name,
'User Postal Code': emailData.user_postal_code,
'Recipient Email': emailData.recipient_email,
'Recipient Name': emailData.recipient_name,
'Recipient Title': emailData.recipient_title,
'Government Level': emailData.recipient_level,
'Email Method': emailData.email_method,
'Subject': emailData.subject,
'Message': emailData.message,
'Status': emailData.status
// Note: 'Sent At' has default value of now() so we don't need to set it
};
const response = await this.create(this.tableIds.campaignEmails, mappedData);
return response;
} catch (error) {
console.error('Log campaign email failed:', error);
@@ -358,7 +397,7 @@ class NocoDBService {
async getCampaignEmailCount(campaignId) {
try {
const response = await this.getAll(this.tableIds.campaignEmails, {
where: `(campaign_id,eq,${campaignId})`,
where: `(Campaign ID,eq,${campaignId})`,
limit: 1000 // Get enough to count
});
return response.pageInfo ? response.pageInfo.totalRows : (response.list ? response.list.length : 0);
@@ -371,7 +410,7 @@ class NocoDBService {
async getCampaignAnalytics(campaignId) {
try {
const response = await this.getAll(this.tableIds.campaignEmails, {
where: `(campaign_id,eq,${campaignId})`,
where: `(Campaign ID,eq,${campaignId})`,
limit: 1000
});
@@ -379,32 +418,36 @@ class NocoDBService {
const analytics = {
totalEmails: emails.length,
smtpEmails: emails.filter(e => e.email_method === 'smtp').length,
mailtoClicks: emails.filter(e => e.email_method === 'mailto').length,
successfulEmails: emails.filter(e => e.status === 'sent' || e.status === 'clicked').length,
failedEmails: emails.filter(e => e.status === 'failed').length,
smtpEmails: emails.filter(e => (e['Email Method'] || e.email_method) === 'smtp').length,
mailtoClicks: emails.filter(e => (e['Email Method'] || e.email_method) === 'mailto').length,
successfulEmails: emails.filter(e => {
const status = e['Status'] || e.status;
return status === 'sent' || status === 'clicked';
}).length,
failedEmails: emails.filter(e => (e['Status'] || e.status) === 'failed').length,
byLevel: {},
byDate: {},
recentEmails: emails.slice(0, 10).map(email => ({
timestamp: email.timestamp,
user_name: email.user_name,
recipient_name: email.recipient_name,
recipient_level: email.recipient_level,
email_method: email.email_method,
status: email.status
timestamp: email['Sent At'] || email.timestamp || email.sent_at,
user_name: email['User Name'] || email.user_name,
recipient_name: email['Recipient Name'] || email.recipient_name,
recipient_level: email['Government Level'] || email.recipient_level,
email_method: email['Email Method'] || email.email_method,
status: email['Status'] || email.status
}))
};
// Group by government level
emails.forEach(email => {
const level = email.recipient_level || 'Other';
const level = email['Government Level'] || email.recipient_level || 'Other';
analytics.byLevel[level] = (analytics.byLevel[level] || 0) + 1;
});
// Group by date
emails.forEach(email => {
if (email.timestamp) {
const date = email.timestamp.split('T')[0]; // Get date part
const timestamp = email['Sent At'] || email.timestamp || email.sent_at;
if (timestamp) {
const date = timestamp.split('T')[0]; // Get date part
analytics.byDate[date] = (analytics.byDate[date] || 0) + 1;
}
});