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