diff --git a/Backend/src/routes/__pycache__/system_settings_routes.cpython-312.pyc b/Backend/src/routes/__pycache__/system_settings_routes.cpython-312.pyc index 1df28c71..c66283f4 100644 Binary files a/Backend/src/routes/__pycache__/system_settings_routes.cpython-312.pyc and b/Backend/src/routes/__pycache__/system_settings_routes.cpython-312.pyc differ diff --git a/Backend/src/routes/system_settings_routes.py b/Backend/src/routes/system_settings_routes.py index 18e22a7f..5e53ec59 100644 --- a/Backend/src/routes/system_settings_routes.py +++ b/Backend/src/routes/system_settings_routes.py @@ -838,6 +838,8 @@ class UpdateCompanySettingsRequest(BaseModel): company_email: Optional[str] = None company_address: Optional[str] = None tax_rate: Optional[float] = None + chat_working_hours_start: Optional[int] = None + chat_working_hours_end: Optional[int] = None @router.get("/company") async def get_company_settings( @@ -853,6 +855,8 @@ async def get_company_settings( "company_email", "company_address", "tax_rate", + "chat_working_hours_start", + "chat_working_hours_end", ] settings_dict = {} @@ -887,6 +891,8 @@ async def get_company_settings( "company_email": settings_dict.get("company_email", ""), "company_address": settings_dict.get("company_address", ""), "tax_rate": float(settings_dict.get("tax_rate", 0)) if settings_dict.get("tax_rate") else 0.0, + "chat_working_hours_start": int(settings_dict.get("chat_working_hours_start", 9)) if settings_dict.get("chat_working_hours_start") else 9, + "chat_working_hours_end": int(settings_dict.get("chat_working_hours_end", 17)) if settings_dict.get("chat_working_hours_end") else 17, "updated_at": updated_at, "updated_by": updated_by, } @@ -915,6 +921,10 @@ async def update_company_settings( db_settings["company_address"] = request_data.company_address if request_data.tax_rate is not None: db_settings["tax_rate"] = str(request_data.tax_rate) + if request_data.chat_working_hours_start is not None: + db_settings["chat_working_hours_start"] = str(request_data.chat_working_hours_start) + if request_data.chat_working_hours_end is not None: + db_settings["chat_working_hours_end"] = str(request_data.chat_working_hours_end) for key, value in db_settings.items(): @@ -940,7 +950,7 @@ async def update_company_settings( updated_settings = {} - for key in ["company_name", "company_tagline", "company_logo_url", "company_favicon_url", "company_phone", "company_email", "company_address", "tax_rate"]: + for key in ["company_name", "company_tagline", "company_logo_url", "company_favicon_url", "company_phone", "company_email", "company_address", "tax_rate", "chat_working_hours_start", "chat_working_hours_end"]: setting = db.query(SystemSettings).filter( SystemSettings.key == key ).first() @@ -976,6 +986,8 @@ async def update_company_settings( "company_email": updated_settings.get("company_email", ""), "company_address": updated_settings.get("company_address", ""), "tax_rate": float(updated_settings.get("tax_rate", 0)) if updated_settings.get("tax_rate") else 0.0, + "chat_working_hours_start": int(updated_settings.get("chat_working_hours_start", 9)) if updated_settings.get("chat_working_hours_start") else 9, + "chat_working_hours_end": int(updated_settings.get("chat_working_hours_end", 17)) if updated_settings.get("chat_working_hours_end") else 17, "updated_at": updated_at, "updated_by": updated_by, } diff --git a/Backend/src/utils/__pycache__/email_templates.cpython-312.pyc b/Backend/src/utils/__pycache__/email_templates.cpython-312.pyc index 85460f0d..bd5502e7 100644 Binary files a/Backend/src/utils/__pycache__/email_templates.cpython-312.pyc and b/Backend/src/utils/__pycache__/email_templates.cpython-312.pyc differ diff --git a/Frontend/src/components/chat/ChatWidget.tsx b/Frontend/src/components/chat/ChatWidget.tsx index 75e2d288..fe2a9677 100644 --- a/Frontend/src/components/chat/ChatWidget.tsx +++ b/Frontend/src/components/chat/ChatWidget.tsx @@ -1,8 +1,10 @@ import React, { useState, useEffect, useRef } from 'react'; -import { MessageCircle, X, Send, Minimize2, Maximize2 } from 'lucide-react'; -import { chatService, type Chat, type ChatMessage } from '../../services/api'; +import { MessageCircle, X, Send, Minimize2, Maximize2, Clock } from 'lucide-react'; +import { chatService, contactService, type Chat, type ChatMessage, type ContactFormData } from '../../services/api'; import useAuthStore from '../../store/useAuthStore'; +import { useCompanySettings } from '../../contexts/CompanySettingsContext'; import { toast } from 'react-toastify'; +import ConfirmationDialog from '../common/ConfirmationDialog'; interface ChatWidgetProps { onClose?: () => void; @@ -23,8 +25,34 @@ const ChatWidget: React.FC = ({ onClose }) => { }); 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' }); @@ -80,6 +108,9 @@ const ChatWidget: React.FC = ({ onClose }) => { 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 { @@ -129,6 +160,8 @@ const ChatWidget: React.FC = ({ onClose }) => { 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) { @@ -194,10 +227,61 @@ const ChatWidget: React.FC = ({ onClose }) => { const handleOpen = () => { setIsOpen(true); setIsMinimized(false); - if (isAuthenticated && !chat && !loading) { + + // 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) { - setShowVisitorForm(true); + // 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); } }; @@ -210,21 +294,30 @@ const ChatWidget: React.FC = ({ onClose }) => { if (onClose) onClose(); }; - const handleEndChat = async () => { + const handleEndChat = () => { + if (!chat) return; + setShowEndChatDialog(true); + }; + + const confirmEndChat = async () => { if (!chat) return; - if (window.confirm('Are you sure you want to end this chat?')) { - try { - await chatService.closeChat(chat.id); - toast.success('Chat ended'); - if (ws) { - ws.close(); - setWs(null); - } - setChat({ ...chat, status: 'closed' }); - } catch (error: any) { - toast.error(error.response?.data?.detail || 'Failed to end chat'); + 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'); } }; @@ -238,7 +331,7 @@ const ChatWidget: React.FC = ({ onClose }) => { id: Date.now(), chat_id: chat.id, sender_type: 'visitor', - sender_name: isAuthenticated ? userInfo?.full_name : 'Guest', + sender_name: isAuthenticated ? userInfo?.name : (visitorInfo.name || 'Guest'), message: messageText, is_read: false, created_at: new Date().toISOString() @@ -268,55 +361,65 @@ const ChatWidget: React.FC = ({ onClose }) => { } }; - if (!isOpen) { - return ( - - ); - } - return ( -
+ <> +
+ {!isOpen ? ( + + ) : ( +
{} -
-
- +
+
+
+
+
+ +
-

- {chat?.status === 'active' && chat?.staff_name +

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

- {chat?.status === 'pending' && ( -

Waiting for staff...

- )} - {chat?.status === 'active' && ( -

Online

- )} - {chat?.status === 'closed' && ( -

This chat has ended

- )} + {!isWithinBusinessHours ? ( +

+ + Chat available 9 AM - 5 PM +

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

Waiting for staff...

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

Online

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

This chat has ended

+ ) : null}
-
+
{chat && chat.status !== 'closed' && (
@@ -346,15 +449,68 @@ const ChatWidget: React.FC = ({ onClose }) => { {!isMinimized && ( <> {} - {showVisitorForm && !chat && ( -
+ {!isWithinBusinessHours ? ( +
-

+
+
+ +
+

Chat Hours

+

+ Our chat support is available Monday to Friday, {settings.chat_working_hours_start || 9}:00 AM - {settings.chat_working_hours_end || 17}: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-[#d4af37]/30 focus:border-[#d4af37] transition-all duration-200 font-light" + placeholder="your.email@example.com" + /> +
+
+ +