/** * Store Messages Page * * Handles the messaging interface for stores including: * - Conversation list with filtering * - Message thread display * - Sending messages * - Creating new conversations with customers */ const messagesLog = window.LogConfig?.createLogger('STORE-MESSAGES') || console; /** * Store Messages Component */ function storeMessages(initialConversationId = null) { return { ...data(), // Loading states loading: true, loadingConversations: false, loadingMessages: false, sendingMessage: false, creatingConversation: false, error: '', // Conversations state conversations: [], page: 1, skip: 0, limit: 20, totalConversations: 0, totalUnread: 0, // Filters filters: { conversation_type: '', is_closed: '' }, // Selected conversation selectedConversationId: initialConversationId, selectedConversation: null, // Reply form replyContent: '', // Close conversation confirm state showCloseConversationConfirm: false, // Compose modal showComposeModal: false, compose: { recipientId: null, subject: '', message: '' }, recipients: [], // Polling pollInterval: null, /** * Initialize component */ async init() { // Load i18n translations await I18n.loadModule('messaging'); // Guard against multiple initialization if (window._storeMessagesInitialized) return; window._storeMessagesInitialized = true; // IMPORTANT: Call parent init first to set storeCode from URL const parentInit = data().init; if (parentInit) { await parentInit.call(this); } try { messagesLog.debug('Initializing store messages page'); await Promise.all([ this.loadConversations(), this.loadRecipients() ]); if (this.selectedConversationId) { await this.loadConversation(this.selectedConversationId); } // Start polling for new messages this.startPolling(); } catch (error) { messagesLog.error('Failed to initialize messages page:', error); } finally { this.loading = false; } }, /** * Start polling for updates */ startPolling() { this.pollInterval = setInterval(async () => { if (this.selectedConversationId && !document.hidden) { await this.refreshCurrentConversation(); } await this.updateUnreadCount(); }, 30000); }, /** * Stop polling */ destroy() { if (this.pollInterval) { clearInterval(this.pollInterval); } }, // ============================================================================ // CONVERSATIONS LIST // ============================================================================ /** * Load conversations with current filters */ async loadConversations() { this.loadingConversations = true; try { this.skip = (this.page - 1) * this.limit; const params = new URLSearchParams(); params.append('skip', this.skip); params.append('limit', this.limit); if (this.filters.conversation_type) { params.append('conversation_type', this.filters.conversation_type); } if (this.filters.is_closed !== '') { params.append('is_closed', this.filters.is_closed); } const response = await apiClient.get(`/store/messages?${params}`); this.conversations = response.conversations || []; this.totalConversations = response.total || 0; this.totalUnread = response.total_unread || 0; messagesLog.debug(`Loaded ${this.conversations.length} conversations`); } catch (error) { messagesLog.error('Failed to load conversations:', error); Utils.showToast(I18n.t('messaging.messages.failed_to_load_conversations'), 'error'); } finally { this.loadingConversations = false; } }, /** * Update unread count */ async updateUnreadCount() { try { const response = await apiClient.get('/store/messages/unread-count'); this.totalUnread = response.total_unread || 0; } catch (error) { messagesLog.error('Failed to update unread count:', error); } }, /** * Select a conversation */ async selectConversation(conversationId) { if (this.selectedConversationId === conversationId) return; this.selectedConversationId = conversationId; await this.loadConversation(conversationId); }, /** * Load conversation detail */ async loadConversation(conversationId) { this.loadingMessages = true; try { const response = await apiClient.get(`/store/messages/${conversationId}?mark_read=true`); this.selectedConversation = response; // Update unread count in list const conv = this.conversations.find(c => c.id === conversationId); if (conv) { this.totalUnread = Math.max(0, this.totalUnread - conv.unread_count); conv.unread_count = 0; } // Scroll to bottom await this.$nextTick(); this.scrollToBottom(); } catch (error) { messagesLog.error('Failed to load conversation:', error); Utils.showToast(I18n.t('messaging.messages.failed_to_load_conversation'), 'error'); } finally { this.loadingMessages = false; } }, /** * Refresh current conversation */ async refreshCurrentConversation() { if (!this.selectedConversationId) return; try { const response = await apiClient.get(`/store/messages/${this.selectedConversationId}?mark_read=true`); const oldCount = this.selectedConversation?.messages?.length || 0; const newCount = response.messages?.length || 0; this.selectedConversation = response; if (newCount > oldCount) { await this.$nextTick(); this.scrollToBottom(); } } catch (error) { messagesLog.error('Failed to refresh conversation:', error); } }, /** * Scroll messages to bottom */ scrollToBottom() { const container = this.$refs.messagesContainer; if (container) { container.scrollTop = container.scrollHeight; } }, // ============================================================================ // SENDING MESSAGES // ============================================================================ /** * Send a message */ async sendMessage() { if (!this.replyContent.trim()) return; if (!this.selectedConversationId) return; this.sendingMessage = true; try { const formData = new FormData(); formData.append('content', this.replyContent); const message = await apiClient.postFormData(`/store/messages/${this.selectedConversationId}/messages`, formData); // Add to messages if (this.selectedConversation) { this.selectedConversation.messages.push(message); this.selectedConversation.message_count++; } // Clear form this.replyContent = ''; // Scroll to bottom await this.$nextTick(); this.scrollToBottom(); } catch (error) { messagesLog.error('Failed to send message:', error); Utils.showToast(error.message || 'Failed to send message', 'error'); } finally { this.sendingMessage = false; } }, // ============================================================================ // CONVERSATION ACTIONS // ============================================================================ /** * Close conversation */ async closeConversation() { try { await apiClient.post(`/store/messages/${this.selectedConversationId}/close`); if (this.selectedConversation) { this.selectedConversation.is_closed = true; } const conv = this.conversations.find(c => c.id === this.selectedConversationId); if (conv) conv.is_closed = true; Utils.showToast(I18n.t('messaging.messages.conversation_closed'), 'success'); } catch (error) { messagesLog.error('Failed to close conversation:', error); Utils.showToast(I18n.t('messaging.messages.failed_to_close_conversation'), 'error'); } }, /** * Reopen conversation */ async reopenConversation() { try { await apiClient.post(`/store/messages/${this.selectedConversationId}/reopen`); if (this.selectedConversation) { this.selectedConversation.is_closed = false; } const conv = this.conversations.find(c => c.id === this.selectedConversationId); if (conv) conv.is_closed = false; Utils.showToast(I18n.t('messaging.messages.conversation_reopened'), 'success'); } catch (error) { messagesLog.error('Failed to reopen conversation:', error); Utils.showToast(I18n.t('messaging.messages.failed_to_reopen_conversation'), 'error'); } }, // ============================================================================ // CREATE CONVERSATION // ============================================================================ /** * Load recipients (customers) */ async loadRecipients() { try { const response = await apiClient.get('/store/messages/recipients?recipient_type=customer&limit=100'); this.recipients = response.recipients || []; } catch (error) { messagesLog.error('Failed to load recipients:', error); } }, /** * Create new conversation */ async createConversation() { if (!this.compose.recipientId || !this.compose.subject.trim()) return; this.creatingConversation = true; try { const response = await apiClient.post('/store/messages', { conversation_type: 'store_customer', subject: this.compose.subject, recipient_type: 'customer', recipient_id: parseInt(this.compose.recipientId), initial_message: this.compose.message || null }); // Close modal and reset this.showComposeModal = false; this.compose = { recipientId: null, subject: '', message: '' }; // Reload and select await this.loadConversations(); await this.selectConversation(response.id); Utils.showToast(I18n.t('messaging.messages.conversation_created'), 'success'); } catch (error) { messagesLog.error('Failed to create conversation:', error); Utils.showToast(error.message || 'Failed to create conversation', 'error'); } finally { this.creatingConversation = false; } }, // ============================================================================ // HELPERS // ============================================================================ getOtherParticipantName() { if (!this.selectedConversation?.participants) return 'Unknown'; const other = this.selectedConversation.participants.find(p => p.participant_type !== 'store'); return other?.participant_info?.name || 'Unknown'; }, formatRelativeTime(dateString) { if (!dateString) return ''; const date = new Date(dateString); const now = new Date(); const diff = Math.floor((now - date) / 1000); if (diff < 60) return 'Now'; if (diff < 3600) return `${Math.floor(diff / 60)}m`; if (diff < 86400) return `${Math.floor(diff / 3600)}h`; if (diff < 172800) return 'Yesterday'; const locale = window.STORE_CONFIG?.locale || 'en-GB'; return date.toLocaleDateString(locale); }, formatTime(dateString) { if (!dateString) return ''; const date = new Date(dateString); const now = new Date(); const isToday = date.toDateString() === now.toDateString(); const locale = window.STORE_CONFIG?.locale || 'en-GB'; if (isToday) { return date.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' }); } return date.toLocaleString(locale, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } }; } // Make available globally window.storeMessages = storeMessages;