/**
* 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 += '
';
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)}
${variantHTML}
Rp ${this.formatPrice(msg.PRODUCT.price)}
`;
}
// Message text
bubbleHTML += `
${this.escapeHtml(msg.MESSAGE_TEXT)}
`;
// Message metadata (time + status)
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)}
${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();
}
}