1312 lines
50 KiB
JavaScript
1312 lines
50 KiB
JavaScript
// SMS Campaign Manager - Dashboard JavaScript
|
||
function campaignApp() {
|
||
return {
|
||
// Tab management
|
||
activeTab: 'campaigns',
|
||
|
||
// Phone IP and status - will be set from template
|
||
phoneIP: '',
|
||
phoneStatus: {
|
||
termux_connected: false,
|
||
adb_connected: false,
|
||
prefer_termux: true,
|
||
last_check: null
|
||
},
|
||
|
||
// Campaign variables
|
||
campaignName: '',
|
||
messageTemplate: '',
|
||
uploadedFile: null,
|
||
selectedList: '',
|
||
savedLists: [],
|
||
campaignReady: false,
|
||
|
||
// Contact preview variables
|
||
contactsPreview: [],
|
||
totalContacts: 0,
|
||
uploadedContacts: [],
|
||
|
||
// Campaign state
|
||
campaignState: {
|
||
status: 'idle',
|
||
current: 0,
|
||
total: 0,
|
||
errors: []
|
||
},
|
||
currentCampaignId: null,
|
||
|
||
// Analytics and data
|
||
analytics: {},
|
||
responseTypes: [],
|
||
recentCampaigns: [],
|
||
followups: [],
|
||
recipients: [],
|
||
|
||
// Connection status
|
||
connectionStatus: {
|
||
termux_api: { available: false, url: '', type: '' },
|
||
adb: { available: false, target: '', type: '' },
|
||
optimal_connection: null
|
||
},
|
||
|
||
// Testing variables
|
||
testPhone: '',
|
||
testMessage: 'Test message from SMS Campaign Manager',
|
||
termuxTestResult: null,
|
||
adbTestResult: null,
|
||
testSmsResult: null,
|
||
testingTermux: false,
|
||
testingAdb: false,
|
||
sendingTest: false,
|
||
testHistory: [],
|
||
testResults: {
|
||
running: false,
|
||
connectionTest: '',
|
||
smsTest: ''
|
||
},
|
||
|
||
// Template management
|
||
selectedTemplate: '',
|
||
savedTemplates: [],
|
||
editingTemplate: null,
|
||
templateForm: {
|
||
name: '',
|
||
content: '',
|
||
description: '',
|
||
category: 'general',
|
||
is_favorite: 0
|
||
},
|
||
_lastLoadedTemplate: '', // Track the last loaded template content
|
||
|
||
// List management
|
||
listUploadName: '',
|
||
listUploadPreview: [],
|
||
viewingList: null,
|
||
viewingListContacts: [],
|
||
|
||
// Database reset
|
||
showResetConfirmation: false,
|
||
resetConfirmText: '',
|
||
resettingDatabase: false,
|
||
resetResult: null,
|
||
|
||
// Initialization
|
||
async init() {
|
||
// Start monitoring connection status
|
||
await this.checkConnectionStatus();
|
||
await this.loadConnectionStatus();
|
||
|
||
// Load initial data
|
||
await this.loadAnalytics();
|
||
await this.loadSavedLists();
|
||
await this.loadSavedTemplates();
|
||
await this.loadRecentCampaigns();
|
||
await this.loadFollowups();
|
||
|
||
// Set up periodic updates
|
||
setInterval(() => this.checkConnectionStatus(), 10000); // Check every 10 seconds
|
||
setInterval(() => this.updateStatus(), 2000); // Campaign status updates
|
||
setInterval(() => this.loadAnalytics(), 10000); // Analytics updates
|
||
setInterval(() => this.loadRecentCampaigns(), 15000); // Recent campaigns updates every 15 seconds
|
||
|
||
// Listen for saved list loads from the ListManager UI
|
||
document.addEventListener('saved-list-loaded', (e) => {
|
||
try {
|
||
this.recipients = e.detail.contacts || [];
|
||
if (e.detail.list && e.detail.list.name) {
|
||
this.campaignName = `List: ${e.detail.list.name}`;
|
||
}
|
||
} catch (err) {
|
||
console.error('Error applying saved list:', err);
|
||
}
|
||
});
|
||
},
|
||
|
||
// Connection management
|
||
async checkConnectionStatus() {
|
||
try {
|
||
const response = await fetch('/api/phone/status');
|
||
const data = await response.json();
|
||
this.phoneStatus = {
|
||
...data,
|
||
last_check: new Date().toISOString()
|
||
};
|
||
} catch (error) {
|
||
console.error('Status check failed:', error);
|
||
}
|
||
},
|
||
|
||
async loadConnectionStatus() {
|
||
try {
|
||
const response = await fetch('/api/connections/status');
|
||
const data = await response.json();
|
||
this.connectionStatus = {
|
||
termux_api: data.connections?.termux_api || { available: false },
|
||
adb: data.connections?.adb || { available: false },
|
||
optimal_connection: data.optimal_connection
|
||
};
|
||
} catch (error) {
|
||
console.error('Error loading connection status:', error);
|
||
}
|
||
},
|
||
|
||
// Data loading functions
|
||
async loadAnalytics() {
|
||
try {
|
||
const response = await fetch('/api/analytics');
|
||
this.analytics = await response.json();
|
||
} catch (error) {
|
||
console.error('Failed to load analytics:', error);
|
||
}
|
||
},
|
||
|
||
async loadSavedLists() {
|
||
try {
|
||
const response = await fetch('/api/lists');
|
||
const data = await response.json();
|
||
this.savedLists = data.success ? data.lists : [];
|
||
} catch (error) {
|
||
console.error('Failed to load lists:', error);
|
||
this.savedLists = []; // Set empty array on error
|
||
}
|
||
},
|
||
|
||
async loadRecentCampaigns() {
|
||
try {
|
||
const response = await fetch('/api/campaigns/recent');
|
||
const campaigns = await response.json();
|
||
this.recentCampaigns = campaigns || [];
|
||
console.log('Recent campaigns loaded:', this.recentCampaigns.length);
|
||
} catch (error) {
|
||
console.error('Failed to load recent campaigns:', error);
|
||
this.recentCampaigns = []; // Set empty array on error
|
||
}
|
||
},
|
||
|
||
async loadFollowups() {
|
||
try {
|
||
const response = await fetch('/api/followups');
|
||
this.followups = await response.json();
|
||
} catch (error) {
|
||
console.error('Failed to load followups:', error);
|
||
}
|
||
},
|
||
|
||
// File handling
|
||
async handleFileUpload(event) {
|
||
const file = event.target.files[0];
|
||
if (file) {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
try {
|
||
const response = await fetch('/api/campaign/upload', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
this.uploadedFile = file.name;
|
||
this.uploadedContacts = data.contacts || [];
|
||
this.contactsPreview = data.preview || data.contacts.slice(0, 10) || [];
|
||
this.totalContacts = data.total_contacts || data.contacts.length || 0;
|
||
this.campaignReady = true;
|
||
|
||
// Store globally for campaign creation
|
||
window.campaignContacts = data.contacts;
|
||
|
||
console.log(`Loaded ${this.totalContacts} contacts`);
|
||
} else {
|
||
alert('Error uploading file: ' + data.error);
|
||
this.resetContactData();
|
||
}
|
||
} catch (error) {
|
||
console.error('Upload failed:', error);
|
||
alert('Upload failed: ' + error.message);
|
||
this.resetContactData();
|
||
}
|
||
}
|
||
},
|
||
|
||
// Reset contact data
|
||
resetContactData() {
|
||
this.uploadedFile = null;
|
||
this.uploadedContacts = [];
|
||
this.contactsPreview = [];
|
||
this.totalContacts = 0;
|
||
this.campaignReady = false;
|
||
window.campaignContacts = [];
|
||
},
|
||
|
||
// Load saved list
|
||
async loadSavedList(listId) {
|
||
if (!listId) {
|
||
this.resetContactData();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/lists/${listId}`);
|
||
const data = await response.json();
|
||
|
||
if (data.success && data.list && data.list.contacts) {
|
||
const list = data.list;
|
||
this.uploadedContacts = list.contacts;
|
||
this.contactsPreview = list.contacts.slice(0, 10);
|
||
this.totalContacts = list.contacts.length;
|
||
this.campaignReady = true;
|
||
this.uploadedFile = `${list.name} (saved list)`;
|
||
|
||
// Store globally for campaign creation
|
||
window.campaignContacts = list.contacts;
|
||
|
||
console.log(`Loaded saved list: ${list.name} with ${this.totalContacts} contacts`);
|
||
} else {
|
||
alert('Error loading saved list: ' + (data.error || 'Unknown error'));
|
||
this.resetContactData();
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading saved list:', error);
|
||
alert('Failed to load saved list');
|
||
this.resetContactData();
|
||
}
|
||
},
|
||
|
||
// Campaign management
|
||
async startCampaign() {
|
||
if (!this.campaignReady || !this.messageTemplate.trim()) {
|
||
alert('Please upload contacts and enter a message template');
|
||
return;
|
||
}
|
||
|
||
// Create campaign with contact data
|
||
try {
|
||
const contactData = window.campaignContacts || this.uploadedContacts || [];
|
||
|
||
if (contactData.length === 0) {
|
||
alert('No contacts loaded. Please upload a CSV file or select a saved list.');
|
||
return;
|
||
}
|
||
|
||
const response = await fetch('/api/campaign/create', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
name: this.campaignName || `Campaign ${new Date().toISOString().split('T')[0]}`,
|
||
message: this.messageTemplate,
|
||
csv_data: contactData,
|
||
list_id: this.selectedList
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
this.currentCampaignId = result.campaign_id;
|
||
|
||
alert(`Campaign "${result.campaign_name}" created with ${result.total_recipients} recipients!`);
|
||
|
||
// Start the campaign
|
||
const startResponse = await fetch('/api/campaign/start', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ campaign_id: this.currentCampaignId })
|
||
});
|
||
|
||
const startResult = await startResponse.json();
|
||
if (startResult.success) {
|
||
this.campaignState.status = 'running';
|
||
this.campaignState.total = startResult.total || result.total_recipients;
|
||
console.log(`Campaign started successfully with ${startResult.total} recipients`);
|
||
alert(`Campaign started successfully! Sending to ${startResult.total} recipients.`);
|
||
} else {
|
||
console.error('Failed to start campaign:', startResult.error);
|
||
alert(`Failed to start campaign: ${startResult.error}`);
|
||
}
|
||
|
||
// Reload recent campaigns
|
||
await this.loadRecentCampaigns();
|
||
} else {
|
||
alert(`Failed to create campaign: ${result.error}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error starting campaign:', error);
|
||
alert('Failed to start campaign. Check console for details.');
|
||
}
|
||
},
|
||
|
||
// Template Management Methods
|
||
async loadSavedTemplates() {
|
||
try {
|
||
const response = await fetch('/api/templates');
|
||
const data = await response.json();
|
||
this.savedTemplates = data || [];
|
||
} catch (error) {
|
||
console.error('Error loading templates:', error);
|
||
this.savedTemplates = [];
|
||
}
|
||
},
|
||
|
||
async loadTemplate(templateId) {
|
||
if (!templateId) {
|
||
this.selectedTemplate = '';
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Convert templateId to number for comparison since select values are strings
|
||
const numericTemplateId = parseInt(templateId);
|
||
|
||
const template = this.savedTemplates.find(t => t.id === numericTemplateId);
|
||
|
||
if (template) {
|
||
// Set the selected template ID first
|
||
this.selectedTemplate = templateId;
|
||
|
||
// Apply template content to message template field
|
||
// Handle both 'template' and 'content' fields for consistency
|
||
const templateContent = template.template || template.content;
|
||
|
||
if (templateContent) {
|
||
this.messageTemplate = templateContent;
|
||
|
||
// Store the template content for comparison
|
||
this._lastLoadedTemplate = templateContent;
|
||
|
||
console.log(`✅ Loaded template: ${template.name}`);
|
||
|
||
// Mark template as used (but don't await to avoid blocking UI)
|
||
fetch(`/api/templates/${templateId}/use`, { method: 'POST' })
|
||
.catch(error => console.log('Usage tracking failed:', error));
|
||
} else {
|
||
console.error('❌ Template content is empty');
|
||
alert('Template content is empty');
|
||
}
|
||
} else {
|
||
console.error('❌ Template not found with ID:', numericTemplateId);
|
||
alert(`Template not found with ID: ${numericTemplateId}`);
|
||
this.selectedTemplate = '';
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Error loading template:', error);
|
||
this.selectedTemplate = '';
|
||
alert('Failed to load template: ' + error.message);
|
||
}
|
||
},
|
||
|
||
clearTemplate() {
|
||
this.selectedTemplate = '';
|
||
this.messageTemplate = '';
|
||
this._lastLoadedTemplate = '';
|
||
},
|
||
|
||
// Check if message template was manually modified
|
||
onMessageTemplateChange() {
|
||
// Only clear selected template if user manually modified the content
|
||
if (this.selectedTemplate && this._lastLoadedTemplate &&
|
||
this.messageTemplate !== this._lastLoadedTemplate) {
|
||
this.selectedTemplate = '';
|
||
this._lastLoadedTemplate = '';
|
||
}
|
||
},
|
||
|
||
async saveTemplate() {
|
||
const name = prompt('Template name:');
|
||
if (!name) return;
|
||
|
||
const description = prompt('Template description (optional):') || '';
|
||
const category = prompt('Category (general, volunteer, reminder, gratitude, followup):') || 'general';
|
||
|
||
try {
|
||
const response = await fetch('/api/templates', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
name: name,
|
||
content: this.messageTemplate,
|
||
description: description,
|
||
category: category,
|
||
is_favorite: 0
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
alert('Template saved!');
|
||
await this.loadSavedTemplates();
|
||
} else {
|
||
alert('Error saving template: ' + result.error);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error saving template:', error);
|
||
alert('Error saving template: ' + error.message);
|
||
}
|
||
},
|
||
|
||
async saveNewTemplate() {
|
||
if (!this.templateForm.name || !this.templateForm.content) {
|
||
alert('Please fill in template name and content');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const url = this.editingTemplate
|
||
? `/api/templates/${this.editingTemplate.id}`
|
||
: '/api/templates';
|
||
const method = this.editingTemplate ? 'PUT' : 'POST';
|
||
|
||
const response = await fetch(url, {
|
||
method: method,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(this.templateForm)
|
||
});
|
||
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
alert(this.editingTemplate ? 'Template updated!' : 'Template created!');
|
||
await this.loadSavedTemplates();
|
||
this.resetTemplateForm();
|
||
} else {
|
||
alert('Error: ' + result.error);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error saving template:', error);
|
||
alert('Error saving template: ' + error.message);
|
||
}
|
||
},
|
||
|
||
loadTemplateForEditing(template) {
|
||
this.editingTemplate = template;
|
||
this.templateForm = {
|
||
name: template.name,
|
||
content: template.template || template.content,
|
||
description: template.description || '',
|
||
category: template.category || 'general',
|
||
is_favorite: template.is_favorite || 0
|
||
};
|
||
},
|
||
|
||
cancelEditTemplate() {
|
||
this.resetTemplateForm();
|
||
},
|
||
|
||
resetTemplateForm() {
|
||
this.editingTemplate = null;
|
||
this.templateForm = {
|
||
name: '',
|
||
content: '',
|
||
description: '',
|
||
category: 'general',
|
||
is_favorite: 0
|
||
};
|
||
},
|
||
|
||
async deleteTemplate(templateId, templateName) {
|
||
if (!confirm(`Are you sure you want to delete the template "${templateName}"?`)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/templates/${templateId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
alert('Template deleted!');
|
||
await this.loadSavedTemplates();
|
||
} else {
|
||
alert('Error deleting template: ' + result.error);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting template:', error);
|
||
alert('Error deleting template: ' + error.message);
|
||
}
|
||
},
|
||
|
||
async toggleTemplateFavorite(template) {
|
||
try {
|
||
const response = await fetch(`/api/templates/${template.id}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
is_favorite: template.is_favorite ? 0 : 1
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
await this.loadSavedTemplates();
|
||
} else {
|
||
alert('Error updating template: ' + result.error);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating template:', error);
|
||
alert('Error updating template: ' + error.message);
|
||
}
|
||
},
|
||
|
||
// === LIST MANAGEMENT METHODS ===
|
||
async handleListUpload(event) {
|
||
const file = event.target.files[0];
|
||
if (file) {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
try {
|
||
const response = await fetch('/api/csv/upload', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
this.listUploadPreview = data.recipients.slice(0, 10);
|
||
|
||
// Auto-save the list if no custom name is provided
|
||
if (!this.listUploadName.trim()) {
|
||
await this.loadSavedLists();
|
||
alert(`Contact list saved automatically as "${data.list_name}"`);
|
||
this.listUploadPreview = [];
|
||
event.target.value = ''; // Clear file input
|
||
}
|
||
} else {
|
||
alert('Error uploading file: ' + data.error);
|
||
}
|
||
} catch (error) {
|
||
console.error('Upload failed:', error);
|
||
alert('Upload failed: ' + error.message);
|
||
}
|
||
}
|
||
},
|
||
|
||
async saveListFromPreview() {
|
||
if (this.listUploadPreview.length === 0) {
|
||
alert('No contacts to save');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const listName = this.listUploadName.trim() ||
|
||
`Custom_List_${new Date().toISOString().slice(0, 19).replace(/[T:]/g, '_')}`;
|
||
|
||
const response = await fetch('/api/lists', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
name: listName,
|
||
contacts: this.listUploadPreview,
|
||
filename: 'custom_upload'
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
alert(`Contact list saved as "${listName}"`);
|
||
this.listUploadPreview = [];
|
||
this.listUploadName = '';
|
||
await this.loadSavedLists();
|
||
} else {
|
||
alert('Error saving list: ' + data.error);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error saving list:', error);
|
||
alert('Error saving list: ' + error.message);
|
||
}
|
||
},
|
||
|
||
async viewListContacts(list) {
|
||
try {
|
||
const response = await fetch(`/api/lists/${list.id}`);
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
this.viewingList = data.list;
|
||
this.viewingListContacts = data.list.contacts || [];
|
||
} else {
|
||
alert('Error loading list details: ' + data.error);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading list details:', error);
|
||
alert('Error loading list details: ' + error.message);
|
||
}
|
||
},
|
||
|
||
async useListForCampaign(list) {
|
||
// Switch to campaigns tab
|
||
this.activeTab = 'campaigns';
|
||
|
||
// Load the list data
|
||
this.selectedList = list.id;
|
||
await this.loadSavedList(list.id);
|
||
|
||
alert(`List "${list.name}" is now loaded for your campaign!`);
|
||
},
|
||
|
||
async downloadList(list) {
|
||
try {
|
||
const response = await fetch(`/api/lists/${list.id}`);
|
||
const data = await response.json();
|
||
|
||
if (data.success && data.list.contacts) {
|
||
// Convert to CSV
|
||
const contacts = data.list.contacts;
|
||
const headers = ['name', 'phone', 'email'];
|
||
const csvContent = [
|
||
headers.join(','),
|
||
...contacts.map(contact =>
|
||
headers.map(header =>
|
||
(contact[header] || '').toString().replace(/"/g, '""')
|
||
).map(field => `"${field}"`).join(',')
|
||
)
|
||
].join('\n');
|
||
|
||
// Download
|
||
const blob = new Blob([csvContent], { type: 'text/csv' });
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `${list.name}.csv`;
|
||
a.click();
|
||
window.URL.revokeObjectURL(url);
|
||
} else {
|
||
alert('Error downloading list: ' + (data.error || 'Unknown error'));
|
||
}
|
||
} catch (error) {
|
||
console.error('Error downloading list:', error);
|
||
alert('Error downloading list: ' + error.message);
|
||
}
|
||
},
|
||
|
||
async deleteContactList(listId, listName) {
|
||
if (!confirm(`Are you sure you want to delete the list "${listName}"?`)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/lists/${listId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
alert('List deleted!');
|
||
await this.loadSavedLists();
|
||
} else {
|
||
alert('Error deleting list: ' + result.error);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting list:', error);
|
||
alert('Error deleting list: ' + error.message);
|
||
}
|
||
},
|
||
|
||
// === TESTING METHODS ===
|
||
async testSMS() {
|
||
if (!this.messageTemplate) {
|
||
alert('Please enter a message template first');
|
||
return;
|
||
}
|
||
|
||
const phone = prompt('Enter test phone number:', '7802921731');
|
||
if (!phone) return;
|
||
|
||
const testMessage = this.messageTemplate.replace('{name}', 'Test User');
|
||
const confirmed = confirm(`Send test SMS?\\n\\n⚠️ WARNING: This will send a REAL SMS message!\\n\\nTo: ${phone}\\nMessage: ${testMessage.substring(0, 100)}${testMessage.length > 100 ? '...' : ''}\\n\\nProceed?`);
|
||
if (!confirmed) return;
|
||
|
||
try {
|
||
const response = await fetch('/api/sms/test/real', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
phone: phone,
|
||
message: testMessage,
|
||
name: 'Test User'
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
alert(`✅ Test SMS sent successfully!\\n\\nMethod: ${result.connection_type}\\nPhone: ${phone}\\nTime: ${new Date(result.timestamp * 1000).toLocaleTimeString()}`);
|
||
} else {
|
||
alert(`❌ Test SMS failed: ${result.error}`);
|
||
}
|
||
} catch (error) {
|
||
alert(`❌ Test SMS error: ${error.message}`);
|
||
}
|
||
},
|
||
|
||
async updateStatus() {
|
||
if (this.campaignState.status !== 'idle') {
|
||
try {
|
||
const response = await fetch('/api/campaign/status');
|
||
this.campaignState = await response.json();
|
||
|
||
if (this.campaignState.status === 'completed') {
|
||
await this.loadAnalytics();
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating campaign status:', error);
|
||
}
|
||
}
|
||
},
|
||
|
||
// Testing functions
|
||
async testTermuxConnection() {
|
||
this.testingTermux = true;
|
||
try {
|
||
const response = await fetch('/api/test/termux', { method: 'POST' });
|
||
this.termuxTestResult = await response.json();
|
||
} catch (error) {
|
||
this.termuxTestResult = { success: false, error: error.message };
|
||
} finally {
|
||
this.testingTermux = false;
|
||
}
|
||
},
|
||
|
||
async testAdbConnection() {
|
||
this.testingAdb = true;
|
||
try {
|
||
const response = await fetch('/api/test/adb', { method: 'POST' });
|
||
this.adbTestResult = await response.json();
|
||
} catch (error) {
|
||
this.adbTestResult = { success: false, error: error.message };
|
||
} finally {
|
||
this.testingAdb = false;
|
||
}
|
||
},
|
||
|
||
async sendTestSms(method) {
|
||
this.sendingTest = true;
|
||
try {
|
||
const response = await fetch('/api/test/sms', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
phone: this.testPhone,
|
||
message: this.testMessage,
|
||
method: method
|
||
})
|
||
});
|
||
this.testSmsResult = await response.json();
|
||
} catch (error) {
|
||
this.testSmsResult = { success: false, error: error.message };
|
||
} finally {
|
||
this.sendingTest = false;
|
||
}
|
||
},
|
||
|
||
// Database reset
|
||
async resetDatabase() {
|
||
if (this.resetConfirmText !== 'RESET') {
|
||
return;
|
||
}
|
||
|
||
this.resettingDatabase = true;
|
||
this.resetResult = null;
|
||
|
||
try {
|
||
const response = await fetch('/api/database/reset', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' }
|
||
});
|
||
|
||
const result = await response.json();
|
||
this.resetResult = result;
|
||
|
||
if (result.success) {
|
||
// Close modal and clear form
|
||
this.showResetConfirmation = false;
|
||
this.resetConfirmText = '';
|
||
|
||
// Reload all data after successful reset
|
||
setTimeout(async () => {
|
||
await this.loadAnalytics();
|
||
await this.loadSavedLists();
|
||
await this.loadSavedTemplates();
|
||
await this.loadRecentCampaigns();
|
||
await this.loadFollowups();
|
||
|
||
// Clear local state
|
||
this.recentCampaigns = [];
|
||
this.savedLists = [];
|
||
this.savedTemplates = [];
|
||
this.campaignName = '';
|
||
this.messageTemplate = '';
|
||
this.selectedList = '';
|
||
this.selectedTemplate = '';
|
||
this.contactsPreview = [];
|
||
this.totalContacts = 0;
|
||
}, 1000);
|
||
}
|
||
} catch (error) {
|
||
this.resetResult = {
|
||
success: false,
|
||
message: `Error: ${error.message}`
|
||
};
|
||
} finally {
|
||
this.resettingDatabase = false;
|
||
}
|
||
},
|
||
|
||
// Conversation management
|
||
loadConversations() {
|
||
// Load enhanced conversations when tab is clicked
|
||
if (typeof window.conversationManager !== 'undefined') {
|
||
window.conversationManager.init();
|
||
}
|
||
},
|
||
|
||
// Utility functions
|
||
formatDate(dateStr) {
|
||
return new Date(dateStr).toLocaleDateString();
|
||
},
|
||
|
||
formatTime(dateStr) {
|
||
if (!dateStr) return 'Never';
|
||
return new Date(dateStr).toLocaleTimeString();
|
||
},
|
||
|
||
// Legacy template helper for backward compatibility (if needed)
|
||
loadLegacyTemplate(type) {
|
||
const templates = {
|
||
initial: "Hi {name}! Hope all is well. I am wondering if you got my last email with the next couple weeks canvassing shifts? We are out every day and would love to have you join us. It is last minute however we are also out today, 5 - 8 PM starting at the Old Strathcona Community League. If you can make it, please let me know. Cheers!",
|
||
followup: "Hi {name}, just following up on my previous message about volunteering. We have several shifts available this week and would love to have you join us. When works best for you?",
|
||
reminder: "Hi {name}! Quick reminder that we're meeting today at {time}. Looking forward to seeing you there!"
|
||
};
|
||
this.messageTemplate = templates[type] || '';
|
||
}
|
||
};
|
||
}
|
||
|
||
// Enhanced Conversations Data Function
|
||
function conversationData() {
|
||
return {
|
||
// Initialize enhanced conversation manager
|
||
manager: null,
|
||
|
||
// State properties
|
||
conversations: [],
|
||
selectedConversation: null,
|
||
messages: [],
|
||
conversationFilter: 'all',
|
||
conversationSearch: '',
|
||
newMessage: '',
|
||
sendingMessage: false,
|
||
hasMoreMessages: false,
|
||
loadingMessages: false,
|
||
syncing: false,
|
||
|
||
// Computed properties
|
||
get filteredConversations() {
|
||
let filtered = this.conversations;
|
||
|
||
// Apply search filter
|
||
if (this.conversationSearch) {
|
||
const search = this.conversationSearch.toLowerCase();
|
||
filtered = filtered.filter(conv =>
|
||
(conv.contact_name && conv.contact_name.toLowerCase().includes(search)) ||
|
||
conv.phone.includes(search)
|
||
);
|
||
}
|
||
|
||
// Apply status filter
|
||
switch (this.conversationFilter) {
|
||
case 'unread':
|
||
filtered = filtered.filter(conv => conv.unread_count > 0);
|
||
break;
|
||
case 'starred':
|
||
filtered = filtered.filter(conv => conv.is_starred);
|
||
break;
|
||
default:
|
||
// 'all' - no additional filtering
|
||
break;
|
||
}
|
||
|
||
return filtered;
|
||
},
|
||
|
||
get unreadCount() {
|
||
return this.conversations.reduce((total, conv) => total + (conv.unread_count || 0), 0);
|
||
},
|
||
|
||
// Initialize
|
||
async init() {
|
||
await this.loadConversations();
|
||
this.setupWebSocket();
|
||
},
|
||
|
||
// Load conversations from API
|
||
async loadConversations() {
|
||
try {
|
||
const response = await fetch('/api/conversations/enhanced/');
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
this.conversations = data.conversations || [];
|
||
console.log('Loaded conversations:', this.conversations.length);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading conversations:', error);
|
||
}
|
||
},
|
||
|
||
// Select and load conversation messages
|
||
async selectConversation(conversationId) {
|
||
try {
|
||
this.loadingMessages = true;
|
||
|
||
// Find and set selected conversation
|
||
this.selectedConversation = this.conversations.find(conv => conv.phone === conversationId);
|
||
|
||
if (this.selectedConversation) {
|
||
// Load messages for this conversation
|
||
await this.loadMessages();
|
||
}
|
||
} catch (error) {
|
||
console.error('Error selecting conversation:', error);
|
||
} finally {
|
||
this.loadingMessages = false;
|
||
}
|
||
},
|
||
|
||
// Load messages for selected conversation
|
||
async loadMessages() {
|
||
if (!this.selectedConversation) return;
|
||
|
||
try {
|
||
const response = await fetch(`/api/conversations/enhanced/${this.selectedConversation.phone}/messages`);
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
this.messages = data.messages || [];
|
||
this.hasMoreMessages = data.has_more || false;
|
||
|
||
// Scroll to bottom after messages load
|
||
this.$nextTick(() => {
|
||
if (this.$refs.messagesContainer) {
|
||
this.$refs.messagesContainer.scrollTop = this.$refs.messagesContainer.scrollHeight;
|
||
}
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading messages:', error);
|
||
}
|
||
},
|
||
|
||
// Send a new message
|
||
async sendMessage() {
|
||
if (!this.newMessage.trim() || !this.selectedConversation || this.sendingMessage) return;
|
||
|
||
try {
|
||
this.sendingMessage = true;
|
||
|
||
const response = await fetch(`/api/conversations/enhanced/${this.selectedConversation.phone}/send`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
message: this.newMessage.trim()
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
// Add optimistic message to UI
|
||
const newMsg = {
|
||
id: Date.now(), // Temporary ID
|
||
message: this.newMessage.trim(),
|
||
direction: 'outbound',
|
||
status: 'pending',
|
||
sent_at: new Date().toISOString(),
|
||
phone: this.selectedConversation.phone
|
||
};
|
||
|
||
this.messages.push(newMsg);
|
||
this.newMessage = '';
|
||
|
||
// Scroll to bottom
|
||
this.$nextTick(() => {
|
||
if (this.$refs.messagesContainer) {
|
||
this.$refs.messagesContainer.scrollTop = this.$refs.messagesContainer.scrollHeight;
|
||
}
|
||
});
|
||
|
||
// Update conversation in list
|
||
this.selectedConversation.message_count = (this.selectedConversation.message_count || 0) + 1;
|
||
this.selectedConversation.last_message_time = Date.now() / 1000;
|
||
}
|
||
} catch (error) {
|
||
console.error('Error sending message:', error);
|
||
} finally {
|
||
this.sendingMessage = false;
|
||
}
|
||
},
|
||
|
||
// Filter and search functions
|
||
setFilter(filter) {
|
||
this.conversationFilter = filter;
|
||
},
|
||
|
||
searchConversations() {
|
||
// Reactive filtering happens automatically via computed property
|
||
},
|
||
|
||
// Star/unstar conversation
|
||
async toggleStar(conversationId) {
|
||
try {
|
||
const response = await fetch(`/api/conversations/enhanced/${conversationId}/star`, {
|
||
method: 'PUT'
|
||
});
|
||
|
||
if (response.ok) {
|
||
// Update local state
|
||
const conv = this.conversations.find(c => c.phone === conversationId);
|
||
if (conv) {
|
||
conv.is_starred = !conv.is_starred;
|
||
}
|
||
if (this.selectedConversation && this.selectedConversation.phone === conversationId) {
|
||
this.selectedConversation.is_starred = !this.selectedConversation.is_starred;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error toggling star:', error);
|
||
}
|
||
},
|
||
|
||
// Sync functions
|
||
async syncConversation(conversationId) {
|
||
if (this.syncing) return;
|
||
|
||
try {
|
||
this.syncing = true;
|
||
|
||
// Trigger sync for specific conversation
|
||
await fetch(`/api/conversations/enhanced/${conversationId}/sync`, {
|
||
method: 'POST'
|
||
});
|
||
|
||
// Reload messages after sync
|
||
if (this.selectedConversation && this.selectedConversation.phone === conversationId) {
|
||
await this.loadMessages();
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error syncing conversation:', error);
|
||
} finally {
|
||
this.syncing = false;
|
||
}
|
||
},
|
||
|
||
async syncAllConversations() {
|
||
if (this.syncing) return;
|
||
|
||
try {
|
||
this.syncing = true;
|
||
|
||
// Trigger full sync
|
||
await fetch('/api/responses/sync', {
|
||
method: 'POST'
|
||
});
|
||
|
||
// Reload conversations
|
||
await this.loadConversations();
|
||
|
||
// Reload current conversation messages if any selected
|
||
if (this.selectedConversation) {
|
||
await this.loadMessages();
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error syncing all conversations:', error);
|
||
} finally {
|
||
this.syncing = false;
|
||
}
|
||
},
|
||
|
||
// Setup WebSocket for real-time updates
|
||
setupWebSocket() {
|
||
if (typeof io === 'undefined') {
|
||
console.warn('Socket.IO not available, real-time updates disabled');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const socket = io();
|
||
|
||
socket.on('connect', () => {
|
||
console.log('Connected to conversation WebSocket');
|
||
});
|
||
|
||
socket.on('new_message', (data) => {
|
||
this.handleNewMessage(data);
|
||
});
|
||
|
||
socket.on('message_status_update', (data) => {
|
||
this.updateMessageStatus(data.message_id, data.status);
|
||
});
|
||
|
||
socket.on('conversation_update', (data) => {
|
||
this.handleConversationUpdate(data);
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Error setting up WebSocket:', error);
|
||
}
|
||
},
|
||
|
||
// Handle real-time message updates
|
||
handleNewMessage(messageData) {
|
||
// Add to messages if it's for the current conversation
|
||
if (this.selectedConversation && messageData.phone === this.selectedConversation.phone) {
|
||
this.messages.push(messageData);
|
||
|
||
// Scroll to bottom
|
||
this.$nextTick(() => {
|
||
if (this.$refs.messagesContainer) {
|
||
this.$refs.messagesContainer.scrollTop = this.$refs.messagesContainer.scrollHeight;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Update conversation in list
|
||
this.updateConversationInList(messageData);
|
||
},
|
||
|
||
updateMessageStatus(messageId, status) {
|
||
// Update message status in current conversation
|
||
const message = this.messages.find(m => m.id === messageId);
|
||
if (message) {
|
||
message.status = status;
|
||
}
|
||
},
|
||
|
||
handleConversationUpdate(data) {
|
||
const conv = this.conversations.find(c => c.phone === data.phone);
|
||
if (conv) {
|
||
Object.assign(conv, data);
|
||
}
|
||
},
|
||
|
||
updateConversationInList(messageData) {
|
||
let conv = this.conversations.find(c => c.phone === messageData.phone);
|
||
|
||
if (!conv) {
|
||
// Create new conversation if it doesn't exist
|
||
conv = {
|
||
phone: messageData.phone,
|
||
contact_name: messageData.name || messageData.phone,
|
||
message_count: 1,
|
||
unread_count: messageData.direction === 'inbound' ? 1 : 0,
|
||
is_starred: false,
|
||
last_message_time: messageData.timestamp || Date.now() / 1000
|
||
};
|
||
this.conversations.unshift(conv);
|
||
} else {
|
||
// Update existing conversation
|
||
conv.message_count = (conv.message_count || 0) + 1;
|
||
if (messageData.direction === 'inbound') {
|
||
conv.unread_count = (conv.unread_count || 0) + 1;
|
||
}
|
||
conv.last_message_time = messageData.timestamp || Date.now() / 1000;
|
||
|
||
// Move to top of list
|
||
const index = this.conversations.indexOf(conv);
|
||
if (index > 0) {
|
||
this.conversations.splice(index, 1);
|
||
this.conversations.unshift(conv);
|
||
}
|
||
}
|
||
},
|
||
|
||
// Utility functions
|
||
getInitials(conversation) {
|
||
if (!conversation) return '';
|
||
|
||
const name = conversation.contact_name || conversation.phone;
|
||
if (name === conversation.phone) {
|
||
// For phone numbers, just use the first two digits
|
||
return name.slice(-4, -2) || name.slice(0, 2) || '??';
|
||
}
|
||
|
||
const words = name.split(' ').filter(word => word.length > 0);
|
||
if (words.length >= 2) {
|
||
return (words[0][0] + words[1][0]).toUpperCase();
|
||
} else if (words.length === 1) {
|
||
return words[0].slice(0, 2).toUpperCase();
|
||
}
|
||
return '??';
|
||
},
|
||
|
||
formatPhone(phone) {
|
||
if (!phone) return '';
|
||
|
||
// Remove any non-digit characters
|
||
const cleaned = phone.replace(/\D/g, '');
|
||
|
||
// Format as (xxx) xxx-xxxx for 10 digit numbers
|
||
if (cleaned.length === 10) {
|
||
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6)}`;
|
||
}
|
||
|
||
return phone;
|
||
},
|
||
|
||
formatTime(timestamp) {
|
||
if (!timestamp) return '';
|
||
|
||
const date = new Date(timestamp * 1000);
|
||
const now = new Date();
|
||
const diffMs = now - date;
|
||
const diffMins = Math.floor(diffMs / (1000 * 60));
|
||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||
|
||
if (diffMins < 1) return 'Just now';
|
||
if (diffMins < 60) return `${diffMins}m`;
|
||
if (diffHours < 24) return `${diffHours}h`;
|
||
if (diffDays < 7) return `${diffDays}d`;
|
||
|
||
return date.toLocaleDateString();
|
||
},
|
||
|
||
formatMessageTime(timestamp) {
|
||
if (!timestamp) return '';
|
||
|
||
let date;
|
||
if (typeof timestamp === 'string') {
|
||
date = new Date(timestamp);
|
||
} else {
|
||
date = new Date(timestamp * 1000);
|
||
}
|
||
|
||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||
},
|
||
|
||
getStatusIcon(status) {
|
||
switch (status) {
|
||
case 'pending': return '⏳';
|
||
case 'sent': return '✓';
|
||
case 'delivered': return '✓✓';
|
||
case 'failed': return '❌';
|
||
default: return '';
|
||
}
|
||
},
|
||
|
||
getStatusColor(status) {
|
||
switch (status) {
|
||
case 'pending': return 'text-yellow-400';
|
||
case 'sent': return 'text-blue-300';
|
||
case 'delivered': return 'text-blue-200';
|
||
case 'failed': return 'text-red-400';
|
||
default: return 'text-gray-400';
|
||
}
|
||
}
|
||
};
|
||
}
|