2025-12-30 08:50:08 -07:00

1312 lines
50 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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';
}
}
};
}