Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect, useRef } from 'react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { supabase } from '../../supabaseClient'; | |
| import FullProfileOverlay from '../FullProfileOverlay'; | |
| import ScheduleInterviewModal from '../ScheduleInterviewModal'; | |
| // --- ICONS --- | |
| const SmallCalendarIcon = () => (<svg style={{ width: '24px', height: '24px', color: 'rgba(255,255,255,0.7)' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clipRule="evenodd" /></svg>); | |
| const ChevronRightIcon = () => (<svg style={{ width: '16px', height: '16px', marginLeft: '4px' }} viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" /></svg>); | |
| const CloseIcon = () => <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>; | |
| const SendIcon = () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="22" y1="2" x2="11" y2="13" /><polygon points="22 2 15 22 11 13 2 9 22 2" /></svg>; | |
| // --- NEW FULL CHAT UI MODAL --- | |
| const AdminChatModal = ({ isOpen, onClose, applicant }) => { | |
| const [messages, setMessages] = useState([]); | |
| const [text, setText] = useState(''); | |
| const [loading, setLoading] = useState(true); | |
| const [adminId, setAdminId] = useState(null); | |
| const messagesEndRef = useRef(null); | |
| // Fetch chat history | |
| useEffect(() => { | |
| if (!isOpen || !applicant?.userId) return; | |
| const fetchMessages = async () => { | |
| setLoading(true); | |
| try { | |
| const { data: { user } } = await supabase.auth.getUser(); | |
| if (!user) return; | |
| setAdminId(user.id); | |
| // Fetch conversation strictly between this Admin and this Applicant | |
| const { data, error } = await supabase | |
| .from('messages') | |
| .select('*') | |
| .or(`and(sender_id.eq.${user.id},receiver_id.eq.${applicant.userId}),and(sender_id.eq.${applicant.userId},receiver_id.eq.${user.id})`) | |
| .order('created_at', { ascending: true }); | |
| if (!error && data) { | |
| setMessages(data); | |
| } | |
| } catch (err) { | |
| console.error("Error fetching chat:", err); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| fetchMessages(); | |
| }, [isOpen, applicant]); | |
| // Auto-scroll to bottom of chat | |
| useEffect(() => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); | |
| }, [messages]); | |
| const sendMsg = async () => { | |
| if (!text.trim() || !adminId || !applicant?.userId) return; | |
| const newMsg = { | |
| id: Date.now(), // Temporary ID for optimistic UI update | |
| sender_id: adminId, | |
| receiver_id: applicant.userId, | |
| content: text.trim(), | |
| created_at: new Date().toISOString() | |
| }; | |
| // Update UI instantly | |
| setMessages(prev => [...prev, newMsg]); | |
| setText(''); | |
| // Send to database | |
| const { error } = await supabase.from('messages').insert([{ | |
| sender_id: adminId, | |
| receiver_id: applicant.userId, | |
| content: newMsg.content | |
| }]); | |
| if (error) console.error("Error sending message:", error); | |
| }; | |
| const unsendMsg = async (msgId) => { | |
| if (!window.confirm("Unsend this message?")) return; | |
| // 1. Try to delete the message | |
| const { data, error } = await supabase.from('messages').delete().eq('id', msgId).select(); | |
| if (error) { | |
| console.error("Unsend Error:", error); | |
| alert("Failed to unsend message."); | |
| return; | |
| } | |
| // 2. If RLS silently blocks deletion, fallback to update | |
| if (!data || data.length === 0) { | |
| const { error: updateError } = await supabase | |
| .from('messages') | |
| .update({ content: "🚫 This message was unsent" }) | |
| .eq('id', msgId); | |
| if (updateError) { | |
| alert("Database policies prevent unsending this message."); | |
| return; | |
| } | |
| setMessages(prev => prev.map(m => m.id === msgId ? { ...m, content: "🚫 This message was unsent" } : m)); | |
| } else { | |
| setMessages(prev => prev.filter(m => m.id !== msgId)); | |
| } | |
| }; | |
| const canUnsend = (rawTime) => { | |
| if (!rawTime) return false; | |
| const msgTime = new Date(rawTime).getTime(); | |
| return (Date.now() - msgTime) < 5 * 60 * 1000; // 5 minutes | |
| }; | |
| if (!isOpen || !applicant) return null; | |
| return ( | |
| <div style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.7)', backdropFilter: 'blur(5px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 100 }}> | |
| <style>{`.chat-scroll::-webkit-scrollbar { width: 6px; } .chat-scroll::-webkit-scrollbar-track { background: transparent; } .chat-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }`}</style> | |
| <motion.div | |
| initial={{ opacity: 0, scale: 0.95, y: 20 }} | |
| animate={{ opacity: 1, scale: 1, y: 0 }} | |
| exit={{ opacity: 0, scale: 0.95, y: 20 }} | |
| style={{ | |
| width: '100%', maxWidth: '600px', height: '75vh', | |
| background: 'rgba(15, 23, 42, 0.95)', border: '1px solid rgba(255,255,255,0.1)', | |
| borderRadius: '1.25rem', display: 'flex', flexDirection: 'column', | |
| overflow: 'hidden', boxShadow: '0 25px 50px rgba(0,0,0,0.5)' | |
| }} | |
| > | |
| {/* Chat Header */} | |
| <div style={{ padding: '1.25rem', borderBottom: '1px solid rgba(255,255,255,0.08)', background: 'rgba(0,0,0,0.2)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}> | |
| <img src={applicant.avatar || `https://ui-avatars.com/api/?name=${encodeURIComponent(applicant.name)}&background=random`} alt={applicant.name} style={{ width: '40px', height: '40px', borderRadius: '50%', objectFit: 'cover', border: '2px solid rgba(255,255,255,0.1)' }} /> | |
| <div> | |
| <h3 style={{ margin: 0, color: 'white', fontSize: '1.1rem', fontWeight: 'bold' }}>{applicant.name}</h3> | |
| <p style={{ margin: 0, fontSize: '0.8rem', color: '#94a3b8' }}>{applicant.role}</p> | |
| </div> | |
| </div> | |
| <button onClick={onClose} style={{ background: 'rgba(255,255,255,0.05)', border: 'none', color: '#94a3b8', cursor: 'pointer', padding: '0.5rem', borderRadius: '50%', display: 'flex' }}> | |
| <CloseIcon /> | |
| </button> | |
| </div> | |
| {/* Chat History */} | |
| <div className="chat-scroll" style={{ flex: 1, padding: '1.5rem', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '1rem' }}> | |
| {loading ? ( | |
| <div style={{ textAlign: 'center', color: '#64748b', marginTop: '2rem' }}>Loading conversation...</div> | |
| ) : messages.length === 0 ? ( | |
| <div style={{ textAlign: 'center', color: '#64748b', marginTop: '2rem', fontSize: '0.9rem' }}> | |
| No previous messages. Start the conversation! | |
| </div> | |
| ) : ( | |
| messages.map(m => { | |
| const isMe = m.sender_id === adminId; | |
| return ( | |
| <motion.div key={m.id} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} style={{ alignSelf: isMe ? 'flex-end' : 'flex-start', maxWidth: '75%', position: 'relative' }}> | |
| <div style={{ | |
| // Use Admin's Red Theme for outgoing bubbles | |
| background: isMe ? 'linear-gradient(135deg, #EF4444 0%, #DC2626 100%)' : 'rgba(255,255,255,0.08)', | |
| color: 'white', padding: '0.85rem 1.25rem', borderRadius: '1.25rem', | |
| borderBottomRightRadius: isMe ? 4 : '1.25rem', borderBottomLeftRadius: isMe ? '1.25rem' : 4, | |
| fontSize: '0.95rem', lineHeight: '1.5', | |
| boxShadow: isMe ? '0 4px 15px rgba(239, 68, 68, 0.3)' : 'none' | |
| }}> | |
| {m.content} | |
| </div> | |
| <div style={{ display: 'flex', justifyContent: isMe ? 'flex-end' : 'flex-start', alignItems: 'center', gap: '8px', marginTop: '4px' }}> | |
| <div style={{ fontSize: '0.7rem', color: '#64748b', padding: '0 0.5rem' }}> | |
| {new Date(m.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} | |
| </div> | |
| {isMe && canUnsend(m.created_at) && ( | |
| <button onClick={() => unsendMsg(m.id)} title="Unsend message (5 min window)" style={{ background: 'transparent', border: 'none', color: '#ef4444', cursor: 'pointer', padding: '0 2px', display: 'flex', alignItems: 'center' }}> | |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg> | |
| </button> | |
| )} | |
| </div> | |
| </motion.div> | |
| ); | |
| }) | |
| )} | |
| <div ref={messagesEndRef} /> | |
| </div> | |
| {/* Input Area */} | |
| <div style={{ padding: '1.25rem', background: 'rgba(0,0,0,0.3)', borderTop: '1px solid rgba(255,255,255,0.05)' }}> | |
| <div style={{ display: 'flex', gap: '0.75rem', background: 'rgba(255,255,255,0.03)', borderRadius: '1rem', padding: '0.5rem', border: '1px solid rgba(255,255,255,0.1)' }}> | |
| <textarea | |
| value={text} | |
| onChange={e => setText(e.target.value)} | |
| onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMsg(); } }} | |
| placeholder="Type a message..." | |
| style={{ flex: 1, padding: '0.75rem', border: 'none', background: 'transparent', color: 'white', outline: 'none', resize: 'none', height: '45px', fontFamily: 'inherit' }} | |
| /> | |
| <button | |
| onClick={sendMsg} | |
| disabled={!text.trim()} | |
| style={{ | |
| background: text.trim() ? '#EF4444' : 'rgba(255,255,255,0.05)', | |
| color: text.trim() ? 'white' : '#64748b', border: 'none', borderRadius: '50%', | |
| width: '45px', height: '45px', cursor: text.trim() ? 'pointer' : 'not-allowed', | |
| display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, | |
| transition: 'all 0.2s ease' | |
| }} | |
| > | |
| <SendIcon /> | |
| </button> | |
| </div> | |
| </div> | |
| </motion.div> | |
| </div> | |
| ); | |
| }; | |
| // --- MAIN COMPONENT --- | |
| export default function AdminInterviewManagement() { | |
| const [activeSubTab, setActiveSubTab] = useState('interviews'); | |
| const [loading, setLoading] = useState(true); | |
| const [applicants, setApplicants] = useState({ interviews: [], accepted: [], rejected: [] }); | |
| // Modals State | |
| const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false); | |
| const [isMessageModalOpen, setIsMessageModalOpen] = useState(false); | |
| const [isDrawerOpen, setIsDrawerOpen] = useState(false); | |
| const [selectedApplicant, setSelectedApplicant] = useState(null); | |
| const [drawerCandidate, setDrawerCandidate] = useState(null); | |
| // ✅ FIXED: Marked this function as 'async' to resolve the Syntax Error | |
| const fetchData = async () => { | |
| try { | |
| setLoading(true); | |
| // Join applications with profiles, jobs, and CHECK interviews table | |
| const { data, error } = await supabase | |
| .from('applications') | |
| .select(` | |
| id, user_id, job_id, created_at, status, experience, skills, match_score, resume_url, | |
| profiles ( id, full_name, email, avatar_url, phone, location, summary, headline, current_position, education, work_experience, experience_years, projects, skills, technical_skills, resume_url ), | |
| jobs ( id, title ), | |
| interviews ( id, date, time, status, created_at ) | |
| `) | |
| .order('created_at', { ascending: false }); | |
| if (error) throw error; | |
| const categorized = { interviews: [], accepted: [], rejected: [] }; | |
| data.forEach(app => { | |
| // Check if an interview row exists for this application (Get the LATEST one) | |
| const interviews = app.interviews || []; | |
| interviews.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); | |
| const interviewData = interviews.length > 0 ? interviews[0] : null; | |
| // Combine skills and technical_skills | |
| const profileSkills = Array.isArray(app.profiles?.skills) ? app.profiles.skills : []; | |
| const profileTechSkills = Array.isArray(app.profiles?.technical_skills) ? app.profiles.technical_skills : []; | |
| const parsedTechSkills = (typeof app.profiles?.technical_skills === 'string') | |
| ? app.profiles.technical_skills.split(',').map(s => s.trim()) | |
| : profileTechSkills; | |
| const combinedSkills = [...new Set([...profileSkills, ...parsedTechSkills])]; | |
| const finalSkills = combinedSkills.length > 0 | |
| ? combinedSkills | |
| : (Array.isArray(app.skills) ? app.skills : (app.skills ? [app.skills] : [])); | |
| const formattedApp = { | |
| ...app, | |
| full_name: app.profiles?.full_name, | |
| email: app.profiles?.email, | |
| phone: app.profiles?.phone, | |
| location: app.profiles?.location, | |
| summary: app.profiles?.summary, | |
| headline: app.profiles?.headline, | |
| current_position: app.profiles?.current_position, | |
| education: app.profiles?.education, | |
| work_experience: app.profiles?.work_experience, | |
| projects: app.profiles?.projects, | |
| name: app.profiles?.full_name || 'Unknown User', | |
| role: app.jobs?.title || 'Unknown Role', | |
| avatar: app.profiles?.avatar_url, | |
| resumeUrl: app.resume_url || app.profiles?.resume_url, | |
| userId: app.profiles?.id || app.user_id, | |
| jobId: app.job_id, | |
| experience: (app.experience === '0' || app.experience === 0) | |
| ? 'Fresher' | |
| : (app.experience ? `${app.experience} years` : 'N/A'), | |
| skills: finalSkills, | |
| interviewId: interviewData?.id, | |
| date: interviewData ? interviewData.date : 'Not Scheduled', | |
| time: interviewData ? interviewData.time : '', | |
| }; | |
| // --- SORTING LOGIC --- | |
| if (interviewData) { | |
| categorized.interviews.push(formattedApp); | |
| } else if (app.status === 'Accepted' || app.status === 'Approved') { | |
| categorized.accepted.push(formattedApp); | |
| } else if (app.status === 'Rejected') { | |
| categorized.rejected.push(formattedApp); | |
| } | |
| }); | |
| setApplicants(categorized); | |
| } catch (error) { | |
| console.error('Error fetching data:', error); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| // 1. Fetch Data on Component Mount | |
| useEffect(() => { | |
| fetchData(); | |
| }, []); | |
| // 2. Updated Schedule Handler | |
| // ✅ Helper: Send message to candidate | |
| const sendMessageToCandidate = async (candidateUserId, message) => { | |
| try { | |
| const { data: { user: adminUser } } = await supabase.auth.getUser(); | |
| if (!adminUser) return; | |
| const { error } = await supabase.from('messages').insert([{ | |
| sender_id: adminUser.id, | |
| receiver_id: candidateUserId, | |
| content: message, | |
| is_read: false | |
| }]); | |
| if (error) console.error('Error sending message:', error); | |
| } catch (err) { | |
| console.error('Failed to send message:', err); | |
| } | |
| }; | |
| const handleScheduleConfirm = async (scheduleData) => { | |
| if (!selectedApplicant) return; | |
| const { date, time, interviewType, mode, details, interviewerName, interviewerRole } = scheduleData; | |
| try { | |
| const scheduledTimeISO = new Date(`${date}T${time}:00`).toISOString(); | |
| const interviewPayload = { | |
| application_id: selectedApplicant.id, | |
| scheduled_time: scheduledTimeISO, | |
| date, time, | |
| status: 'Scheduled', | |
| interview_type: interviewType, | |
| mode: mode, | |
| meeting_link: mode === 'Online' ? details : null, | |
| location: mode === 'Offline' ? details : null, | |
| interviewer_name: interviewerName, | |
| interviewer_role: interviewerRole, | |
| duration_mins: 45 | |
| }; | |
| let dbError; | |
| if (selectedApplicant.interviewId) { | |
| const { error } = await supabase | |
| .from('interviews') | |
| .update(interviewPayload) | |
| .eq('id', selectedApplicant.interviewId); | |
| dbError = error; | |
| } else { | |
| const { error } = await supabase | |
| .from('interviews') | |
| .insert([interviewPayload]); | |
| dbError = error; | |
| } | |
| if (dbError) throw dbError; | |
| // ✅ Send interview scheduled message to candidate | |
| const interviewDatesTime = `${date} at ${time}`; | |
| const modeInfo = mode === 'Online' ? `via ${details}` : `at ${details}`; | |
| const jobContext = selectedApplicant.role ? ` for ${selectedApplicant.role}` : ''; | |
| await sendMessageToCandidate( | |
| selectedApplicant.userId, | |
| `📅 Great news! Your interview${jobContext} has been scheduled for ${interviewDatesTime} (${interviewType}) ${modeInfo}. Interviewer: ${interviewerName} (${interviewerRole}). Please confirm your availability.` | |
| ); | |
| if (selectedApplicant.email) { | |
| await supabase.functions.invoke('send-interview-email', { | |
| body: { | |
| candidateName: selectedApplicant.name, | |
| candidateEmail: selectedApplicant.email, | |
| role: selectedApplicant.role, | |
| date, time, mode, details | |
| } | |
| }); | |
| } | |
| alert("Interview Scheduled Successfully and candidate notified!"); | |
| setIsScheduleModalOpen(false); | |
| fetchData(); | |
| } catch (error) { | |
| console.error("Error scheduling:", error); | |
| alert(`Failed to schedule: ${error.message}`); | |
| } | |
| }; | |
| const openScheduleModal = (applicant) => { setSelectedApplicant(applicant); setIsScheduleModalOpen(true); }; | |
| const openMessageModal = (applicant) => { setSelectedApplicant(applicant); setIsMessageModalOpen(true); }; | |
| const openDrawer = (applicant) => { setDrawerCandidate(applicant); setIsDrawerOpen(true); }; | |
| const primaryButtonStyle = { backgroundColor: '#EF4444', color: 'white', padding: '0.5rem 1rem', borderRadius: '9999px', fontWeight: '500', cursor: 'pointer', border: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%' }; | |
| const secondaryButtonStyle = { backgroundColor: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.2)', color: 'white', padding: '0.5rem 1rem', borderRadius: '9999px', fontWeight: '500', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%' }; | |
| return ( | |
| <div> | |
| <header style={{ marginBottom: '2rem' }}><h1 style={{ fontSize: '1.875rem', fontWeight: 'bold' }}>Interview Management</h1></header> | |
| {/* Tabs */} | |
| <div style={{ display: 'flex', justifyContent: 'center', width: '100%', marginBottom: '2rem' }}> | |
| <nav style={{ position: 'relative', display: 'inline-flex', gap: '1rem', backgroundColor: 'rgba(255, 255, 255, 0.1)', borderRadius: '1rem', padding: '0.5rem' }}> | |
| {['interviews', 'accepted', 'rejected'].map(key => ( | |
| <div key={key} onClick={() => setActiveSubTab(key)} style={{ padding: '0.75rem 1.5rem', borderRadius: '0.5rem', cursor: 'pointer', color: key === activeSubTab ? '#F87171' : '#d1d5db', fontWeight: key === activeSubTab ? 'bold' : 'normal', backgroundColor: key === activeSubTab ? 'rgba(239, 68, 68, 0.2)' : 'transparent' }}> | |
| {key.charAt(0).toUpperCase() + key.slice(1)} ({applicants[key]?.length || 0}) | |
| </div> | |
| ))} | |
| </nav> | |
| </div> | |
| <AnimatePresence mode="wait"> | |
| <motion.div key={activeSubTab} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }}> | |
| {loading ? (<div style={{ textAlign: 'center', color: '#888', padding: '2rem' }}>Loading...</div>) : applicants[activeSubTab].length === 0 ? (<div style={{ textAlign: 'center', color: '#666', padding: '2rem' }}>No candidates found.</div>) : ( | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '1.5rem' }}> | |
| {applicants[activeSubTab].map((applicant, index) => ( | |
| <div key={applicant.id || index} style={{ backgroundColor: 'rgba(239, 68, 68, 0.05)', border: '1px solid rgba(239, 68, 68, 0.2)', borderRadius: '1rem', padding: '1.5rem', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: '1rem' }}> | |
| {/* INFO SECTION */} | |
| <div style={{ display: 'flex', alignItems: 'flex-start', gap: '1rem' }}> | |
| <img src={applicant.avatar || `https://ui-avatars.com/api/?name=${encodeURIComponent(applicant.name)}&background=random`} alt={applicant.name} style={{ width: '48px', height: '48px', borderRadius: '50%', objectFit: 'cover' }} /> | |
| <div> | |
| <h3 style={{ fontSize: '1.25rem', fontWeight: 'bold' }}>{applicant.name}</h3> | |
| <p style={{ color: '#d1d5db', fontSize: '0.9rem' }}>{applicant.role} • {applicant.experience}</p> | |
| {activeSubTab === 'interviews' && applicant.time && ( | |
| <div style={{ marginTop: '12px', fontSize: '0.85rem', color: '#9ca3af', display: 'flex', alignItems: 'center', gap: '6px' }}> | |
| <SmallCalendarIcon /> <span>Interview: <span style={{ color: 'white' }}>{applicant.date} at {applicant.time}</span></span> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* BUTTONS SECTION */} | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', minWidth: '170px' }}> | |
| {activeSubTab === 'interviews' && (<> | |
| <motion.button onClick={() => openScheduleModal(applicant)} whileHover={{ scale: 1.02 }} style={secondaryButtonStyle}>Reschedule</motion.button> | |
| <motion.button onClick={() => openMessageModal(applicant)} whileHover={{ scale: 1.02 }} style={secondaryButtonStyle}>Message</motion.button> | |
| <motion.button onClick={() => openDrawer(applicant)} whileHover={{ scale: 1.02 }} style={primaryButtonStyle}>View CV <ChevronRightIcon /></motion.button> | |
| </>)} | |
| {activeSubTab === 'accepted' && (<> | |
| <motion.button onClick={() => openScheduleModal(applicant)} whileHover={{ scale: 1.02 }} style={secondaryButtonStyle}>Schedule Interview</motion.button> | |
| <motion.button onClick={() => openMessageModal(applicant)} whileHover={{ scale: 1.02 }} style={secondaryButtonStyle}>Send Message</motion.button> | |
| <motion.button onClick={() => openDrawer(applicant)} whileHover={{ scale: 1.02 }} style={primaryButtonStyle}>View CV <ChevronRightIcon /></motion.button> | |
| </>)} | |
| {activeSubTab === 'rejected' && (<> | |
| <motion.button onClick={() => openMessageModal(applicant)} whileHover={{ scale: 1.02 }} style={secondaryButtonStyle}>Send Message</motion.button> | |
| <motion.button onClick={() => openDrawer(applicant)} whileHover={{ scale: 1.02 }} style={primaryButtonStyle}>View CV <ChevronRightIcon /></motion.button> | |
| </>)} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </motion.div> | |
| </AnimatePresence> | |
| {/* --- MODALS --- */} | |
| <AnimatePresence> | |
| {isScheduleModalOpen && ( | |
| <ScheduleInterviewModal | |
| isOpen={isScheduleModalOpen} | |
| onClose={() => setIsScheduleModalOpen(false)} | |
| onConfirm={handleScheduleConfirm} | |
| candidateName={selectedApplicant?.name} | |
| /> | |
| )} | |
| </AnimatePresence> | |
| <AnimatePresence> | |
| {isMessageModalOpen && ( | |
| <AdminChatModal | |
| isOpen={isMessageModalOpen} | |
| onClose={() => setIsMessageModalOpen(false)} | |
| applicant={selectedApplicant} | |
| /> | |
| )} | |
| </AnimatePresence> | |
| <AnimatePresence> | |
| {isDrawerOpen && ( | |
| <FullProfileOverlay | |
| candidate={drawerCandidate} | |
| onClose={() => setIsDrawerOpen(false)} | |
| /> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| ); | |
| } |