This commit is contained in:
Iliyan Angelov
2025-11-21 09:43:54 +02:00
parent 4488e3a795
commit b56f1a6769
14 changed files with 462 additions and 225 deletions

View File

@@ -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,
}

View File

@@ -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<ChatWidgetProps> = ({ onClose }) => {
});
const [showVisitorForm, setShowVisitorForm] = useState(false);
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
const [showEndChatDialog, setShowEndChatDialog] = useState(false);
const [inquiry, setInquiry] = useState('');
const [inquiryEmail, setInquiryEmail] = useState('');
const [submittingInquiry, setSubmittingInquiry] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(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<ChatWidgetProps> = ({ 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<ChatWidgetProps> = ({ 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<ChatWidgetProps> = ({ 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<ChatWidgetProps> = ({ 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<ChatWidgetProps> = ({ 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<ChatWidgetProps> = ({ onClose }) => {
}
};
if (!isOpen) {
return (
<button
onClick={handleOpen}
className="fixed bottom-6 right-6 z-50 bg-gradient-to-r from-blue-600 to-blue-700 text-white p-4 rounded-full shadow-2xl hover:from-blue-700 hover:to-blue-800 transition-all duration-300 hover:scale-110 flex items-center justify-center group"
aria-label="Open chat"
>
<MessageCircle className="w-6 h-6 group-hover:scale-110 transition-transform" />
{chat && chat.status === 'pending' && (
<span className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full animate-pulse"></span>
)}
</button>
);
}
return (
<div
className={`fixed bottom-6 right-6 z-50 bg-white rounded-2xl shadow-2xl border border-gray-200 flex flex-col transition-all duration-300 ${
isMinimized ? 'w-80 h-16' : 'w-96 h-[600px]'
}`}
>
<>
<div className="fixed bottom-6 right-6 z-50" style={{ willChange: 'transform' }}>
{!isOpen ? (
<button
onClick={handleOpen}
className="bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-slate-900 p-5 rounded-full shadow-2xl shadow-[#d4af37]/40 hover:from-[#c9a227] hover:to-[#d4af37] transition-all duration-300 hover:scale-110 flex items-center justify-center group relative"
aria-label="Open chat"
>
<div className="absolute inset-0 bg-gradient-to-r from-[#d4af37] to-[#c9a227] rounded-full blur-xl opacity-50 group-hover:opacity-70 transition-opacity"></div>
<MessageCircle className="w-6 h-6 group-hover:scale-110 transition-transform relative z-10" strokeWidth={2.5} />
{chat && chat.status === 'pending' && (
<span className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full animate-pulse border-2 border-white shadow-lg z-10"></span>
)}
</button>
) : (
<div
className={`luxury-glass rounded-2xl shadow-2xl border border-[#d4af37]/30 flex flex-col transition-all duration-300 ${
isMinimized ? 'w-80 h-16' : 'w-96 h-[600px]'
}`}
style={{ transformOrigin: 'bottom right' }}
>
{}
<div className="bg-gradient-to-r from-blue-600 to-blue-700 text-white p-4 rounded-t-2xl flex items-center justify-between">
<div className="flex items-center gap-2">
<MessageCircle className="w-5 h-5" />
<div className="bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-slate-900 p-4 rounded-t-2xl flex items-center justify-between relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-r from-[#d4af37]/20 to-[#c9a227]/20"></div>
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-transparent via-slate-900/20 to-transparent"></div>
<div className="flex items-center gap-3 relative z-10">
<div className="bg-slate-900/10 p-2 rounded-lg backdrop-blur-sm">
<MessageCircle className="w-5 h-5 text-slate-900" strokeWidth={2.5} />
</div>
<div>
<h3 className="font-semibold">
{chat?.status === 'active' && chat?.staff_name
<h3 className="font-serif font-semibold text-slate-900 tracking-wide">
{!isWithinBusinessHours
? 'Leave Your Inquiry'
: chat?.status === 'active' && chat?.staff_name
? `Chat with ${chat.staff_name}`
: chat?.status === 'closed'
? 'Chat Ended'
: 'Support Chat'}
</h3>
{chat?.status === 'pending' && (
<p className="text-xs text-blue-100">Waiting for staff...</p>
)}
{chat?.status === 'active' && (
<p className="text-xs text-blue-100">Online</p>
)}
{chat?.status === 'closed' && (
<p className="text-xs text-blue-100">This chat has ended</p>
)}
{!isWithinBusinessHours ? (
<p className="text-xs text-slate-700/80 font-light flex items-center gap-1">
<Clock className="w-3 h-3" />
Chat available 9 AM - 5 PM
</p>
) : chat?.status === 'pending' ? (
<p className="text-xs text-slate-700/80 font-light">Waiting for staff...</p>
) : chat?.status === 'active' ? (
<p className="text-xs text-slate-700/80 font-light">Online</p>
) : chat?.status === 'closed' ? (
<p className="text-xs text-slate-700/80 font-light">This chat has ended</p>
) : null}
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 relative z-10">
{chat && chat.status !== 'closed' && (
<button
onClick={handleEndChat}
className="px-3 py-1 text-xs bg-red-500/20 hover:bg-red-500/30 rounded transition-colors border border-red-400/30"
className="px-3 py-1.5 text-xs bg-red-500/20 hover:bg-red-500/30 rounded-lg transition-all duration-200 border border-red-400/30 text-red-700 font-semibold hover:shadow-md"
title="End chat"
>
End Chat
@@ -324,21 +427,21 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
)}
<button
onClick={() => setIsMinimized(!isMinimized)}
className="p-1 hover:bg-blue-700 rounded transition-colors"
className="p-2 hover:bg-slate-900/10 rounded-lg transition-all duration-200 backdrop-blur-sm"
aria-label={isMinimized ? 'Maximize' : 'Minimize'}
>
{isMinimized ? (
<Maximize2 className="w-4 h-4" />
<Maximize2 className="w-4 h-4 text-slate-900" />
) : (
<Minimize2 className="w-4 h-4" />
<Minimize2 className="w-4 h-4 text-slate-900" />
)}
</button>
<button
onClick={handleClose}
className="p-1 hover:bg-blue-700 rounded transition-colors"
className="p-2 hover:bg-slate-900/10 rounded-lg transition-all duration-200 backdrop-blur-sm"
aria-label="Close chat widget"
>
<X className="w-4 h-4" />
<X className="w-4 h-4 text-slate-900" />
</button>
</div>
</div>
@@ -346,15 +449,68 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
{!isMinimized && (
<>
{}
{showVisitorForm && !chat && (
<div className="flex-1 overflow-y-auto p-4 bg-gray-50">
{!isWithinBusinessHours ? (
<div className="flex-1 overflow-y-auto p-4 bg-gradient-to-b from-slate-50/50 to-white">
<div className="max-w-md mx-auto">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
<div className="mb-6 p-4 bg-gradient-to-r from-[#d4af37]/10 to-[#c9a227]/10 rounded-xl border border-[#d4af37]/20">
<div className="flex items-start gap-3">
<Clock className="w-5 h-5 text-[#d4af37] mt-0.5 flex-shrink-0" />
<div>
<p className="text-sm font-semibold text-slate-900 mb-1">Chat Hours</p>
<p className="text-xs text-slate-600 font-light">
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.
</p>
</div>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">
Your Email <span className="text-red-500">*</span>
</label>
<input
type="email"
value={inquiryEmail}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">
Your Inquiry <span className="text-red-500">*</span>
</label>
<textarea
value={inquiry}
onChange={(e) => setInquiry(e.target.value)}
rows={6}
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 resize-none"
placeholder="Please describe your inquiry or question..."
/>
</div>
<button
onClick={handleSubmitInquiry}
disabled={submittingInquiry || !inquiryEmail.trim() || !inquiry.trim()}
className="w-full bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-slate-900 px-4 py-3 rounded-xl hover:from-[#c9a227] hover:to-[#d4af37] disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed transition-all duration-300 font-semibold shadow-lg shadow-[#d4af37]/30 hover:shadow-xl disabled:shadow-none"
>
{submittingInquiry ? 'Sending...' : 'Send Inquiry'}
</button>
<p className="text-xs text-slate-500 text-center font-light">
We'll respond to your inquiry as soon as possible.
</p>
</div>
</div>
</div>
) : showVisitorForm && !chat && (
<div className="flex-1 overflow-y-auto p-4 bg-gradient-to-b from-slate-50/50 to-white">
<div className="max-w-md mx-auto">
<h3 className="text-lg font-serif font-semibold text-slate-900 mb-4 tracking-wide">
Please provide your information to start chatting
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-slate-700 mb-1.5">
Name <span className="text-red-500">*</span>
</label>
<input
@@ -364,8 +520,8 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
setVisitorInfo({ ...visitorInfo, name: e.target.value });
if (formErrors.name) setFormErrors({ ...formErrors, name: '' });
}}
className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${
formErrors.name ? 'border-red-500' : 'border-gray-300'
className={`w-full px-4 py-2.5 border-2 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#d4af37]/30 transition-all duration-200 font-light ${
formErrors.name ? 'border-red-500 focus:border-red-500' : 'border-slate-200 focus:border-[#d4af37]'
}`}
placeholder="Your full name"
/>
@@ -374,7 +530,7 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-slate-700 mb-1.5">
Email <span className="text-red-500">*</span>
</label>
<input
@@ -384,8 +540,8 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
setVisitorInfo({ ...visitorInfo, email: e.target.value });
if (formErrors.email) setFormErrors({ ...formErrors, email: '' });
}}
className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${
formErrors.email ? 'border-red-500' : 'border-gray-300'
className={`w-full px-4 py-2.5 border-2 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#d4af37]/30 transition-all duration-200 font-light ${
formErrors.email ? 'border-red-500 focus:border-red-500' : 'border-slate-200 focus:border-[#d4af37]'
}`}
placeholder="your.email@example.com"
/>
@@ -394,7 +550,7 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-slate-700 mb-1.5">
Phone <span className="text-red-500">*</span>
</label>
<input
@@ -404,8 +560,8 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
setVisitorInfo({ ...visitorInfo, phone: e.target.value });
if (formErrors.phone) setFormErrors({ ...formErrors, phone: '' });
}}
className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${
formErrors.phone ? 'border-red-500' : 'border-gray-300'
className={`w-full px-4 py-2.5 border-2 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#d4af37]/30 transition-all duration-200 font-light ${
formErrors.phone ? 'border-red-500 focus:border-red-500' : 'border-slate-200 focus:border-[#d4af37]'
}`}
placeholder="+1 (555) 123-4567"
/>
@@ -416,7 +572,7 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
<button
onClick={createChat}
disabled={loading}
className="w-full bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors font-medium"
className="w-full bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-slate-900 px-4 py-3 rounded-xl hover:from-[#c9a227] hover:to-[#d4af37] disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed transition-all duration-300 font-semibold shadow-lg shadow-[#d4af37]/30 hover:shadow-xl disabled:shadow-none"
>
{loading ? 'Starting chat...' : 'Start Chat'}
</button>
@@ -426,14 +582,47 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
)}
{}
{!showVisitorForm && (
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50">
{!showVisitorForm && isWithinBusinessHours && (
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gradient-to-b from-slate-50/50 to-white">
{loading && !chat ? (
<div className="text-center text-gray-500 py-8">
<div className="text-center text-slate-500 py-8 font-light">
Starting chat...
</div>
) : (chat && chat.status === 'closed') || (!chat && !isAuthenticated && visitorInfo.name && visitorInfo.email && visitorInfo.phone) ? (
<div className="flex flex-col items-center justify-center py-12 px-4">
<div className="text-center mb-6">
<p className="text-sm text-slate-600 mb-2 font-light">
{chat && chat.status === 'closed' ? 'This chat has ended.' : 'Ready to start a new chat.'}
</p>
{!isAuthenticated && visitorInfo.name && visitorInfo.email && visitorInfo.phone && (
<p className="text-xs text-slate-500 mb-4 font-light">
Start a new chat with {visitorInfo.name}
</p>
)}
</div>
<button
onClick={() => {
setChat(null);
setMessages([]);
setNewMessage('');
// If visitor info exists, start chat directly, otherwise show form
if (!isAuthenticated && visitorInfo.name && visitorInfo.email && visitorInfo.phone) {
setShowVisitorForm(false);
createChat();
} else if (!isAuthenticated) {
setShowVisitorForm(true);
} else {
createChat();
}
}}
disabled={loading}
className="bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-slate-900 px-8 py-3 rounded-xl hover:from-[#c9a227] hover:to-[#d4af37] transition-all duration-300 font-semibold shadow-lg shadow-[#d4af37]/30 hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Starting...' : 'Start New Chat'}
</button>
</div>
) : messages.length === 0 ? (
<div className="text-center text-gray-500 py-8">
<div className="text-center text-slate-500 py-8 font-light">
No messages yet. Start the conversation!
</div>
) : (
@@ -445,25 +634,25 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
}`}
>
<div
className={`max-w-[80%] rounded-lg p-3 ${
className={`max-w-[80%] rounded-xl p-4 shadow-lg ${
message.sender_type === 'visitor'
? 'bg-blue-600 text-white'
: 'bg-white text-gray-800 border border-gray-200'
? 'bg-gradient-to-br from-[#d4af37] to-[#c9a227] text-slate-900 border border-[#d4af37]/30'
: 'bg-white text-slate-800 border border-slate-200/60 shadow-sm'
}`}
>
{message.sender_type === 'staff' && (
<div className="text-xs font-semibold mb-1 text-blue-600">
<div className="text-xs font-semibold mb-1.5 text-[#d4af37] tracking-wide">
{message.sender_name || 'Staff'}
</div>
)}
<p className="text-sm whitespace-pre-wrap break-words">
<p className="text-sm whitespace-pre-wrap break-words leading-relaxed">
{message.message}
</p>
<p
className={`text-xs mt-1 ${
className={`text-xs mt-2 ${
message.sender_type === 'visitor'
? 'text-blue-100'
: 'text-gray-500'
? 'text-slate-700/70'
: 'text-slate-500'
}`}
>
{new Date(message.created_at).toLocaleTimeString([], {
@@ -481,34 +670,47 @@ const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
{}
{!showVisitorForm && chat && chat.status !== 'closed' && (
<div className="p-4 border-t border-gray-200 bg-white rounded-b-2xl">
<div className="flex gap-2">
<div className="p-4 border-t border-[#d4af37]/20 bg-white/90 backdrop-blur-sm rounded-b-2xl">
<div className="flex gap-3">
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Type your message..."
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
className="flex-1 px-4 py-3 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"
/>
<button
onClick={handleSend}
disabled={!newMessage.trim()}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors flex items-center gap-2"
className="bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-slate-900 px-5 py-3 rounded-xl hover:from-[#c9a227] hover:to-[#d4af37] disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed transition-all duration-300 flex items-center gap-2 font-semibold shadow-lg shadow-[#d4af37]/30 hover:shadow-xl disabled:shadow-none"
>
<Send className="w-4 h-4" />
</button>
</div>
{chat?.status === 'pending' && (
<p className="text-xs text-gray-500 mt-2">
Waiting for a staff member to accept your chat. You can still send messages.
<p className="text-xs text-slate-500 mt-2 font-light">
Your message will be delivered. A staff member will respond soon.
</p>
)}
</div>
)}
</>
)}
</div>
)}
</div>
<ConfirmationDialog
isOpen={showEndChatDialog}
onClose={() => setShowEndChatDialog(false)}
onConfirm={confirmEndChat}
title="End Chat"
message="Are you sure you want to end this chat? You can start a new chat anytime."
confirmText="End Chat"
cancelText="Cancel"
variant="warning"
/>
</>
);
};

View File

@@ -9,14 +9,47 @@ const OfflineIndicator: React.FC = () => {
return (
<div
className="fixed bottom-0 left-0 right-0 z-50 bg-yellow-500 text-white px-4 py-3 shadow-lg flex items-center justify-center gap-2 animate-slide-up"
className="fixed bottom-0 left-0 right-0 z-50 animate-slide-up"
role="alert"
aria-live="polite"
>
<WifiOff className="w-5 h-5" />
<span className="text-sm font-medium">
You're currently offline. Some features may be unavailable.
</span>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-4 sm:pb-6">
<div className="relative overflow-hidden rounded-lg shadow-2xl border border-[#d4af37]/30 backdrop-blur-xl bg-gradient-to-r from-slate-900/95 via-slate-800/95 to-slate-900/95">
{/* Gold accent border top */}
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-transparent via-[#d4af37] to-transparent"></div>
{/* Subtle glow effect */}
<div className="absolute inset-0 bg-gradient-to-r from-[#d4af37]/5 via-transparent to-[#d4af37]/5 pointer-events-none"></div>
{/* Content */}
<div className="relative px-6 py-4 sm:px-8 sm:py-5 flex items-center justify-center gap-3 sm:gap-4">
{/* Icon with gold accent */}
<div className="relative flex-shrink-0">
<div className="absolute inset-0 bg-[#d4af37]/20 blur-xl rounded-full"></div>
<div className="relative bg-gradient-to-br from-[#d4af37] to-[#c9a227] p-2.5 rounded-full shadow-lg shadow-[#d4af37]/30">
<WifiOff className="w-5 h-5 sm:w-6 sm:h-6 text-slate-900" strokeWidth={2.5} />
</div>
</div>
{/* Text content */}
<div className="flex flex-col sm:flex-row sm:items-center sm:gap-2 text-center sm:text-left">
<span className="text-sm sm:text-base font-serif font-semibold text-white tracking-wide">
Connection Lost
</span>
<span className="text-xs sm:text-sm text-slate-300 font-light tracking-wide">
You're currently offline. Some features may be unavailable.
</span>
</div>
{/* Decorative elements */}
<div className="hidden sm:flex absolute right-6 top-1/2 -translate-y-1/2 items-center gap-1 opacity-30">
<div className="w-1 h-1 rounded-full bg-[#d4af37] animate-pulse"></div>
<div className="w-1 h-1 rounded-full bg-[#d4af37] animate-pulse" style={{ animationDelay: '0.2s' }}></div>
<div className="w-1 h-1 rounded-full bg-[#d4af37] animate-pulse" style={{ animationDelay: '0.4s' }}></div>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@@ -15,6 +15,8 @@ type CompanySettings = {
company_phone: string;
company_email: string;
company_address: string;
chat_working_hours_start: number;
chat_working_hours_end: number;
};
type CompanySettingsContextValue = {
@@ -31,6 +33,8 @@ const defaultSettings: CompanySettings = {
company_phone: '',
company_email: '',
company_address: '',
chat_working_hours_start: 9,
chat_working_hours_end: 17,
};
const CompanySettingsContext = createContext<CompanySettingsContextValue | undefined>(undefined);
@@ -65,6 +69,8 @@ export const CompanySettingsProvider: React.FC<CompanySettingsProviderProps> = (
company_phone: response.data.company_phone || defaultSettings.company_phone,
company_email: response.data.company_email || defaultSettings.company_email,
company_address: response.data.company_address || defaultSettings.company_address,
chat_working_hours_start: response.data.chat_working_hours_start || defaultSettings.chat_working_hours_start,
chat_working_hours_end: response.data.chat_working_hours_end || defaultSettings.chat_working_hours_end,
});

View File

@@ -1,58 +0,0 @@
import { UpdatePageContentData } from '../services/api';
export const luxuryContentSeed: {
home: Partial<UpdatePageContentData>;
} = {
home: {
luxury_section_title: 'Experience Unparalleled Luxury',
luxury_section_subtitle: 'Where elegance meets comfort in every detail',
luxury_section_image: '',
luxury_features: [
{
icon: 'Crown',
title: 'Premium Accommodations',
description: 'Indulge in our exquisitely designed suites and rooms, each thoughtfully crafted to provide the ultimate in comfort and sophistication.'
},
{
icon: 'Sparkles',
title: 'World-Class Service',
description: 'Our dedicated team of hospitality professionals is committed to exceeding your expectations with personalized, attentive service.'
},
{
icon: 'Wine',
title: 'Fine Dining',
description: 'Savor exceptional culinary experiences at our award-winning restaurants, featuring world-renowned chefs and the finest ingredients.'
},
{
icon: 'Umbrella',
title: 'Exclusive Amenities',
description: 'Enjoy access to our state-of-the-art spa, fitness center, and exclusive concierge services designed to enhance your stay.'
}
],
luxury_gallery: [],
luxury_testimonials: [
{
name: 'Sarah Johnson',
title: 'Luxury Travel Enthusiast',
quote: 'An absolutely breathtaking experience. Every detail was perfect, from the elegant decor to the impeccable service. This is what true luxury hospitality means.',
image: ''
},
{
name: 'Michael Chen',
title: 'Business Executive',
quote: 'The perfect blend of sophistication and comfort. The attention to detail and personalized service made my stay truly memorable. Highly recommended.',
image: ''
},
{
name: 'Emma Williams',
title: 'Travel Blogger',
quote: 'From the moment I arrived, I was treated like royalty. The luxurious amenities and world-class service exceeded all my expectations. A truly exceptional experience.',
image: ''
}
],
about_preview_title: 'Discover Our Story',
about_preview_content: 'For decades, we have been dedicated to creating extraordinary experiences for our guests. Our commitment to excellence, attention to detail, and passion for hospitality have made us a destination of choice for discerning travelers seeking the finest in luxury accommodations.',
about_preview_image: ''
}
};

View File

@@ -607,42 +607,6 @@ const PageContentDashboard: React.FC = () => {
{/* Home Tab */}
{activeTab === 'home' && (
<div className="space-y-8">
{/* Seed Data Button */}
<div className="bg-gradient-to-r from-purple-50 to-indigo-50 border-2 border-purple-200 rounded-xl p-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-1">Quick Start</h3>
<p className="text-sm text-gray-600">Load pre-configured luxury content to get started quickly</p>
</div>
<button
type="button"
onClick={() => {
if (window.confirm('This will add pre-configured luxury content to your home page. Continue?')) {
// Merge seed data with existing data, preserving all existing fields
const seedData = luxuryContentSeed.home;
setHomeData((prevData) => ({
...prevData,
// Update luxury-related fields from seed data
luxury_section_title: seedData.luxury_section_title,
luxury_section_subtitle: seedData.luxury_section_subtitle,
luxury_section_image: seedData.luxury_section_image || prevData.luxury_section_image || '',
luxury_features: Array.isArray(seedData.luxury_features) ? [...seedData.luxury_features] : [],
luxury_gallery: Array.isArray(seedData.luxury_gallery) ? [...seedData.luxury_gallery] : (prevData.luxury_gallery || []),
luxury_testimonials: Array.isArray(seedData.luxury_testimonials) ? [...seedData.luxury_testimonials] : [],
about_preview_title: seedData.about_preview_title || prevData.about_preview_title || '',
about_preview_content: seedData.about_preview_content || prevData.about_preview_content || '',
about_preview_image: seedData.about_preview_image || prevData.about_preview_image || '',
}));
toast.success('Luxury content loaded successfully! Review and save when ready.');
}
}}
className="px-6 py-2 bg-gradient-to-r from-purple-500 to-indigo-500 text-white rounded-lg font-semibold hover:from-purple-600 hover:to-indigo-600 transition-all shadow-lg"
>
Load Seed Data
</button>
</div>
</div>
{/* Home Page Content Section */}
<div className="bg-white/90 backdrop-blur-xl rounded-2xl shadow-xl border border-gray-200/50 p-8">
<h2 className="text-3xl font-extrabold text-gray-900 mb-6">Home Page Content</h2>

View File

@@ -18,7 +18,9 @@ import {
Mail,
Building2,
Upload,
Image as ImageIcon
Image as ImageIcon,
MessageCircle,
Clock
} from 'lucide-react';
import { toast } from 'react-toastify';
import adminPrivacyService, {
@@ -107,6 +109,8 @@ const SettingsPage: React.FC = () => {
company_email: '',
company_address: '',
tax_rate: 0,
chat_working_hours_start: 9,
chat_working_hours_end: 17,
});
const [logoPreview, setLogoPreview] = useState<string | null>(null);
const [faviconPreview, setFaviconPreview] = useState<string | null>(null);
@@ -234,6 +238,8 @@ const SettingsPage: React.FC = () => {
company_email: companyRes.data.company_email || '',
company_address: companyRes.data.company_address || '',
tax_rate: companyRes.data.tax_rate || 0,
chat_working_hours_start: companyRes.data.chat_working_hours_start || 9,
chat_working_hours_end: companyRes.data.chat_working_hours_end || 17,
});
@@ -2220,6 +2226,57 @@ const SettingsPage: React.FC = () => {
Default tax rate percentage to be applied to all invoices (e.g., 10 for 10%). This will be used for all bookings unless overridden.
</p>
</div>
{}
<div className="border-t border-gray-200 pt-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<MessageCircle className="w-5 h-5 text-amber-600" />
Chat Working Hours
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
<Clock className="w-4 h-4 text-gray-600" />
Start Hour (24-hour format)
</label>
<input
type="number"
min="0"
max="23"
value={companyFormData.chat_working_hours_start || 9}
onChange={(e) =>
setCompanyFormData({ ...companyFormData, chat_working_hours_start: parseInt(e.target.value) || 9 })
}
placeholder="9"
className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
/>
<p className="text-xs text-gray-500">
Hour when chat support becomes available (0-23, e.g., 9 for 9 AM)
</p>
</div>
<div className="space-y-4">
<label className="flex items-center gap-2 text-sm font-bold text-gray-900 tracking-wide">
<Clock className="w-4 h-4 text-gray-600" />
End Hour (24-hour format)
</label>
<input
type="number"
min="0"
max="23"
value={companyFormData.chat_working_hours_end || 17}
onChange={(e) =>
setCompanyFormData({ ...companyFormData, chat_working_hours_end: parseInt(e.target.value) || 17 })
}
placeholder="17"
className="w-full px-4 py-3.5 bg-white border border-gray-300 rounded-xl shadow-sm focus:ring-2 focus:ring-amber-500/50 focus:border-amber-500 transition-all duration-200 text-sm"
/>
<p className="text-xs text-gray-500">
Hour when chat support ends (0-23, e.g., 17 for 5 PM)
</p>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from 'react';
import { MessageCircle, CheckCircle, XCircle, Send, Clock, User, X } from 'lucide-react';
import { MessageCircle, CheckCircle, XCircle, Send, Clock, User, X, RefreshCw } from 'lucide-react';
import { chatService, type Chat, type ChatMessage } from '../../services/api';
import { toast } from 'react-toastify';
import { Loading, EmptyState } from '../../components/common';
@@ -311,22 +311,22 @@ const ChatManagementPage: React.FC = () => {
switch (status) {
case 'pending':
return (
<span className="px-2 py-1 text-xs font-semibold rounded-full bg-amber-100 text-amber-800 border border-amber-200">
<Clock className="w-3 h-3 inline mr-1" />
<span className="px-3 py-1.5 text-xs font-semibold rounded-full bg-gradient-to-r from-amber-50 to-yellow-50 text-amber-800 border border-amber-200/60 shadow-sm">
<Clock className="w-3 h-3 inline mr-1.5" />
Pending
</span>
);
case 'active':
return (
<span className="px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800 border border-green-200">
<CheckCircle className="w-3 h-3 inline mr-1" />
<span className="px-3 py-1.5 text-xs font-semibold rounded-full bg-gradient-to-r from-emerald-50 to-green-50 text-emerald-800 border border-emerald-200/60 shadow-sm">
<CheckCircle className="w-3 h-3 inline mr-1.5" />
Active
</span>
);
case 'closed':
return (
<span className="px-2 py-1 text-xs font-semibold rounded-full bg-gray-100 text-gray-800 border border-gray-200">
<XCircle className="w-3 h-3 inline mr-1" />
<span className="px-3 py-1.5 text-xs font-semibold rounded-full bg-gradient-to-r from-slate-50 to-gray-50 text-slate-700 border border-slate-200/60 shadow-sm">
<XCircle className="w-3 h-3 inline mr-1.5" />
Closed
</span>
);
@@ -343,22 +343,29 @@ const ChatManagementPage: React.FC = () => {
<div className="space-y-6 bg-gradient-to-br from-slate-50 via-white to-slate-50 min-h-screen -m-6 p-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-slate-900">Chat Management</h1>
<p className="text-slate-600 mt-2">Manage customer support chats</p>
<div className="flex items-center gap-3 mb-2">
<div className="h-1 w-16 bg-gradient-to-r from-[#d4af37] to-[#c9a227] rounded-full"></div>
<h1 className="text-3xl font-serif font-bold bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 bg-clip-text text-transparent tracking-tight">
Chat Management
</h1>
</div>
<p className="text-slate-600 mt-2 font-light">Manage customer support chats with elegance</p>
</div>
<button
onClick={fetchChats}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
className="px-6 py-2.5 bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-slate-900 rounded-lg font-semibold hover:from-[#c9a227] hover:to-[#d4af37] transition-all duration-300 shadow-lg shadow-[#d4af37]/30 hover:shadow-xl hover:shadow-[#d4af37]/40 flex items-center gap-2"
>
<RefreshCw className="w-4 h-4" />
Refresh
</button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 h-[calc(100vh-200px)]">
{}
<div className="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden flex flex-col">
<div className="p-4 border-b border-gray-200 bg-gradient-to-r from-blue-50 to-blue-100">
<h2 className="font-semibold text-slate-900">Chats ({chats.length})</h2>
<div className="luxury-glass rounded-xl shadow-2xl border border-[#d4af37]/20 overflow-hidden flex flex-col">
<div className="p-4 border-b border-[#d4af37]/20 bg-gradient-to-r from-[#d4af37]/10 via-[#c9a227]/10 to-[#d4af37]/10 relative">
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-transparent via-[#d4af37] to-transparent"></div>
<h2 className="font-serif font-semibold text-slate-900 tracking-wide">Chats ({chats.length})</h2>
</div>
<div className="flex-1 overflow-y-auto">
{chats.length === 0 ? (
@@ -372,8 +379,10 @@ const ChatManagementPage: React.FC = () => {
<div
key={chat.id}
onClick={() => setSelectedChat(chat)}
className={`p-4 cursor-pointer hover:bg-gray-50 transition-colors ${
selectedChat?.id === chat.id ? 'bg-blue-50 border-l-4 border-blue-600' : ''
className={`p-4 cursor-pointer hover:bg-gradient-to-r hover:from-[#d4af37]/5 hover:to-[#c9a227]/5 transition-all duration-300 ${
selectedChat?.id === chat.id
? 'bg-gradient-to-r from-[#d4af37]/10 to-[#c9a227]/10 border-l-4 border-[#d4af37] shadow-sm'
: 'border-l-4 border-transparent'
}`}
>
<div className="flex items-start justify-between mb-2">
@@ -397,7 +406,7 @@ const ChatManagementPage: React.FC = () => {
e.stopPropagation();
handleAcceptChat(chat.id);
}}
className="mt-2 w-full px-3 py-1.5 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700 transition-colors"
className="mt-2 w-full px-3 py-1.5 bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-slate-900 text-sm font-semibold rounded-lg hover:from-[#c9a227] hover:to-[#d4af37] transition-all duration-300 shadow-md shadow-[#d4af37]/30 hover:shadow-lg"
>
Accept Chat
</button>
@@ -410,19 +419,20 @@ const ChatManagementPage: React.FC = () => {
</div>
{}
<div className="lg:col-span-2 bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden flex flex-col">
<div className="lg:col-span-2 luxury-glass rounded-xl shadow-2xl border border-[#d4af37]/20 overflow-hidden flex flex-col">
{selectedChat ? (
<>
<div className="p-4 border-b border-gray-200 bg-gradient-to-r from-blue-50 to-blue-100 flex items-center justify-between">
<div className="p-4 border-b border-[#d4af37]/20 bg-gradient-to-r from-[#d4af37]/10 via-[#c9a227]/10 to-[#d4af37]/10 flex items-center justify-between relative">
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-transparent via-[#d4af37] to-transparent"></div>
<div>
<h2 className="font-semibold text-slate-900">
<h2 className="font-serif font-semibold text-slate-900 tracking-wide">
{selectedChat.visitor_name || 'Guest'}
</h2>
{selectedChat.visitor_email && (
<p className="text-sm text-gray-600">{selectedChat.visitor_email}</p>
<p className="text-sm text-slate-600 font-light">{selectedChat.visitor_email}</p>
)}
{selectedChat.staff_name && (
<p className="text-xs text-blue-600 mt-1">
<p className="text-xs text-[#d4af37] mt-1 font-medium">
Accepted by: {selectedChat.staff_name}
</p>
)}
@@ -441,7 +451,7 @@ const ChatManagementPage: React.FC = () => {
</div>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50">
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gradient-to-b from-slate-50/50 to-white">
{loadingMessages ? (
<Loading text="Loading messages..." />
) : messages.length === 0 ? (
@@ -458,25 +468,25 @@ const ChatManagementPage: React.FC = () => {
}`}
>
<div
className={`max-w-[80%] rounded-lg p-3 ${
className={`max-w-[80%] rounded-xl p-4 shadow-lg ${
message.sender_type === 'staff'
? 'bg-blue-600 text-white'
: 'bg-white text-gray-800 border border-gray-200'
? 'bg-gradient-to-br from-[#d4af37] to-[#c9a227] text-slate-900 border border-[#d4af37]/30'
: 'bg-white text-slate-800 border border-slate-200/60 shadow-sm'
}`}
>
{message.sender_type === 'visitor' && (
<div className="text-xs font-semibold mb-1 text-blue-600">
<div className="text-xs font-semibold mb-1.5 text-[#d4af37] tracking-wide">
{message.sender_name || 'Visitor'}
</div>
)}
<p className="text-sm whitespace-pre-wrap break-words">
<p className="text-sm whitespace-pre-wrap break-words leading-relaxed">
{message.message}
</p>
<p
className={`text-xs mt-1 ${
className={`text-xs mt-2 ${
message.sender_type === 'staff'
? 'text-blue-100'
: 'text-gray-500'
? 'text-slate-700/70'
: 'text-slate-500'
}`}
>
{new Date(message.created_at).toLocaleTimeString([], {
@@ -492,27 +502,27 @@ const ChatManagementPage: React.FC = () => {
</div>
{selectedChat.status !== 'closed' && (
<div className="p-4 border-t border-gray-200 bg-white">
<div className="flex gap-2">
<div className="p-4 border-t border-[#d4af37]/20 bg-white/90 backdrop-blur-sm">
<div className="flex gap-3">
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Type your message..."
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
className="flex-1 px-4 py-3 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"
disabled={selectedChat.status === 'pending'}
/>
<button
onClick={handleSendMessage}
disabled={!newMessage.trim() || selectedChat.status === 'pending'}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors flex items-center gap-2"
className="bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-slate-900 px-5 py-3 rounded-xl hover:from-[#c9a227] hover:to-[#d4af37] disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed transition-all duration-300 flex items-center gap-2 font-semibold shadow-lg shadow-[#d4af37]/30 hover:shadow-xl disabled:shadow-none"
>
<Send className="w-4 h-4" />
</button>
</div>
{selectedChat.status === 'pending' && (
<p className="text-xs text-gray-500 mt-2">
<p className="text-xs text-slate-500 mt-2 font-light">
Accept the chat to start messaging
</p>
)}

View File

@@ -114,6 +114,7 @@ export default {
createChat,
acceptChat,
listChats,
getChat,
getMessages,
sendMessage,
closeChat,

View File

@@ -20,3 +20,7 @@ export const submitContactForm = async (
return response.data;
};
export default {
submitContactForm,
};

View File

@@ -37,8 +37,10 @@ export { default as dashboardService } from './dashboardService';
export { default as auditService } from './auditService';
export { default as pageContentService } from './pageContentService';
export { default as chatService } from './chatService';
export { default as contactService } from './contactService';
export type { CustomerDashboardStats, CustomerDashboardResponse } from './dashboardService';
export type * from './reportService';
export type * from './auditService';
export type * from './pageContentService';
export type * from './chatService';
export type * from './contactService';

View File

@@ -108,6 +108,8 @@ export interface CompanySettingsResponse {
company_email: string;
company_address: string;
tax_rate: number;
chat_working_hours_start: number;
chat_working_hours_end: number;
updated_at?: string | null;
updated_by?: string | null;
};
@@ -121,6 +123,8 @@ export interface UpdateCompanySettingsRequest {
company_email?: string;
company_address?: string;
tax_rate?: number;
chat_working_hours_start?: number;
chat_working_hours_end?: number;
}
export interface UploadLogoResponse {