2026-06-02 12:54:24 +05:30

499 lines
29 KiB
JavaScript

import React, { useState, useEffect, useRef } from 'react';
import { Head, Link, router } from '@inertiajs/react';
import EmployerLayout from '../../../Layouts/EmployerLayout';
import { useTranslation } from '../../../lib/LanguageContext';
import {
Send,
ArrowLeft,
CheckCircle2,
Globe2,
DollarSign,
Sparkles,
MoreVertical,
Phone,
Video,
Paperclip,
Search,
Info,
Calendar,
Briefcase,
ShieldCheck,
ChevronRight,
MapPin,
Smile,
X,
MessageSquare,
Eye,
UploadCloud,
FileText
} from 'lucide-react';
import { toast } from 'sonner';
export default function Show({ conversation, initialMessages, conversations = [] }) {
const { t } = useTranslation();
const [messages, setMessages] = useState(initialMessages || []);
const [input, setInput] = useState('');
const [isTyping, setIsTyping] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [showProfile, setShowProfile] = useState(true);
const [showAttachmentModal, setShowAttachmentModal] = useState(false);
const [uploadedFiles, setUploadedFiles] = useState([]);
const messagesEndRef = useRef(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, [messages, isTyping]);
useEffect(() => {
setMessages(initialMessages || []);
}, [initialMessages]);
// Simulate typing indicator when conversation opens
useEffect(() => {
setIsTyping(true);
const timer = setTimeout(() => setIsTyping(false), 2500);
return () => clearTimeout(timer);
}, [conversation.id]);
const filteredConversations = (conversations || []).filter(c =>
c.worker_name.toLowerCase().includes(searchTerm.toLowerCase())
);
const sendMsg = (text) => {
if (!text.trim()) return;
// Optimistically update message feed
const newMsg = {
id: Date.now(),
sender: 'employer',
text: text,
time: t('just_now', 'Just Now'),
read: false
};
setMessages(prev => [...prev, newMsg]);
setInput('');
const isLookingJobQuery = text.toLowerCase().includes('looking') && text.toLowerCase().includes('job');
router.post(`/employer/messages/${conversation.id}/send`, {
text: text
}, {
preserveScroll: true,
onSuccess: () => {
// Show immediate confirmation of the push alert popping up on worker mobile app
toast.success(t('direct_push_sent', 'Direct Mobile Push Notification sent!'), {
description: t('popped_up_instantly_desc', "Popped up instantly on {name}'s mobile device.").replace('{name}', conversation.worker_name),
duration: 4500,
});
// Simulate worker typing response shortly after
setTimeout(() => {
setIsTyping(true);
setTimeout(() => {
setIsTyping(false);
// Reload conversation to pull actual database messages (including automated worker 'Yes')
router.reload({
preserveScroll: true,
onSuccess: () => {
if (isLookingJobQuery) {
toast.success(t('candidate_hired_success', "Candidate response: YES! Automatically marked as HIRED!"), {
description: t('candidate_hired_desc', "{name} has been successfully added to your Hired candidate pipeline.").replace('{name}', conversation.worker_name),
duration: 6000,
});
} else {
toast.info(t('new_message_from', 'New message from {name}!').replace('{name}', conversation.worker_name), {
description: t('auto_reply_text', "Thank you for your response! I have reviewed your offer proposal and am looking forward to our video interview."),
duration: 5000
});
}
}
});
}, 2000);
}, 1500);
}
});
};
const handleFormSubmit = (e) => {
e.preventDefault();
sendMsg(input);
};
const handleFileUpload = (e) => {
const file = e.target.files[0];
if (!file) return;
const newFile = {
name: file.name,
size: (file.size / 1024).toFixed(1) + ' KB',
date: t('today', 'Today')
};
setUploadedFiles([...uploadedFiles, newFile]);
toast.success(t('document_attached', 'Document attached successfully!'));
setShowAttachmentModal(false);
// Optimistically insert attachment message
sendMsg(`[${t('document_attached_msg', 'Document Attached')}: ${file.name}]`);
};
const chips = [
t('chip_looking_job', "Are you looking for a job?"),
t('chip_interview', "Are you available for a video interview today?"),
t('chip_salary', "What is your final expected salary?"),
t('chip_visa', "Can you transfer your visa immediately?")
];
return (
<EmployerLayout title={null} fullPage={true}>
<Head title={`${t('chat_with', 'Chat with')} ${conversation.worker_name} - ${t('messages', 'Messages')}`} />
<div className="flex h-full bg-[#FAFBFF] overflow-hidden select-none w-full">
{/* Conversation List Sidebar (Desktop) */}
<aside className="hidden xl:flex w-90 border-r border-slate-200 flex-col flex-shrink-0 bg-white shadow-sm z-30">
<div className="p-6 border-b border-slate-100">
<div className="flex items-center justify-between mb-6">
<h2 className="font-black text-2xl text-slate-900 tracking-tight">{t('messages_hub', 'Messages Hub')}</h2>
<span className="text-[9px] font-black bg-emerald-50 text-emerald-700 px-2 py-0.5 rounded uppercase tracking-wider">
{t('ssl_secure', 'SSL SECURE')}
</span>
</div>
<div className="relative">
<input
type="text"
placeholder={t('search_conversations_placeholder', 'Search conversations...')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-5 pr-4 py-3 bg-slate-50 border-none rounded-2xl text-sm font-bold focus:ring-2 focus:ring-blue-100 transition-all placeholder:text-slate-400 outline-none"
/>
</div>
</div>
<div className="flex-1 overflow-y-auto divide-y divide-slate-50">
{filteredConversations.map((c) => (
<Link
key={c.id}
href={`/employer/messages/${c.id}`}
className={`flex items-center space-x-4 p-5 transition-all relative group ${
c.id === conversation.id ? 'bg-blue-50/50' : 'hover:bg-slate-50'
}`}
>
{c.id === conversation.id && (
<div className="absolute left-0 top-0 bottom-0 w-1 bg-[#185FA5] rounded-r-full shadow-[0_0_15px_rgba(24,95,165,0.4)]" />
)}
<div className="relative flex-shrink-0">
<div className={`w-12 h-12 rounded-[20px] text-white flex items-center justify-center font-black text-lg shadow-sm ${
c.id === conversation.id ? 'bg-gradient-to-br from-[#185FA5] to-blue-600' : 'bg-slate-200 text-slate-400'
}`}>
{c.worker_name.charAt(0)}
</div>
<span className="absolute -bottom-1 -right-1 w-3.5 h-3.5 bg-emerald-500 border-4 border-white rounded-full shadow-sm" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<span className={`text-xs font-black truncate ${c.id === conversation.id ? 'text-[#185FA5]' : 'text-slate-900'}`}>
{c.worker_name}
</span>
<span className="text-[9px] text-slate-400 font-bold uppercase tracking-tighter">{c.sent_at}</span>
</div>
<p className={`text-[11px] font-bold truncate leading-tight ${c.unread ? 'text-slate-900' : 'text-slate-400'}`}>
{c.last_message}
</p>
</div>
{c.unread && (
<div className="w-2.5 h-2.5 bg-red-500 rounded-full flex-shrink-0 shadow-[0_0_8px_rgba(239,68,68,0.4)]" />
)}
</Link>
))}
</div>
</aside>
{/* Main Chat Interface */}
<div className="flex-1 flex flex-col h-full bg-white relative z-10 shadow-2xl overflow-hidden">
{/* Header */}
<div className="bg-white p-4 sm:p-5 border-b border-slate-100 flex items-center justify-between z-45 sticky top-0 shadow-xs">
<div className="flex items-center space-x-3">
<Link
href="/employer/messages"
className="xl:hidden p-2 text-slate-400 hover:text-[#185FA5] hover:bg-blue-50 rounded-2xl transition-all"
>
<ArrowLeft className="w-5 h-5" />
</Link>
<div className="relative group cursor-pointer" onClick={() => setShowProfile(!showProfile)}>
<div className="w-11 h-11 rounded-2xl bg-gradient-to-br from-[#185FA5] to-blue-600 text-white flex items-center justify-center font-black text-base shadow-md">
{conversation.worker_name.charAt(0)}
</div>
<span className="absolute -bottom-1 -right-1 w-3.5 h-3.5 bg-emerald-500 border-4 border-white rounded-full shadow-lg" />
</div>
<div>
<div className="flex items-center gap-2">
<h3 className="font-extrabold text-slate-900 text-sm tracking-tight">{conversation.worker_name}</h3>
<span className="bg-emerald-50 text-emerald-600 p-0.5 rounded-full shadow-sm" title="OCR Verified Passport">
<ShieldCheck className="w-3.5 h-3.5" />
</span>
</div>
<div className="flex items-center space-x-1.5 mt-0.5">
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">
{t('active_now', 'Active Now')}
</span>
</div>
</div>
</div>
{/* WhatsApp Bridge Widget & Dossier Button */}
<div className="flex items-center space-x-2">
<a
href={`https://wa.me/971501112222?text=Hi, I am reviewing candidate ${conversation.worker_name} (${conversation.nationality}) on the platform. Please assist with hiring workflow details.`}
target="_blank"
rel="noreferrer"
className="px-3.5 py-2 bg-emerald-50 text-emerald-800 border border-emerald-200 hover:bg-emerald-100 rounded-xl transition-all shadow-xs flex items-center space-x-1.5 text-xs font-bold"
>
<MessageSquare className="w-4 h-4 text-emerald-600 fill-emerald-600" />
<span className="hidden sm:inline">{t('whatsapp_bridge', 'WhatsApp Bridge')}</span>
</a>
<button
onClick={() => setShowProfile(!showProfile)}
className={`p-2.5 rounded-xl transition-all shadow-xs border ${
showProfile
? 'bg-blue-50 border-blue-100 text-[#185FA5]'
: 'bg-slate-50 border-slate-200 text-slate-400 hover:text-[#185FA5]'
}`}
title="Toggle Context Sidebar"
>
<Info className="w-4.5 h-4.5" />
</button>
</div>
</div>
{/* Chat Messages Log */}
<div className="flex-1 px-6 py-6 overflow-y-auto bg-[#F8FAFC]/60 space-y-6 scroll-smooth">
<div className="flex flex-col items-center mb-6">
<div className="bg-white border border-slate-200 px-4 py-1.5 rounded-full shadow-xs">
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">{t('today_realtime_chat_logs', 'Today • Realtime Chat Logs')}</span>
</div>
</div>
{messages.map((msg, idx) => {
const isEmp = msg.sender === 'employer';
return (
<div
key={msg.id}
className={`flex w-full ${isEmp ? 'justify-end' : 'justify-start'} animate-in fade-in slide-in-from-bottom-4 duration-300`}
>
<div className={`flex flex-col ${isEmp ? 'items-end' : 'items-start'} max-w-[80%] sm:max-w-[65%]`}>
<div className={`px-5 py-3 rounded-2xl text-xs font-semibold leading-relaxed shadow-xs relative ${
isEmp
? 'bg-[#185FA5] text-white rounded-tr-none'
: 'bg-white border border-slate-100 text-slate-800 rounded-tl-none'
}`}>
{msg.text}
</div>
<div className="flex items-center space-x-1 mt-1 px-1 text-[9px] font-bold text-slate-400 uppercase">
<span>{msg.time}</span>
{isEmp && (
<span className="flex items-center space-x-0.5 text-emerald-600">
<span></span>
<span>{t('read', 'Read')}</span>
<CheckCircle2 className="w-2.5 h-2.5" />
</span>
)}
</div>
</div>
</div>
);
})}
{isTyping && (
<div className="flex w-full justify-start animate-in fade-in slide-in-from-bottom-2">
<div className="bg-white border border-slate-100 px-4 py-3 rounded-2xl rounded-tl-none shadow-xs flex items-center space-x-1.5">
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-bounce [animation-delay:-0.3s]"></div>
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-bounce [animation-delay:-0.15s]"></div>
<div className="w-1.5 h-1.5 bg-[#185FA5] rounded-full animate-bounce"></div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Quick replies & typing input */}
<div className="bg-white border-t border-slate-100 p-5 space-y-4 shadow-sm shrink-0">
<div className="flex items-center space-x-2.5 overflow-x-auto scrollbar-none pb-1.5">
<div className="flex-shrink-0 p-1.5 bg-blue-50 text-[#185FA5] rounded-lg">
<Sparkles className="w-3.5 h-3.5" />
</div>
{chips.map((chip, idx) => (
<button
key={idx}
onClick={() => sendMsg(chip)}
className="bg-slate-50 hover:bg-[#185FA5] text-slate-600 hover:text-white px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-wider flex-shrink-0 transition-all border border-slate-100"
>
{chip}
</button>
))}
</div>
<form onSubmit={handleFormSubmit} className="flex items-center gap-3">
<button
type="button"
onClick={() => setShowAttachmentModal(true)}
className="p-3 bg-slate-50 text-slate-400 hover:text-slate-600 rounded-xl transition-all border border-slate-100"
title="Attach Document Details"
>
<Paperclip className="w-5 h-5" />
</button>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder={t('type_message_placeholder', 'Type a compliant secure message...')}
className="flex-1 px-5 py-3.5 bg-slate-50 rounded-xl text-xs font-semibold outline-none border border-transparent focus:border-slate-200 transition-all"
/>
<button
type="submit"
disabled={!input.trim()}
className="bg-[#185FA5] hover:bg-[#144f8a] disabled:bg-slate-200 text-white p-3.5 rounded-xl transition-all shadow-xs flex items-center justify-center flex-shrink-0"
>
<Send className="w-5 h-5" />
</button>
</form>
</div>
</div>
{/* Candidate Context Sidebar (Right) */}
{showProfile && (
<aside className="hidden lg:flex w-72 2xl:w-80 border-l border-slate-200 flex-col flex-shrink-0 bg-white z-20 animate-in slide-in-from-right-10 duration-350 overflow-y-auto">
<div className="p-6 space-y-6">
<div className="text-center space-y-3 pb-4 border-b border-slate-100">
<div className="w-20 h-20 rounded-[30px] bg-gradient-to-br from-[#185FA5] to-blue-600 text-white flex items-center justify-center font-black text-2xl mx-auto shadow-md relative">
{conversation.worker_name.charAt(0)}
<span className="absolute -bottom-1 -right-1 bg-emerald-500 p-1 rounded-lg border-2 border-white shadow">
<ShieldCheck className="w-4 h-4 text-white" />
</span>
</div>
<div>
<h4 className="font-extrabold text-base text-slate-900 tracking-tight">{conversation.worker_name}</h4>
<span className="inline-block bg-slate-50 px-2 py-0.5 border border-slate-200 rounded text-[9px] uppercase font-black tracking-widest mt-1">
{conversation.category}
</span>
</div>
</div>
<div className="grid grid-cols-2 gap-2 text-center text-xs font-bold">
<div className="bg-slate-50/70 p-3 rounded-xl border border-slate-100">
<DollarSign className="w-3.5 h-3.5 text-emerald-600 mx-auto mb-1" />
<div className="text-[8px] font-black text-slate-400 uppercase tracking-widest">{t('expected', 'Expected')}</div>
<div className="text-slate-800 mt-0.5 truncate">{conversation.salary}</div>
</div>
<div className="bg-slate-50/70 p-3 rounded-xl border border-slate-100">
<MapPin className="w-3.5 h-3.5 text-blue-600 mx-auto mb-1" />
<div className="text-[8px] font-black text-slate-400 uppercase tracking-widest">{t('region', 'Region')}</div>
<div className="text-slate-800 mt-0.5 truncate">{conversation.nationality}</div>
</div>
</div>
<div className="space-y-3">
<h5 className="text-[9px] font-black text-slate-400 uppercase tracking-widest pl-1">{t('compliance_profile', 'Compliance Profile')}</h5>
<div className="space-y-2">
{[
{ label: t('visa_transfer', 'Visa Transfer'), value: t('transferable', 'Transferable'), icon: Briefcase, color: 'text-blue-500' },
{ label: t('medical_status', 'Medical Status'), value: t('cleared', 'Cleared'), icon: ShieldCheck, color: 'text-emerald-500' },
{ label: t('passport_ocr', 'Passport OCR'), value: t('verified', 'Verified'), icon: FileText, color: 'text-slate-500' }
].map((item, i) => (
<div key={i} className="flex items-center justify-between p-3 bg-slate-50/40 border border-slate-100 rounded-xl text-[10px] font-bold text-slate-600">
<div className="flex items-center space-x-2">
<item.icon className={`w-3.5 h-3.5 ${item.color}`} />
<span className="text-slate-500">{item.label}</span>
</div>
<span className="text-slate-900 font-extrabold">{item.value}</span>
</div>
))}
</div>
</div>
<div className="pt-2 space-y-2">
<Link
href={`/employer/workers/${conversation.worker_id || conversation.id}`}
className="w-full bg-[#185FA5] hover:bg-[#144f8a] text-white py-3 rounded-xl text-xs font-black uppercase tracking-widest flex items-center justify-center space-x-1.5 shadow-xs"
>
<Eye className="w-4 h-4" />
<span>{t('open_worker_profile', 'Open Worker Profile')}</span>
</Link>
{conversation.worker_status !== 'hired' && conversation.worker_status !== 'hidden' && (
<button
type="button"
onClick={() => {
router.post(`/employer/workers/${conversation.worker_id}/mark-hired`, {}, {
preserveScroll: true,
onSuccess: () => {
toast.success(t('marked_hired', 'Worker successfully marked as Hired!'));
router.reload();
}
});
}}
className="w-full bg-emerald-650 bg-emerald-600 hover:bg-emerald-700 text-white py-3 rounded-xl text-xs font-black uppercase tracking-widest flex items-center justify-center space-x-1.5 shadow-md border border-emerald-700"
>
<CheckCircle2 className="w-4.5 h-4.5 text-white" />
<span>{t('mark_hired_action', 'Mark Hired')}</span>
</button>
)}
</div>
</div>
</aside>
)}
</div>
{/* Interactive File Attachment Modal Placeholder */}
{showAttachmentModal && (
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 z-50 animate-fade-in">
<div className="bg-white rounded-3xl w-full max-w-sm border border-slate-200 shadow-2xl p-6 relative animate-zoom-in space-y-4">
<button
onClick={() => setShowAttachmentModal(false)}
className="absolute top-4 right-4 p-2 bg-slate-50 hover:bg-slate-100 rounded-full text-slate-400 hover:text-slate-600 transition-colors"
>
<X className="w-5 h-5" />
</button>
<h4 className="font-extrabold text-sm text-slate-900">{t('attach_sponsor_documents', 'Attach Verified Sponsor Documents')}</h4>
<p className="text-xs text-slate-500 font-medium leading-relaxed">
{t('attach_documents_desc', 'Upload sponsor papers, employment agreements, or work descriptions to review securely.')}
</p>
<div className="border-2 border-dashed border-slate-200 rounded-2xl p-6 text-center space-y-2 relative hover:border-[#185FA5] transition-all bg-slate-50/50">
<UploadCloud className="w-10 h-10 text-slate-355 mx-auto" />
<div className="text-xs font-bold text-slate-600">{t('select_files_drag', 'Select files or drag here')}</div>
<div className="text-[9px] text-slate-400">PDF, JPG, PNG (Max 5MB)</div>
<input
type="file"
onChange={handleFileUpload}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
</div>
<div className="flex justify-end pt-2 text-xs font-bold">
<button
type="button"
onClick={() => setShowAttachmentModal(false)}
className="px-4 py-2 border border-slate-200 hover:bg-slate-50 rounded-xl"
>
{t('close', 'Close')}
</button>
</div>
</div>
</div>
)}
</EmployerLayout>
);
}