From b56f1a67694ba6b42caabf5a0d9d2c34cecaf02f Mon Sep 17 00:00:00 2001 From: Iliyan Angelov Date: Fri, 21 Nov 2025 09:43:54 +0200 Subject: [PATCH] updates --- .../system_settings_routes.cpython-312.pyc | Bin 55628 -> 56625 bytes Backend/src/routes/system_settings_routes.py | 14 +- .../email_templates.cpython-312.pyc | Bin 24904 -> 24904 bytes Frontend/src/components/chat/ChatWidget.tsx | 380 ++++++++++++++---- .../components/common/OfflineIndicator.tsx | 43 +- .../src/contexts/CompanySettingsContext.tsx | 6 + Frontend/src/data/luxuryContentSeed.ts | 58 --- .../src/pages/admin/PageContentDashboard.tsx | 36 -- Frontend/src/pages/admin/SettingsPage.tsx | 59 ++- .../src/pages/staff/ChatManagementPage.tsx | 80 ++-- Frontend/src/services/api/chatService.ts | 1 + Frontend/src/services/api/contactService.ts | 4 + Frontend/src/services/api/index.ts | 2 + .../src/services/api/systemSettingsService.ts | 4 + 14 files changed, 462 insertions(+), 225 deletions(-) delete mode 100644 Frontend/src/data/luxuryContentSeed.ts 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 1df28c711b73abbb43a23398071097e9b61a000f..c66283f434fb353e3995c7afc04d4b5da706117e 100644 GIT binary patch delta 2948 zcma)8X>b(B6`r2i+56O9x(^T_Kw4Jf5QjiU67nV-u`EKwxvVsUB>@uj4j9ntim{zA?m5X5V$0PwSGD(P&O44uS3Bi$5nUcPF z{k`vXzkb{EX5PCjT)HOMZ`o{Sj$Nz#g>8AsOZM*{AU}ZX2M(1=WP?rC$@VPfl=YIa zirpFK_z%v6+z{(LhB)mwTf1O!VZ=VO_;&X)vM7 zJ`p<8cyjfKE(l$*B8!tnzQq1N*`kZT6KV(^edc_YZARHrjHq0Km# zhj1T3)fYdRWfqdN5lRsP2y+;)!oOKv0L%Nc^uI^$Ds1fUDf|;NS7DLovSNJ1J?`Vu z#`$8^IB&@`AGW5mb$w55W}lfmVQnoWr< z;o;!ck6r8V`_Fm+2MR%@j-4V7tYi=NFKZy%z+y-bddjw-RqbT zahy`5S?yDx!J6%i>BAWs6(jA_A=2b_=))e3JJzrG;rJ{5g6SIb6-oz9-c7A>TN4jSY=}10i0ap%Xf6*45l4W}d z$7v5kySiJ^7&As|zi^q}A8J*(eb`Bgp`pSn7~6}X-0y~;z3zs>9HW^%XI!@_W^4~a z(uM9Ix=97R+T%BwC97nM8QSMV(M6A4vTF~WN|iUd9sDGmWfIQuU!2$W^lZD=PP%~j zxxi$P2aX@H*eWM}1$JyOyJU|y%c&)&m5A-kr6rnW&{)<*7CjZqwvofS#w5`h|Bg2P zujtiA$vsK5J7$WRB@gRH-qV8GjTUR`vTno^?(3X_=U%-SGrQXot3a2zhTercUAYFeJ<)s6d(^A{Ba_qnaO&v%-~%+Vv(k?d*rUzS zhX~^cA%rJAsc>R0AH_sJn~+&!Vw)oGjH(T*>R4s&M8iUfh=A3k&oh{5rGJJa$L5en zaP3%)_iv0kgw3o(r!_O{25=q!QQfG;pRxp#mSDnC^}&KgS4vZh!^y?r;RRa{uS{fC zB&~B*MIdPjBrJfjjU%4G7)?x6|74J>k=!%iAeKsuw|rFVxlm=l^>P& zq~!fcc|RL>v39C<)wQfcCIZV+ftqBXCQ-XFu{k^(*g7%`+v)~>(^D^lCYVtV# z?ul;_@^@HwYE6lUy(Y&@+uC->;aw4>b&PDInOJI;8AsWRlcNTvh`OL?&;xf)<%orB zc<=Ie9#~l?{P?YPqzUTJF5m?MJ!e~7|G;&GZE}H8=+lk;p$Usm}(Qi=h#^F>1AA%nt6B^Dv%j$CST%JA)iEOxjE@)BL z&Bt*8{N-H1M4HnecW|LNhp{W->jy9B_2nq20Q=?h;(TUajQ{wuBIY7H=Am*+WS7D| zj>1v7h002JyS$%izj9?L?=CyG^aaeQ5TS-a{eYgTvsJ>pt3@RhIEA$}!alOt6QYK0 zN9ht=@X4EhF?3(;vd=|w7rN&$3w|+w3Y1*S7pvIdSMinCZW^5G%(x7#L~TVp|EG{Qeu-p-8-XTkL6=TQrEj!xe4uytL(u5H! z;j8bLi;Zj?jvsyhqCkA{s$c6#!x~il78M^N;D-#w20-ypQv5L})}G>aDQ$}%3Y4X$ l%?T6c%m}0fH11^h(jppZJ!f)`8_@hLLS<`&cz@~^`4=N9{HOo` delta 2097 zcmZvddrVVz6vyxH-uCuMDJ|t)K#K_0wGc!^(M?n^M(U=FO*2EFE%K7bZQ}+OC-@@j z?A&q7k`Wz=|1e&h#rs}n2UxqMdB-AE3?xyD1129hd=+TCxmsUZ;KSqnQoMp6Vv zJVn;)k|ZIKmIz}MjdH8Kc4u8lO@p0KH`p}P*hhblKH+?prUGs??~yq)ZAwRI8@q)u zG47~fW)dUBHp$(wLEoWu8-un--ublCt}qz+HbJ7Zb;tpl^J4DP6d@V$3nCYB7cl{E zAB#$4sdr%Q9DSVfewRUL@%mF#OKk?|=v<5MOLDuZTHs?jO~j7Fh+JRJ`MSt$$jWPjem zyv59*ndIxq;9^C-mK)5B$X9VgDk-&xv|PR(dJe?F8JmLLCiDGpz)1)PVVmRN-k~rU zSgr@x=5_FLs|L!yiG{!lDeP(07(!-N#4xjaR2dKLt;P^-FybMV^P&BqK9n!k1+}Bi zZaq9`jWo><(~tTG*%2f{TDbtk@T+C-_o zP#?<9p(*CjLgvu8=d?M8kUp2Dhl!)=kc^qB%ua^m9U3^l*O003C>>gQ{=?ch@%%3d zX43P|5~hW5Fa3{VZc@>$&!$^%RAmNrhYnUBGY~yo{VstGw?V8oKB|KguqOeFg?z+s zi2LxxvCO0iG;w!?2Z)D=^#}{XN|9=$y_jTvX@%Rl(*^A>CP0lhD{>s;QbaalGiIuV zDH!ltNdP9jxrX1V)Qnf8kz(eGs0(5JyPw0F<6p9N*7x`E&z`v=vQWGtp?F0S;f`TN zvdv8P7bF*$nc+o*+QVj@ErlCinQSw0S4>iBUrFI?X5afKhGgVEY;P+d)4t9&FCmX% z#Yvk+!|zzepBbd9E zvi~q(@3M#eiHUzv6bet!oPZl$sW~QE)2J#d*;Qe$t+7xDNzW**T{~vObu~K#hK2=# zn1-D0ES^JC0u9~fl~Odtt>ke49$lQfaGbh+j(#@<8M+%W8j6skt3W7Wtb6_v6`E>9 z2ttDpU(2Xif^GsrhtMO!2~>HLVS7&^-K;%5M&5vG1hn_0sl>5k(2s>5dlF`*IuA(i zGO3xW*}hf1y}b4XOt2tM;~>dU-=YU?pI4erwbQ=SeQs$2=J>+}r@f@kZ7(k=b~){W z(_LKUY!E9U&inxkpR*cACA5bf*i9lLmm;r&7sMelAi95E3J&B~mzTJTUGyA^Jyc<8 z7EWO1D~;|_DDB^^vZ7drej1GTuaPdJ{smv!*B4|nv&Q~~3{0~4xWO%iXsuxi(VW+FnN<-lA zer1KL*jZCjTJ3ZQ>#6GJ?bQ{|8n>_(Q(ni4d;(9r@v!xpPP&PTTYPob&TvHMn>Wgn zj18FfGp3Cp@J~m;YbD^374T&UxZ46QP{1h*n_$}wBPsR0b)%G9#*+mfXHPTq5zsQM z{7_7DOzVd4N#vL=`omy_-sTHUO-K_=It#n&%>MQ{cjXS!C ZfE0}Y&nUE0GBlr7i0UrZS9a?X`4<9JAi4km 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 85460f0d7471e1f037dd9b960858d97442f1c09b..bd5502e76f03cfb98785c4b0b05f92f2a916dc82 100644 GIT binary patch delta 20 acmX?ci1EZBM$Xf`yj%=G@M0sUeIfu%MFvp- delta 20 acmX?ci1EZBM$Xf`yj%=G(7KV+J`n&)Rt4?= 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" + /> +
+
+ +