/** * Plaza IT Chat Widget - Lightweight Client * Vanilla JavaScript - No dependencies */ class PlazaChat { constructor(options = {}) { this.API_BASE = options.apiBase || '/api/chat'; this.currentConversationId = null; this.currentView = 'list'; // 'list', 'messages', 'new' this.pollingInterval = null; this.pollingRate = options.pollingRate || 30000; // 30 seconds this.unreadCount = 0; this.conversations = []; this.messages = []; this.csrfToken = null; // CSRF token for security this.isWidgetOpen = false; // Track widget visibility this.pendingProductAttachment = null; // Product to attach (draft state) this.init(); } init() { this.cacheElements(); this.bindEvents(); this.checkOperatingHours(); this.startPolling(); this.setupVisibilityTracking(); this.setupViewportHeight(); this.checkAutoOpenHash(); } /** * Check for #openchat hash and auto-open widget */ checkAutoOpenHash() { if (window.location.hash === '#openchat') { const widget = document.getElementById('chat-widget'); const isLoggedIn = widget?.dataset.isLoggedIn === 'true'; if (isLoggedIn) { // Auto-open chat widget setTimeout(() => { this.openWidget(); // Remove hash from URL without page reload history.replaceState(null, null, window.location.pathname + window.location.search); }, 300); // Small delay to ensure DOM is ready } else { // Not logged in, remove hash history.replaceState(null, null, window.location.pathname + window.location.search); } } } /** * Check operating hours and display notice if outside hours */ checkOperatingHours() { const now = new Date(); const hour = now.getHours(); const subtitle = document.getElementById('chatOperatingHours'); console.log('🕐 Checking operating hours:', { currentHour: hour, currentTime: now.toLocaleTimeString('id-ID'), elementFound: !!subtitle, isOutsideHours: hour < 9 || hour >= 22 }); if (!subtitle) { console.warn('⚠️ chatOperatingHours element not found'); return; } // Check if outside operating hours (before 9 AM or after 10 PM) const isOutsideHours = hour < 9 || hour >= 22; if (isOutsideHours) { subtitle.innerHTML = ` Di luar jam operasional • Balasan mungkin tertunda `; subtitle.style.display = 'flex'; console.log('✅ Operating hours notice displayed'); } else { subtitle.style.display = 'none'; console.log('✅ Operating hours notice hidden (within hours)'); } } setupVisibilityTracking() { // Pause polling when tab is hidden to save resources document.addEventListener('visibilitychange', () => { if (document.hidden) { // Tab hidden - slow down polling or pause this.stopPolling(); } else { // Tab visible - resume polling this.startPolling(); // Immediate refresh when user comes back this.updateUnreadCount(); if (this.isWidgetOpen) { if (this.currentView === 'list') { this.loadConversations(); } else if (this.currentView === 'messages' && this.currentConversationId) { this.loadMessages(this.currentConversationId, true); } } } }); } setupViewportHeight() { // Set CSS custom property for viewport height (fallback for non-dvh browsers) const setViewportHeight = () => { // Use visualViewport API if available (more accurate for mobile) const height = window.visualViewport ? window.visualViewport.height : window.innerHeight; document.documentElement.style.setProperty('--viewport-height', `${height}px`); }; // Initial set setViewportHeight(); // Update on resize and viewport changes if (window.visualViewport) { window.visualViewport.addEventListener('resize', setViewportHeight); window.visualViewport.addEventListener('scroll', setViewportHeight); } else { window.addEventListener('resize', setViewportHeight); } // Update when orientation changes window.addEventListener('orientationchange', () => { setTimeout(setViewportHeight, 100); }); } cacheElements() { // Main widget this.widget = document.getElementById('chat-widget'); this.toggleBtn = document.getElementById('chat-toggle-btn'); this.popup = document.getElementById('chat-popup'); this.closeBtn = document.getElementById('chat-close-btn'); this.unreadBadge = document.getElementById('chat-unread-badge'); // Get CSRF token from data attribute if (this.widget) { this.csrfToken = this.widget.dataset.csrfToken; } // Views this.listView = document.getElementById('chat-list-view'); this.messagesView = document.getElementById('chat-messages-view'); this.newView = document.getElementById('chat-new-view'); // List view this.conversationsList = document.getElementById('chat-conversations-list'); this.newBtn = document.getElementById('chat-new-btn'); // Hide new conversation button (single-conversation model) if (this.newBtn) { this.newBtn.style.display = 'none'; } // Messages view // No back button or title needed - single conversation model this.messagesContainer = document.getElementById('chat-messages-container'); this.messageForm = document.getElementById('chat-message-form'); this.messageInput = document.getElementById('chat-message-input'); this.currentConversationIdInput = document.getElementById('chat-current-conversation-id'); // New chat view this.newBackBtn = document.getElementById('chat-new-back-btn'); this.startForm = document.getElementById('chat-start-form'); this.newSubjectInput = document.getElementById('chat-new-subject'); this.newMessageInput = document.getElementById('chat-new-message'); } bindEvents() { if (this.toggleBtn) { this.toggleBtn.addEventListener('click', () => this.toggleWidget()); } if (this.closeBtn) { this.closeBtn.addEventListener('click', () => this.closeWidget()); } if (this.newBtn) { this.newBtn.addEventListener('click', () => this.showNewView()); } if (this.newBackBtn) { this.newBackBtn.addEventListener('click', () => this.showListView()); } if (this.messageForm) { this.messageForm.addEventListener('submit', (e) => this.handleSendMessage(e)); } if (this.startForm) { this.startForm.addEventListener('submit', (e) => this.handleStartConversation(e)); } // Notification toggle button const notifToggle = document.getElementById('chatNotifToggle'); if (notifToggle) { notifToggle.addEventListener('click', () => { if (window.ChatNotification) { window.ChatNotification.handleToggleClick(); } }); } // Auto-resize textarea if (this.messageInput) { this.messageInput.addEventListener('input', () => this.autoResizeTextarea(this.messageInput)); this.messageInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.messageForm.dispatchEvent(new Event('submit')); } }); } if (this.newMessageInput) { this.newMessageInput.addEventListener('input', () => this.autoResizeTextarea(this.newMessageInput)); } } toggleWidget() { if (!this.popup) { return; } // Check if popup is hidden (display is 'none' or empty string) const isHidden = !this.popup.style.display || this.popup.style.display === 'none'; if (isHidden) { this.openWidget(); } else { this.closeWidget(); } } openWidget() { if (!this.popup) { return; } // Check if user is logged in const isLoggedIn = this.widget.dataset.isLoggedIn === 'true'; if (!isLoggedIn) { // Show login required message this.showLoginRequired(); return; } this.popup.style.display = 'block'; this.isWidgetOpen = true; // Check operating hours when widget opens this.checkOperatingHours(); // Trigger mobile browser address bar auto-hide this.hideAddressBar(); // Single-conversation model: Auto-load customer's conversation this.autoLoadConversation(); } hideAddressBar() { // Only on mobile devices if (!this.isMobileDevice()) return; // Save current scroll position this.savedScrollPosition = window.pageYOffset || document.documentElement.scrollTop; // Method 1: Scroll down slightly to trigger browser auto-hide // Most mobile browsers hide address bar when scrolling down if (window.pageYOffset === 0) { // If at top, scroll down 1px window.scrollTo(0, 1); } // Method 2: Request minimal scroll for consistent behavior setTimeout(() => { window.scrollTo(0, Math.max(1, window.pageYOffset)); // Update viewport height after scroll to recalculate this.updateViewportHeight(); }, 100); // Additional attempt after longer delay (for stubborn browsers) setTimeout(() => { this.updateViewportHeight(); }, 300); } updateViewportHeight() { const height = window.visualViewport ? window.visualViewport.height : window.innerHeight; document.documentElement.style.setProperty('--viewport-height', `${height}px`); } restoreScrollPosition() { // Restore scroll position when closing chat if (typeof this.savedScrollPosition === 'number') { window.scrollTo(0, this.savedScrollPosition); this.savedScrollPosition = null; } } isMobileDevice() { // Detect mobile devices return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 768; } async autoLoadConversation() { // Show loading in messages view immediately this.showMessagesView(null); // Show view with loading state try { // Try to get or create conversation const res = await fetch(`${this.API_BASE}/start.php`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.csrfToken }, body: JSON.stringify({ subject: 'Customer Support' // No initial_message - just get/create conversation }) }); // Handle session expired if (res.status === 401 || res.status === 403) { this.handleSessionExpired(); return; } const data = await res.json(); if (data.success && data.data && data.data.conversation_id) { // Load messages for the conversation this.currentConversationId = data.data.conversation_id; this.currentConversationIdInput.value = data.data.conversation_id; this.loadMessages(data.data.conversation_id, false); } else { // Show error in messages view this.messagesContainer.innerHTML = `

Gagal memuat percakapan

`; const retryBtn = document.getElementById('chat-retry-load-btn'); if (retryBtn) { retryBtn.addEventListener('click', () => this.autoLoadConversation()); } } } catch (err) { // Show error in messages view this.messagesContainer.innerHTML = `

Terjadi kesalahan: ${err.message}

`; const retryBtn = document.getElementById('chat-retry-error-btn'); if (retryBtn) { retryBtn.addEventListener('click', () => this.autoLoadConversation()); } } } closeWidget() { this.popup.style.display = 'none'; this.isWidgetOpen = false; // Restore scroll position (optional - helps maintain context) // Comment out if you prefer to leave scroll position as-is // this.restoreScrollPosition(); } showMessagesView(conversationId) { this.currentView = 'messages'; this.listView.style.display = 'none'; this.messagesView.style.display = 'flex'; this.newView.style.display = 'none'; // If conversationId is null, show loading (for autoLoadConversation) if (conversationId === null) { this.messagesContainer.innerHTML = '
Memuat percakapan...
'; return; } this.currentConversationId = conversationId; this.currentConversationIdInput.value = conversationId; // Clear messages when switching conversation this.messagesContainer.innerHTML = ''; // Save to sessionStorage for persistence sessionStorage.setItem('plazaChat_lastConversation', conversationId); this.loadMessages(conversationId, false); } showNewView() { this.currentView = 'new'; this.listView.style.display = 'none'; this.messagesView.style.display = 'none'; this.newView.style.display = 'flex'; this.newMessageInput.focus(); } async loadMessages(conversationId, preserveScroll = false) { try { if (!preserveScroll) { this.showLoading(this.messagesContainer); } const res = await fetch(`${this.API_BASE}/messages.php?conversation_id=${conversationId}&limit=50`); // Handle session expired if (res.status === 401 || res.status === 403) { this.handleSessionExpired(); return; } const data = await res.json(); if (data.success) { // Check if user was at bottom before update const wasAtBottom = preserveScroll ? (this.messagesContainer.scrollHeight - this.messagesContainer.scrollTop - this.messagesContainer.clientHeight < 100) : false; this.messages = data.data.messages; // No need to set title - single conversation model this.renderMessages(); // Mark as read await this.markAsRead(conversationId); // Scroll behavior if (!preserveScroll || wasAtBottom) { this.scrollToBottom(); } setTimeout(() => this.scrollToBottom(), 100); } else { this.showError(this.messagesContainer, data.message || 'Gagal memuat pesan'); } } catch (err) { this.showError(this.messagesContainer, 'Terjadi kesalahan'); } } async handleSendMessage(e) { e.preventDefault(); const message = this.messageInput.value.trim(); if (!message) return; const conversationId = this.currentConversationIdInput.value; // Clear input immediately for better UX this.messageInput.value = ''; this.messageInput.style.height = 'auto'; // Optimistic UI: Show message immediately with loading status const tempId = 'temp_' + Date.now(); const optimisticMessage = { ID: tempId, MESSAGE_TEXT: message, IS_MINE: true, CREATED_AT: new Date().toISOString(), STATUS: 'sending' // sending, sent, failed }; // Add to messages array and render this.messages.push(optimisticMessage); this.renderOptimisticMessage(optimisticMessage); this.scrollToBottom(); try { // Include product_id if this is first message with pending product const payload = { conversation_id: conversationId, message: message }; if (this.pendingProductAttachment) { payload.product_id = this.pendingProductAttachment.id; if (this.pendingProductAttachment.variant_id) { payload.variant_id = this.pendingProductAttachment.variant_id; } } const res = await fetch(`${this.API_BASE}/send.php`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.csrfToken }, body: JSON.stringify(payload) }); const data = await res.json(); if (data.success) { // Remove temp message from array this.messages = this.messages.filter(m => m.ID !== tempId); // Remove temp message element from DOM const tempEl = this.messagesContainer.querySelector(`[data-message-id="${tempId}"]`); if (tempEl) tempEl.remove(); // Clear product attachment after successful send if (this.pendingProductAttachment) { this.removeProductAttachment(); } // Reload messages to get real ID and sync (preserve scroll) this.loadMessages(conversationId, true); } else { // Mark as failed this.updateMessageStatus(tempId, 'failed', message, conversationId); } } catch (err) { console.error('Failed to send message:', err); // Mark as failed this.updateMessageStatus(tempId, 'failed', message, conversationId); } } renderOptimisticMessage(msg) { const messageDiv = document.createElement('div'); messageDiv.className = 'chat-message mine'; messageDiv.dataset.messageId = msg.ID; let bubbleHTML = '
'; bubbleHTML += `
${this.escapeHtml(msg.MESSAGE_TEXT)}
`; bubbleHTML += '
'; bubbleHTML += `${this.formatTime(msg.CREATED_AT)}`; // Status icon based on state if (msg.STATUS === 'sending') { bubbleHTML += ''; } else if (msg.STATUS === 'failed') { bubbleHTML += ` `; } else { bubbleHTML += ` `; } bubbleHTML += '
'; messageDiv.innerHTML = bubbleHTML; // Fade in animation messageDiv.style.opacity = '0'; messageDiv.style.transition = 'opacity 0.3s ease'; this.messagesContainer.appendChild(messageDiv); requestAnimationFrame(() => { messageDiv.style.opacity = '1'; }); } updateMessageStatus(messageId, status, messageText = null, conversationId = null) { const messageEl = this.messagesContainer.querySelector(`[data-message-id="${messageId}"]`); if (!messageEl) return; const statusIcon = messageEl.querySelector('.message-status'); if (!statusIcon) return; // Remove all status classes statusIcon.classList.remove('loading', 'sent', 'failed'); if (status === 'sent') { statusIcon.classList.add('sent'); statusIcon.innerHTML = ` `; } else if (status === 'failed') { statusIcon.classList.add('failed'); statusIcon.setAttribute('title', 'Gagal terkirim. Klik untuk coba lagi'); statusIcon.innerHTML = ` `; // Add retry click handler statusIcon.style.cursor = 'pointer'; statusIcon.addEventListener('click', async () => { // Change to loading statusIcon.classList.remove('failed'); statusIcon.classList.add('loading'); statusIcon.innerHTML = ''; // Retry send try { const res = await fetch(`${this.API_BASE}/send.php`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.csrfToken }, body: JSON.stringify({ conversation_id: conversationId, message: messageText }) }); const data = await res.json(); if (data.success) { this.updateMessageStatus(messageId, 'sent'); setTimeout(() => { this.loadMessages(conversationId, true); }, 500); } else { this.updateMessageStatus(messageId, 'failed', messageText, conversationId); } } catch (err) { this.updateMessageStatus(messageId, 'failed', messageText, conversationId); } }); } } async handleStartConversation(e) { e.preventDefault(); const subject = this.newSubjectInput.value.trim() || 'Bantuan Pelanggan'; const message = this.newMessageInput.value.trim(); if (!message) { alert('Pesan tidak boleh kosong'); return; } try { const res = await fetch(`${this.API_BASE}/start.php`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.csrfToken }, body: JSON.stringify({ subject: subject, initial_message: message }) }); const data = await res.json(); if (data.success) { // Clear form this.newSubjectInput.value = ''; this.newMessageInput.value = ''; // Open conversation this.showMessagesView(data.data.conversation_id); } else { alert(data.message || 'Gagal membuat percakapan'); } } catch (err) { console.error('Failed to start conversation:', err); alert('Terjadi kesalahan'); } } async markAsRead(conversationId) { try { await fetch(`${this.API_BASE}/mark_read.php`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.csrfToken }, body: JSON.stringify({conversation_id: conversationId}) }); // Update unread count this.updateUnreadCount(); } catch (err) { console.error('Failed to mark as read:', err); } } async updateUnreadCount() { // Don't call API if user is not logged in const isLoggedIn = this.widget.dataset.isLoggedIn === 'true'; if (!isLoggedIn) { return; } try { const res = await fetch(`${this.API_BASE}/unread.php`); // Handle session expired (logged out in another tab) if (res.status === 401 || res.status === 403) { this.handleSessionExpired(); return; } const data = await res.json(); if (data.success) { this.unreadCount = data.data.unread_count; this.updateUnreadBadge(); } } catch (err) { // Silent fail for polling } } updateUnreadBadge() { if (this.unreadBadge) { this.unreadBadge.textContent = this.unreadCount; this.unreadBadge.style.display = this.unreadCount > 0 ? 'block' : 'none'; } // Also update bottom nav badge for mobile const bottomChatBadge = document.getElementById('bottomChatCount'); if (bottomChatBadge) { bottomChatBadge.textContent = this.unreadCount; bottomChatBadge.style.display = this.unreadCount > 0 ? 'flex' : 'none'; } } renderMessages() { if (this.messages.length === 0) { if (!this.messagesContainer.querySelector('.text-center')) { this.messagesContainer.innerHTML = `
Belum ada pesan
`; } return; } // Remove loading and empty state if exists const loadingState = this.messagesContainer.querySelector('.chat-loading'); if (loadingState) loadingState.remove(); const emptyState = this.messagesContainer.querySelector('.text-center'); if (emptyState) emptyState.remove(); // Get existing message IDs and date separators for differential update const existingMessages = this.messagesContainer.querySelectorAll('.chat-message'); const existingIds = Array.from(existingMessages).map(el => parseInt(el.dataset.messageId || 0)); const existingSeparators = new Set(Array.from(this.messagesContainer.querySelectorAll('.date-separator')).map(el => el.dataset.dateKey)); // Group messages by date and render with separators let lastDateKey = null; this.messages.forEach((msg, index) => { const msgDateKey = this.getDateKey(msg.CREATED_AT); // Insert date separator if date changed if (msgDateKey !== lastDateKey && !existingSeparators.has(msgDateKey)) { const separator = document.createElement('div'); separator.className = 'date-separator'; separator.dataset.dateKey = msgDateKey; separator.innerHTML = `
${this.getDateLabel(msg.CREATED_AT)}
`; this.messagesContainer.appendChild(separator); existingSeparators.add(msgDateKey); } lastDateKey = msgDateKey; if (existingIds.includes(msg.ID)) return; // Skip already rendered const messageDiv = document.createElement('div'); messageDiv.className = `chat-message ${msg.IS_MINE ? 'mine' : ''}`; messageDiv.dataset.messageId = msg.ID; // Build message bubble with WhatsApp-style structure let bubbleHTML = '
'; // Sender name (only for non-mine messages) if (!msg.IS_MINE) { bubbleHTML += `
Admin Support
`; } // Product attachment (if exists) if (msg.PRODUCT && msg.PRODUCT.id) { const variantHTML = msg.PRODUCT.variant_summary ? `
${this.escapeHtml(msg.PRODUCT.variant_summary)}
` : ''; bubbleHTML += `
${this.escapeHtml(msg.PRODUCT.name)}
${this.escapeHtml(msg.PRODUCT.name)}
${variantHTML}
Rp ${this.formatPrice(msg.PRODUCT.price)}
`; } // Message text bubbleHTML += `
${this.escapeHtml(msg.MESSAGE_TEXT)}
`; // Message metadata (time + status) bubbleHTML += '
'; bubbleHTML += `${this.formatTime(msg.CREATED_AT)}`; // Status icon (only for mine messages) - checkmarks for sent/delivered/read if (msg.IS_MINE) { bubbleHTML += ` `; } bubbleHTML += '
'; // close message-meta bubbleHTML += '
'; // close chat-message-bubble messageDiv.innerHTML = bubbleHTML; // Smooth fade-in animation messageDiv.style.opacity = '0'; messageDiv.style.transition = 'opacity 0.3s ease'; this.messagesContainer.appendChild(messageDiv); requestAnimationFrame(() => { messageDiv.style.opacity = '1'; }); }); } formatPrice(price) { // Format number with thousands separator return Math.floor(price).toLocaleString('id-ID'); } getDateLabel(dateStr) { if (!dateStr) return ''; const msgDate = new Date(dateStr); const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); // Reset time for comparison today.setHours(0, 0, 0, 0); yesterday.setHours(0, 0, 0, 0); msgDate.setHours(0, 0, 0, 0); if (msgDate.getTime() === today.getTime()) { return 'Hari ini'; } else if (msgDate.getTime() === yesterday.getTime()) { return 'Kemarin'; } else { return msgDate.toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' }); } } getDateKey(dateStr) { if (!dateStr) return ''; const date = new Date(dateStr); return date.toISOString().split('T')[0]; // YYYY-MM-DD } showLoading(container) { container.innerHTML = '
Memuat...
'; } showError(container, message) { container.innerHTML = `
${this.escapeHtml(message)}
`; } scrollToBottom() { if (this.messagesContainer) { this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight; } } autoResizeTextarea(textarea) { textarea.style.height = 'auto'; textarea.style.height = Math.min(textarea.scrollHeight, 100) + 'px'; } startPolling() { // Check if user is logged in const isLoggedIn = this.widget.dataset.isLoggedIn === 'true'; if (!isLoggedIn) { return; // Don't poll if not logged in } // Initial load this.updateUnreadCount(); // Smart polling based on widget state this.pollingInterval = setInterval(() => { // Always update unread count (lightweight) this.updateUnreadCount(); // Only poll content when widget is open if (this.isWidgetOpen) { if (this.currentView === 'messages' && this.currentConversationId) { // Refresh messages (preserve scroll position) this.loadMessages(this.currentConversationId, true); } // Single-conversation model - no list view to refresh } }, this.pollingRate); } stopPolling() { if (this.pollingInterval) { clearInterval(this.pollingInterval); } } formatDate(dateStr) { const date = new Date(dateStr); const now = new Date(); const diff = now - date; const days = Math.floor(diff / (1000 * 60 * 60 * 24)); if (days === 0) { return this.formatTime(dateStr); } else if (days === 1) { return 'Kemarin'; } else if (days < 7) { return days + ' hari lalu'; } else { return date.toLocaleDateString('id-ID', {day: 'numeric', month: 'short'}); } } formatTime(dateStr) { const date = new Date(dateStr); return date.toLocaleTimeString('id-ID', {hour: '2-digit', minute: '2-digit'}); } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } showLoginRequired() { const modal = document.getElementById('chatLoginModal'); if (!modal) return; // Show modal modal.classList.add('show'); // Handle confirm button const confirmBtn = document.getElementById('chatLoginConfirm'); const cancelBtn = document.getElementById('chatLoginCancel'); const handleConfirm = () => { const loginUrl = '/login'; const returnUrl = window.location.pathname; window.location.href = loginUrl + '?redirect=' + encodeURIComponent(returnUrl); }; const handleCancel = () => { modal.classList.remove('show'); confirmBtn.removeEventListener('click', handleConfirm); cancelBtn.removeEventListener('click', handleCancel); }; // Remove existing listeners before adding new ones confirmBtn.removeEventListener('click', handleConfirm); cancelBtn.removeEventListener('click', handleCancel); confirmBtn.addEventListener('click', handleConfirm); cancelBtn.addEventListener('click', handleCancel); // Close on overlay click modal.addEventListener('click', (e) => { if (e.target === modal) { handleCancel(); } }); } handleSessionExpired() { // Stop polling immediately this.stopPolling(); // Update widget state this.widget.dataset.isLoggedIn = 'false'; this.isWidgetOpen = false; // Show session expired message if (this.popup && this.popup.style.display !== 'none') { this.messagesContainer.innerHTML = `

Sesi Berakhir

Anda telah logout di tab lain. Silakan login kembali untuk melanjutkan chat.

`; // Attach event listener (CSP-compliant) const loginBtn = document.getElementById('chat-session-expired-login-btn'); if (loginBtn) { loginBtn.addEventListener('click', () => { const returnUrl = window.location.pathname; window.location.href = '/login?redirect=' + encodeURIComponent(returnUrl); }); } } // Update badge to hide this.unreadCount = 0; this.updateUnreadBadge(); } attachProduct(productData) { this.pendingProductAttachment = { id: productData.id, name: productData.name, price: productData.price, image: productData.image, url: productData.url, variant_id: productData.variant_id || null, variant_summary: productData.variant_summary || null }; this.renderPendingProductCard(); } renderPendingProductCard() { if (!this.pendingProductAttachment || !this.messageForm) return; let productCard = this.messageForm.querySelector('.chat-product-card'); if (!productCard) { productCard = document.createElement('div'); productCard.className = 'chat-product-card'; this.messageForm.insertBefore(productCard, this.messageInput); } const product = this.pendingProductAttachment; const variantHTML = product.variant_summary ? `
${this.escapeHtml(product.variant_summary)}
` : ''; productCard.innerHTML = ` ${this.escapeHtml(product.name)}
${this.escapeHtml(product.name)}
${variantHTML}
${this.escapeHtml(product.price)}
`; const removeBtn = productCard.querySelector('#chat-remove-product-btn'); if (removeBtn) { removeBtn.addEventListener('click', () => this.removeProductAttachment()); } } removeProductAttachment() { this.pendingProductAttachment = null; // Remove product card from DOM const productCard = this.messageForm?.querySelector('.chat-product-card'); if (productCard) { productCard.remove(); } } destroy() { this.stopPolling(); } } // Initialize chat widget when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { if (document.getElementById('chat-widget')) { window.plazaChat = new PlazaChat(); } }); } else { if (document.getElementById('chat-widget')) { window.plazaChat = new PlazaChat(); } }