updates
This commit is contained in:
@@ -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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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: ''
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -114,6 +114,7 @@ export default {
|
||||
createChat,
|
||||
acceptChat,
|
||||
listChats,
|
||||
getChat,
|
||||
getMessages,
|
||||
sendMessage,
|
||||
closeChat,
|
||||
|
||||
@@ -20,3 +20,7 @@ export const submitContactForm = async (
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export default {
|
||||
submitContactForm,
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user