651 lines
36 KiB
JavaScript
651 lines
36 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,
|
|
Mic,
|
|
Square,
|
|
Clock
|
|
} from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
|
|
export default function Show({ conversation, initialMessages, conversations = [], application = null }) {
|
|
const { t } = useTranslation();
|
|
const [messages, setMessages] = useState(initialMessages || []);
|
|
const [input, setInput] = useState('');
|
|
const [isTyping, setIsTyping] = useState(false);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [selectedApp, setSelectedApp] = useState(null);
|
|
const [newStatus, setNewStatus] = useState('');
|
|
const [notes, setNotes] = useState('');
|
|
const [showModal, setShowModal] = useState(false);
|
|
const [showAttachmentModal, setShowAttachmentModal] = useState(false);
|
|
const [uploadedFiles, setUploadedFiles] = useState([]);
|
|
const [isRecording, setIsRecording] = useState(false);
|
|
const mediaRecorderRef = useRef(null);
|
|
const audioChunksRef = useRef([]);
|
|
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 sendFile = (file) => {
|
|
if (!file) return;
|
|
|
|
const isImage = file.type.startsWith('image/');
|
|
const isAudio = file.type.startsWith('audio/');
|
|
const type = isImage ? 'image' : (isAudio ? 'voice' : 'document');
|
|
|
|
// Optimistically update message feed
|
|
const newMsg = {
|
|
id: Date.now(),
|
|
sender: 'employer',
|
|
text: file.name,
|
|
time: t('just_now', 'Just Now'),
|
|
read: false,
|
|
attachment_url: URL.createObjectURL(file),
|
|
attachment_type: type
|
|
};
|
|
setMessages(prev => [...prev, newMsg]);
|
|
|
|
router.post(`/employer/messages/${conversation.id}/send`, {
|
|
text: file.name,
|
|
file: file
|
|
}, {
|
|
preserveScroll: true,
|
|
onSuccess: () => {
|
|
toast.success(t('file_sent_success', 'File and notification sent successfully!'));
|
|
setShowAttachmentModal(false);
|
|
},
|
|
onError: (err) => {
|
|
toast.error(t('file_sent_error', 'Failed to upload file attachment.'));
|
|
}
|
|
});
|
|
};
|
|
|
|
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: () => {
|
|
router.reload({ preserveScroll: true });
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleFormSubmit = (e) => {
|
|
e.preventDefault();
|
|
sendMsg(input);
|
|
};
|
|
|
|
const handleFileUpload = (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
sendFile(file);
|
|
};
|
|
|
|
const startRecording = async () => {
|
|
try {
|
|
// Check browser permission status if query API is supported
|
|
if (navigator.permissions && navigator.permissions.query) {
|
|
try {
|
|
const permissionStatus = await navigator.permissions.query({ name: 'microphone' });
|
|
if (permissionStatus.state === 'denied') {
|
|
toast.error(t('microphone_blocked', 'Microphone access is blocked. Please enable microphone permissions in your browser settings.'));
|
|
return;
|
|
}
|
|
} catch (pe) {
|
|
// Ignore query check failures if name is not supported in some browsers
|
|
}
|
|
}
|
|
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
audioChunksRef.current = [];
|
|
const recorder = new MediaRecorder(stream);
|
|
|
|
recorder.ondataavailable = (e) => {
|
|
if (e.data.size > 0) {
|
|
audioChunksRef.current.push(e.data);
|
|
}
|
|
};
|
|
|
|
recorder.onstop = () => {
|
|
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' });
|
|
const file = new File([audioBlob], `voice_${Date.now()}.webm`, { type: 'audio/webm' });
|
|
sendFile(file);
|
|
// Stop all tracks to release mic
|
|
stream.getTracks().forEach(track => track.stop());
|
|
};
|
|
|
|
mediaRecorderRef.current = recorder;
|
|
recorder.start();
|
|
setIsRecording(true);
|
|
toast.info(t('recording_started', 'Recording voice message... Click microphone again to stop.'));
|
|
} catch (err) {
|
|
console.error(err);
|
|
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
|
|
toast.error(t('microphone_permission_denied', 'Permission to access the microphone was denied. Please allow microphone access in your browser settings.'));
|
|
} else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
|
|
toast.error(t('microphone_not_found', 'No microphone device was detected on your system.'));
|
|
} else {
|
|
toast.error(t('microphone_access_error', 'Could not access the microphone. Please check your system settings.'));
|
|
}
|
|
}
|
|
};
|
|
|
|
const openStatusModal = (app, statusPreset = '') => {
|
|
setSelectedApp(app);
|
|
let resolvedStatus = statusPreset || app.status || 'Applied';
|
|
const lowerStatus = resolvedStatus.toLowerCase();
|
|
if (lowerStatus === 'hire_requested' || lowerStatus === 'hired') {
|
|
resolvedStatus = 'Hired';
|
|
} else if (lowerStatus === 'rejected') {
|
|
resolvedStatus = 'Rejected';
|
|
} else if (lowerStatus === 'shortlisted') {
|
|
resolvedStatus = 'Shortlisted';
|
|
} else {
|
|
resolvedStatus = 'Applied';
|
|
}
|
|
setNewStatus(resolvedStatus);
|
|
setNotes(app.notes || '');
|
|
setShowModal(true);
|
|
};
|
|
|
|
const handleSaveStatus = () => {
|
|
if (!selectedApp) return;
|
|
|
|
router.post(`/employer/candidates/${selectedApp.application_id}/status`, {
|
|
status: newStatus,
|
|
notes: notes
|
|
}, {
|
|
preserveScroll: true,
|
|
onSuccess: () => {
|
|
toast.success(t('status_updated_success', 'Applicant status updated successfully.'));
|
|
setShowModal(false);
|
|
setSelectedApp(null);
|
|
},
|
|
onError: (errors) => {
|
|
toast.error(t('status_updated_error', 'Failed to update status: ' + (errors.error || errors.status || 'Error occurred')));
|
|
}
|
|
});
|
|
};
|
|
|
|
const stopRecording = () => {
|
|
if (mediaRecorderRef.current && isRecording) {
|
|
mediaRecorderRef.current.stop();
|
|
setIsRecording(false);
|
|
}
|
|
};
|
|
|
|
const chips = [
|
|
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">
|
|
<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>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
{application ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => openStatusModal(application)}
|
|
className="px-3.5 py-2 bg-emerald-600 text-white hover:bg-emerald-700 rounded-xl transition-all shadow-md flex items-center space-x-1.5 text-xs font-black uppercase tracking-wider border border-emerald-700 cursor-pointer"
|
|
>
|
|
<CheckCircle2 className="w-4 h-4 text-white" />
|
|
<span>{t('update_status', 'Update Status')}</span>
|
|
</button>
|
|
) : null}
|
|
</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';
|
|
const hasImage = msg.attachment_url && msg.attachment_type === 'image';
|
|
const hasVoice = msg.attachment_url && msg.attachment_type === 'voice';
|
|
const hasDoc = msg.attachment_url && msg.attachment_type === 'document';
|
|
const isImageFilename = msg.text && msg.text.match(/\.(jpg|jpeg|png|gif|webp)$/i);
|
|
|
|
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-[85%] sm:max-w-[70%]`}>
|
|
|
|
{/* Main Message Bubble (WhatsApp Style) */}
|
|
<div className={`rounded-2xl shadow-xs overflow-hidden border relative ${
|
|
isEmp
|
|
? 'bg-[#185FA5] text-white border-transparent rounded-tr-none'
|
|
: 'bg-white border-slate-100 text-slate-800 rounded-tl-none'
|
|
} ${hasImage ? 'p-1.5 pb-2.5 max-w-[320px]' : 'px-5 py-3'}`}>
|
|
|
|
{/* Image Attachment (WhatsApp Full Width style) */}
|
|
{hasImage && (
|
|
<div className="relative mb-2 rounded-xl overflow-hidden bg-black/5 aspect-video flex items-center justify-center">
|
|
<a href={msg.attachment_url} target="_blank" rel="noopener noreferrer" className="w-full h-full block">
|
|
<img
|
|
src={msg.attachment_url}
|
|
alt="Attachment"
|
|
className="w-full h-full object-cover hover:scale-[1.02] transition-transform duration-200"
|
|
/>
|
|
</a>
|
|
</div>
|
|
)}
|
|
|
|
{/* Voice Message */}
|
|
{hasVoice && (
|
|
<div className="mb-2 flex items-center bg-slate-100 p-2 rounded-xl border border-slate-200 min-w-[240px]">
|
|
<audio src={msg.attachment_url} controls className="w-full h-8 text-[#185FA5]" />
|
|
</div>
|
|
)}
|
|
|
|
{/* Document Attachment */}
|
|
{hasDoc && (
|
|
<div className="mb-2">
|
|
<a
|
|
href={msg.attachment_url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center space-x-2 bg-slate-50 hover:bg-slate-100 text-[#185FA5] px-3.5 py-2.5 rounded-xl border border-slate-200 transition-colors font-bold text-xs shadow-sm"
|
|
>
|
|
<FileText className="w-4 h-4" />
|
|
<span className="truncate max-w-[150px]">{msg.text || t('download_document', 'Download Document')}</span>
|
|
</a>
|
|
</div>
|
|
)}
|
|
|
|
{/* Text Content (if not just the filename of the image) */}
|
|
{msg.text && (!hasImage || !isImageFilename) && (
|
|
<div className={`text-xs font-semibold leading-relaxed ${hasImage ? 'px-2' : ''}`}>
|
|
{msg.text}
|
|
</div>
|
|
)}
|
|
|
|
{/* Time & Read Status Indicator */}
|
|
<div className={`flex items-center justify-end space-x-1 mt-1.5 text-[8px] font-black uppercase opacity-60 ${hasImage ? 'px-2' : ''}`}>
|
|
<span>{msg.time}</span>
|
|
{isEmp && (
|
|
<span className="flex items-center space-x-0.5 text-emerald-400">
|
|
<span>•</span>
|
|
<CheckCircle2 className="w-2.5 h-2.5" />
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
</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 cursor-pointer"
|
|
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="button"
|
|
onClick={isRecording ? stopRecording : startRecording}
|
|
className={`p-3.5 rounded-xl transition-all border cursor-pointer ${
|
|
isRecording
|
|
? 'bg-rose-50 border-rose-200 text-rose-600 animate-pulse'
|
|
: 'bg-slate-50 border-slate-100 text-slate-400 hover:text-[#185FA5]'
|
|
}`}
|
|
title={isRecording ? "Stop Recording" : "Record Voice Message"}
|
|
>
|
|
{isRecording ? <Square className="w-5 h-5" /> : <Mic className="w-5 h-5" />}
|
|
</button>
|
|
<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 cursor-pointer"
|
|
>
|
|
<Send className="w-5 h-5" />
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
</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>
|
|
)}
|
|
|
|
{/* Status Update Modal */}
|
|
{showModal && (
|
|
<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 w-full max-w-lg rounded-[28px] shadow-2xl border border-slate-200 overflow-hidden flex flex-col animate-in fade-in zoom-in-95 duration-200 text-left">
|
|
{/* Modal Header */}
|
|
<div className="p-6 border-b border-slate-100 flex items-center justify-between">
|
|
<div>
|
|
<h3 className="font-black text-slate-900 text-base">Update Application Status</h3>
|
|
<p className="text-[10px] font-bold text-[#185FA5] uppercase tracking-widest mt-0.5">{selectedApp?.name}</p>
|
|
</div>
|
|
<button
|
|
onClick={() => { setShowModal(false); setSelectedApp(null); }}
|
|
className="p-2 hover:bg-slate-50 text-slate-400 hover:text-slate-950 rounded-full transition-colors cursor-pointer"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Modal Body */}
|
|
<div className="p-6 space-y-5">
|
|
{selectedApp?.status?.toLowerCase() === 'hire_requested' && (
|
|
<div className="p-3 bg-amber-50 rounded-xl border border-amber-100 text-[10px] text-amber-700 font-bold">
|
|
A hiring offer is pending. Waiting for worker confirmation.
|
|
</div>
|
|
)}
|
|
{/* Status Selector */}
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Select Stage</label>
|
|
<select
|
|
value={newStatus}
|
|
onChange={(e) => setNewStatus(e.target.value)}
|
|
className="w-full px-4 py-3 bg-slate-50 border border-slate-100 rounded-xl text-xs font-bold text-slate-900 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all cursor-pointer"
|
|
>
|
|
<option value="Applied">Review</option>
|
|
<option value="Shortlisted">Shortlisted</option>
|
|
<option value="Rejected">Rejected</option>
|
|
<option value="Hired">Hired</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Notes Textarea */}
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Internal Sponsor Notes</label>
|
|
<textarea
|
|
value={notes}
|
|
onChange={(e) => setNotes(e.target.value)}
|
|
placeholder="Add any internal feedback, interview times, or notes about this candidate..."
|
|
rows={4}
|
|
className="w-full px-4 py-3 bg-slate-50 border border-slate-100 rounded-xl text-xs font-medium text-slate-900 placeholder-slate-400 focus:ring-4 focus:ring-blue-500/10 focus:border-[#185FA5] outline-none transition-all resize-none"
|
|
/>
|
|
</div>
|
|
|
|
{/* Status History Audit Trail */}
|
|
{selectedApp?.status_history && selectedApp.status_history.length > 0 && (
|
|
<div className="space-y-2 pt-2">
|
|
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Status Audit Log</label>
|
|
<div className="max-h-24 overflow-y-auto space-y-2 pr-1 border border-slate-100 rounded-xl p-2 bg-slate-50/40">
|
|
{selectedApp.status_history.map((hist, index) => (
|
|
<div key={index} className="text-[9px] text-slate-500 flex justify-between items-start">
|
|
<div>
|
|
<span className="font-bold uppercase text-[#185FA5]">{hist.status}</span>
|
|
{hist.notes && <span className="ml-1 text-slate-400 italic">({hist.notes})</span>}
|
|
</div>
|
|
<span className="text-[8px] font-mono text-slate-300">{new Date(hist.timestamp).toLocaleDateString()}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Modal Footer */}
|
|
<div className="p-6 bg-slate-50 border-t border-slate-100 flex items-center justify-end space-x-3">
|
|
<button
|
|
onClick={() => { setShowModal(false); setSelectedApp(null); }}
|
|
className="px-5 py-2.5 bg-white border border-slate-200 hover:bg-slate-100 rounded-xl text-[10px] font-black uppercase tracking-widest text-slate-600 transition-colors cursor-pointer"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleSaveStatus}
|
|
className="px-5 py-2.5 bg-[#185FA5] hover:bg-[#144f8a] text-white rounded-xl text-[10px] font-black uppercase tracking-widest transition-all shadow-md shadow-blue-500/10 cursor-pointer"
|
|
>
|
|
Save Status
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</EmployerLayout>
|
|
);
|
|
}
|