import React, { useState, useEffect, useRef } from 'react'; import { MessageCircle, X, Send, Minimize2, Maximize2, Clock } from 'lucide-react'; import chatService, { type Chat, type ChatMessage } from '../services/chatService'; import contactService, { type ContactFormData } from '../../content/services/contactService'; import useAuthStore from '../../../store/useAuthStore'; import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext'; import { toast } from 'react-toastify'; import ConfirmationDialog from '../../../shared/components/ConfirmationDialog'; import { formatWorkingHours } from '../../../shared/utils/format'; interface ChatWidgetProps { onClose?: () => void; } const ChatWidget: React.FC = ({ onClose }) => { const [isOpen, setIsOpen] = useState(false); const [isMinimized, setIsMinimized] = useState(false); const [chat, setChat] = useState(null); const [messages, setMessages] = useState([]); const [newMessage, setNewMessage] = useState(''); const [loading, setLoading] = useState(false); const [ws, setWs] = useState(null); const [visitorInfo, setVisitorInfo] = useState({ name: '', email: '', phone: '' }); const [showVisitorForm, setShowVisitorForm] = useState(false); const [formErrors, setFormErrors] = useState>({}); const [showEndChatDialog, setShowEndChatDialog] = useState(false); const [inquiry, setInquiry] = useState(''); const [inquiryEmail, setInquiryEmail] = useState(''); const [submittingInquiry, setSubmittingInquiry] = useState(false); const messagesEndRef = useRef(null); const { isAuthenticated, userInfo } = useAuthStore(); const { settings } = useCompanySettings(); // Check if current time is within business hours from settings const isBusinessHours = () => { const now = new Date(); const hour = now.getHours(); const startHour = settings.chat_working_hours_start || 9; const endHour = settings.chat_working_hours_end || 17; return hour >= startHour && hour < endHour; }; const [isWithinBusinessHours, setIsWithinBusinessHours] = useState(isBusinessHours()); // Update business hours check periodically useEffect(() => { const checkBusinessHours = () => { setIsWithinBusinessHours(isBusinessHours()); }; checkBusinessHours(); const interval = setInterval(checkBusinessHours, 60000); // Check every minute return () => clearInterval(interval); }, [settings.chat_working_hours_start, settings.chat_working_hours_end]); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { scrollToBottom(); }, [messages]); const validateVisitorForm = (): boolean => { const errors: Record = {}; if (!visitorInfo.name.trim()) { errors.name = 'Name is required'; } if (!visitorInfo.email.trim()) { errors.email = 'Email is required'; } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(visitorInfo.email)) { errors.email = 'Please enter a valid email address'; } if (!visitorInfo.phone.trim()) { errors.phone = 'Phone is required'; } setFormErrors(errors); return Object.keys(errors).length === 0; }; const createChat = async () => { if (!isAuthenticated) { if (!validateVisitorForm()) { return; } } try { setLoading(true); const response = await chatService.createChat( isAuthenticated ? undefined : { visitor_name: visitorInfo.name, visitor_email: visitorInfo.email, visitor_phone: visitorInfo.phone } ); setChat(response.data); setShowVisitorForm(false); const messagesResponse = await chatService.getMessages(response.data.id); setMessages(messagesResponse.data); connectWebSocket(response.data.id); // Show success message - chat is ready to use toast.success('Chat started! You can now send messages.'); } catch (error: any) { toast.error(error.response?.data?.detail || 'Failed to start chat'); } finally { setLoading(false); } }; const connectWebSocket = (chatId: number) => { if (ws) { ws.close(); } const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000'; const normalizedBase = baseUrl.replace(/\/$/, ''); const wsProtocol = normalizedBase.startsWith('https') ? 'wss' : 'ws'; const wsBase = normalizedBase.replace(/^https?/, wsProtocol); const wsUrl = `${wsBase}/api/chat/ws/${chatId}?user_type=visitor`; const websocket = new WebSocket(wsUrl); websocket.onopen = () => { console.log('WebSocket connected for chat', chatId); }; websocket.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'new_message') { setMessages(prev => { const exists = prev.find(m => m.id === data.data.id); if (exists) return prev; return [...prev, data.data]; }); } else if (data.type === 'chat_accepted') { if (chat) { const updatedChat = { ...chat, status: 'active' as const, staff_name: data.data.staff_name, staff_id: data.data.staff_id }; setChat(updatedChat); toast.success(`Chat accepted by ${data.data.staff_name}`); } } else if (data.type === 'chat_closed') { toast.info('Chat has been closed'); if (chat) { setChat({ ...chat, status: 'closed' }); // Optionally reset after a delay to allow user to see the closed state // The user can click "Start New Chat" button to start fresh } } } catch (error) { console.error('Error parsing WebSocket message:', error); } }; websocket.onerror = (error) => { console.error('WebSocket error:', error); }; websocket.onclose = (event) => { console.log('WebSocket disconnected', event.code, event.reason); if (event.code !== 1000 && chat) { console.log('Attempting to reconnect WebSocket...'); setTimeout(() => { if (chat) { connectWebSocket(chat.id); } }, 3000); } }; setWs(websocket); }; useEffect(() => { if (!chat || !isOpen || isMinimized) return; const pollInterval = setInterval(async () => { try { const messagesResponse = await chatService.getMessages(chat.id); if (messagesResponse.success) { setMessages(messagesResponse.data); } if (chat.status === 'pending') { try { const chatResponse = await chatService.getChat(chat.id); if (chatResponse.success) { const updatedChat = chatResponse.data; if (updatedChat.status !== chat.status || updatedChat.staff_name !== chat.staff_name || updatedChat.staff_id !== chat.staff_id) { setChat(updatedChat); if (updatedChat.status === 'active' && updatedChat.staff_name && chat.status === 'pending') { toast.success(`Chat accepted by ${updatedChat.staff_name}`); } } } } catch (error) { console.debug('Could not poll chat status:', error); } } } catch (error) { console.error('Error polling chat updates:', error); } }, 5000); return () => clearInterval(pollInterval); }, [chat, isOpen, isMinimized]); const handleOpen = () => { setIsOpen(true); setIsMinimized(false); // If outside business hours, don't show chat options if (!isWithinBusinessHours) { return; } // If there's a closed chat, reset chat state but keep visitor info if (chat && chat.status === 'closed') { setChat(null); setMessages([]); setNewMessage(''); setShowVisitorForm(false); // Don't auto-start, let user click the button } else if (isAuthenticated && !chat && !loading) { createChat(); } else if (!isAuthenticated && !chat) { // If visitor info exists, show the "Start New Chat" button instead of form if (visitorInfo.name && visitorInfo.email && visitorInfo.phone) { setShowVisitorForm(false); } else { setShowVisitorForm(true); } } }; const handleSubmitInquiry = async () => { if (!inquiryEmail.trim() || !inquiry.trim()) { toast.error('Please provide your email and inquiry'); return; } if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(inquiryEmail)) { toast.error('Please enter a valid email address'); return; } try { setSubmittingInquiry(true); const contactData: ContactFormData = { name: visitorInfo.name || 'Chat Visitor', email: inquiryEmail, phone: visitorInfo.phone || '', subject: 'Chat Inquiry - Outside Business Hours', message: inquiry }; await contactService.submitContactForm(contactData); toast.success('Your inquiry has been sent! We will get back to you as soon as possible.'); setInquiry(''); setInquiryEmail(''); setIsOpen(false); } catch (error: any) { toast.error(error.response?.data?.detail || 'Failed to send inquiry. Please try again.'); } finally { setSubmittingInquiry(false); } }; const handleClose = () => { if (ws) { ws.close(); setWs(null); } setIsOpen(false); if (onClose) onClose(); }; const handleEndChat = () => { if (!chat) return; setShowEndChatDialog(true); }; const confirmEndChat = async () => { if (!chat) return; try { setShowEndChatDialog(false); await chatService.closeChat(chat.id); toast.success('Chat ended'); if (ws) { ws.close(); setWs(null); } // Reset chat state but preserve visitor info for new chat setChat(null); setMessages([]); setNewMessage(''); setShowVisitorForm(false); // Keep visitor info so they can start a new chat with same info } catch (error: any) { toast.error(error.response?.data?.detail || 'Failed to end chat'); } }; const handleSend = async () => { if (!newMessage.trim() || !chat) return; const messageText = newMessage.trim(); setNewMessage(''); const tempMessage: ChatMessage = { id: Date.now(), chat_id: chat.id, sender_type: 'visitor', sender_name: isAuthenticated ? userInfo?.name : (visitorInfo.name || 'Guest'), message: messageText, is_read: false, created_at: new Date().toISOString() }; setMessages(prev => [...prev, tempMessage]); try { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'message', message: messageText })); } else { await chatService.sendMessage(chat.id, messageText); } } catch (error: any) { toast.error('Failed to send message'); setMessages(prev => prev.filter(m => m.id !== tempMessage.id)); } }; const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; return ( <>
{!isOpen ? ( ) : (
{}

{!isWithinBusinessHours ? 'Leave Your Inquiry' : chat?.status === 'active' && chat?.staff_name ? `Chat with ${chat.staff_name}` : chat?.status === 'closed' ? 'Chat Ended' : 'Support Chat'}

{!isWithinBusinessHours ? (

Chat available {settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined ? formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end) : '9:00 AM - 5:00 PM'}

) : chat?.status === 'pending' ? (

Waiting for staff...

) : chat?.status === 'active' ? (

Online

) : chat?.status === 'closed' ? (

This chat has ended

) : null}
{chat && chat.status !== 'closed' && ( )}
{!isMinimized && ( <> {} {!isWithinBusinessHours ? (

Chat Hours

Our chat support is available Monday to Friday, {settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined ? formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end) : '9:00 AM - 5:00 PM'}. Please leave your inquiry below and we'll get back to you as soon as possible.

setInquiryEmail(e.target.value)} className="w-full px-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/30 focus:border-[var(--luxury-gold)] transition-all duration-200 font-light" placeholder="your.email@example.com" />