import React, { useState, useMemo, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { supabase } from '../../supabaseClient'; import CandidateDrawer from '../CandidateDrawer'; // ✅ IMPORT ICONS FROM YOUR SEPARATE FILE import { FilterIcon, ScoringIcon, ClearIcon, ViewIcon, ChevronDownIcon, SearchIcon, ChevronLeftIcon, ChevronRightIcon, CheckSquareIcon, MailIcon, LoaderIcon, RoundCheckbox } from '../../components/Icons'; // --- REUSABLE BUTTON COMPONENT --- const BulkActionButton = ({ Icon, label, color, onClick }) => { const [hover, setHover] = useState(false); return ( setHover(true)} onMouseLeave={() => setHover(false)} layout style={{ display: 'flex', alignItems: 'center', backgroundColor: hover ? color : 'rgba(255,255,255,0.05)', border: `1px solid ${hover ? color : 'rgba(255,255,255,0.2)'}`, borderRadius: '20px', padding: '0.5rem', height: '40px', minWidth: '40px', cursor: 'pointer', color: hover ? 'white' : '#94a3b8', boxShadow: hover ? `0 0 15px ${color}66` : 'none', justifyContent: 'center', outline: 'none' }} transition={{ type: 'spring', stiffness: 500, damping: 30 }} > {hover && ( {label} )} ); }; // --- FILTER PANEL COMPONENT --- const FilterPanel = ({ filters, setFilters, jobOptions = [] }) => { const languages = ["JavaScript", "TypeScript", "Python", "Java", "C++", "C#", "React", "Go", "Rust", "Swift", "Kotlin", "PHP"]; const toggleItem = (category, item) => { const current = filters[category] || []; const updated = current.includes(item) ? current.filter(i => i !== item) : [...current, item]; setFilters({ ...filters, [category]: updated }); }; return (
{/* Status & Score Group */}

Status

{['All', 'Pending', 'Accepted', 'Rejected'].map(status => ( ))}

Min. Match Score

setFilters({ ...filters, minScore: parseInt(e.target.value) })} style={{ width: '100%', accentColor: '#EF4444', height: '4px', background: 'rgba(255,255,255,0.1)', borderRadius: '2px' }} /> {filters.minScore}%
{/* List Group */}

Languages

{languages.map(lang => ( ))}

Job Positions

{jobOptions.length > 0 ? ( jobOptions.map(pos => ( )) ) : ( No applicants found. )}
); }; // --- SCORING PANEL COMPONENT --- const ScoringPanel = ({ config, setConfig, onReset, onClose }) => { const handleChange = (key, value) => setConfig({ ...config, [key]: parseInt(value) }); // Internal slider for this component const ConfigSlider = ({ label, value, min, max, onChangeKey }) => (
{label} {value}
handleChange(onChangeKey, e.target.value)} style={{ width: '100%', accentColor: '#EF4444', height: '4px', background: 'rgba(255,255,255,0.1)', borderRadius: '2px' }} />
); return (
); }; // --- MAIN PAGE COMPONENT --- export default function AdminSortingPage() { const [applicants, setApplicants] = useState([]); const [isLoading, setIsLoading] = useState(true); const [openPanel, setOpenPanel] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [drawerCandidate, setDrawerCandidate] = useState(null); // Pagination & Selection const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 5; const [selectedIds, setSelectedIds] = useState([]); // Filters const initialFilters = { sortBy: 'Match Score', status: 'All', minScore: 0, languages: [], positions: [] }; const [filters, setFilters] = useState(initialFilters); const [availableJobs, setAvailableJobs] = useState([]); // Scoring const defaultScoring = { skillsWeight: 2, experienceWeight: 5, certBonus: 3, projectsWeight: 3 }; const [scoringConfig, setScoringConfig] = useState(defaultScoring); //candidate overview const handleViewCandidate = (candidate) => { setDrawerCandidate(candidate); setIsDrawerOpen(true); }; // --- DATA FETCHING (Supabase) --- useEffect(() => { const fetchApplicants = async () => { setIsLoading(true); try { const { data, error } = await supabase .from('applications') .select(` id, resume_url, created_at, status, match_score, skills, skills_match, technical_skills_match, work_experience_match, education_match, certifications_match, project_match, profiles ( id, full_name, email, avatar_url, experience_years, languages, resume_url ), jobs ( id, title ) `); if (error) throw error; const formattedData = data.map(app => ({ id: app.id, userId: app.profiles?.id, jobId: app.jobs?.id, name: app.profiles?.full_name || 'Unknown Candidate', email: app.profiles?.email || 'No Email', img: app.profiles?.avatar_url || `https://ui-avatars.com/api/?name=${encodeURIComponent(app.profiles?.full_name || 'User')}&background=random`, jobTitle: app.jobs?.title || app.profiles?.job_title || 'Applicant', experience: parseInt(app.profiles?.experience_years) || 0, skills: app.skills || [], status: app.status, resumeUrl: app.resume_url || app.profiles?.resume_url, dbScore: app.match_score || 0, scores: { skills: app.skills_match || 0, technical: app.technical_skills_match || 0, experience: app.work_experience_match || 0, certifications: app.certifications_match || 0, languages: 0, education: app.education_match || 0, projects: app.project_match || 0 } })); setApplicants(formattedData); // Extract unique job titles const uniqueTitles = [...new Set(formattedData.map(app => app.jobTitle).filter(Boolean))]; setAvailableJobs(uniqueTitles); } catch (error) { console.error('Error fetching applicants:', error.message); } finally { setIsLoading(false); } }; fetchApplicants(); }, []); // --- 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); } }; // --- BULK ACTIONS --- const handleBulkReject = async () => { if (!confirm(`Are you sure you want to REJECT ${selectedIds.length} candidates?`)) return; try { const { error } = await supabase .from('applications') .update({ status: 'Rejected' }) .in('id', selectedIds); if (error) throw error; // ✅ Send rejection message to each candidate const rejectedApplicants = applicants.filter(a => selectedIds.includes(a.id)); for (const applicant of rejectedApplicants) { const jobContext = applicant.jobTitle ? ` for ${applicant.jobTitle}` : ''; await sendMessageToCandidate( applicant.userId, `📧 We regret to inform you that your application${jobContext} has been rejected. We appreciate your interest and wish you the best of luck in your career. Feel free to apply again in the future!` ); } // Update UI instantly setApplicants(prev => prev.map(app => selectedIds.includes(app.id) ? { ...app, status: 'Rejected' } : app )); setSelectedIds([]); alert('Candidates Rejected and notified.'); } catch (error) { console.error('Error rejecting:', error.message); alert('Failed to reject.'); } }; const handleBulkApprove = async () => { if (!confirm(`Are you sure you want to approve ${selectedIds.length} candidates?`)) return; try { const { error } = await supabase .from('applications') .update({ status: 'Accepted' }) .in('id', selectedIds); if (error) throw error; // ✅ Send acceptance message to each candidate const approvedApplicants = applicants.filter(a => selectedIds.includes(a.id)); for (const applicant of approvedApplicants) { const jobContext = applicant.jobTitle ? ` for ${applicant.jobTitle}` : ''; await sendMessageToCandidate( applicant.userId, `🎉 Congratulations! Your application${jobContext} has been accepted. We are excited about the possibility of working with you. Our team will be in touch soon to schedule an interview.` ); } setApplicants(prev => prev.map(app => selectedIds.includes(app.id) ? { ...app, status: 'Accepted' } : app )); setSelectedIds([]); alert('Approved Successfully and candidates notified!'); } catch (error) { console.error('Error approving:', error.message); alert('Failed to update.'); } }; const handleBulkEmail = () => { const emails = applicants.filter(a => selectedIds.includes(a.id)).map(a => a.email).join(','); window.location.href = `mailto:?bcc=${emails}&subject=Interview Update`; }; // --- SORTING & FILTERING --- const filteredApplicants = useMemo(() => { return applicants.map(app => { // Dynamic Score Calculation const { skillsWeight, experienceWeight, certBonus, projectsWeight } = scoringConfig; // Using a weighted average formula // Denominator: weights + fixed education(2) const totalWeight = skillsWeight + experienceWeight + projectsWeight + certBonus; const dynamicScore = Math.round( (Math.max(app.scores.skills, app.scores.technical) * skillsWeight + app.scores.experience * experienceWeight + app.scores.education * 2 + // Education as a 2x base app.scores.projects * projectsWeight + // Projects now dynamic app.scores.certifications * certBonus) / (totalWeight + 2) ); // If calculation leads to 0 but DB has a score, and weights are default, show DB score const finalScore = (dynamicScore === 0 && app.dbScore > 0) ? app.dbScore : dynamicScore; return { ...app, score: finalScore }; }).filter(app => { const matchesSearch = app.name.toLowerCase().includes(searchQuery.toLowerCase()) || (app.jobTitle || '').toLowerCase().includes(searchQuery.toLowerCase()); const matchesStatus = filters.status === 'All' || app.status === filters.status; const matchesScore = (app.score || 0) >= filters.minScore; const matchesLang = (filters.languages || []).length === 0 || (filters.languages || []).some(l => (app.skills || []).includes(l)); const matchesPos = (filters.positions || []).length === 0 || (filters.positions || []).includes(app.jobTitle); return matchesSearch && matchesStatus && matchesScore && matchesLang && matchesPos; }).sort((a, b) => { if (filters.sortBy === 'Match Score') return (b.score || 0) - (a.score || 0); if (filters.sortBy === 'Experience') return (b.experience || 0) - (a.experience || 0); if (filters.sortBy === 'Name') return a.name.localeCompare(b.name); return 0; }); }, [searchQuery, filters, applicants, scoringConfig]); // --- PAGINATION & SELECTION UTILS --- const totalPages = Math.ceil(filteredApplicants.length / itemsPerPage); const paginatedApplicants = filteredApplicants.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage); const toggleSelectAll = () => { if (selectedIds.length === paginatedApplicants.length && paginatedApplicants.length > 0) { setSelectedIds([]); } else { setSelectedIds(paginatedApplicants.map(a => a.id)); } }; const toggleSelectRow = (id) => { if (selectedIds.includes(id)) setSelectedIds(selectedIds.filter(sid => sid !== id)); else setSelectedIds([...selectedIds, id]); }; const togglePanel = (panelName) => { if (openPanel === panelName) setOpenPanel(null); else setOpenPanel(panelName); }; return (

CV Sorting

{/* Controls Bar */}
setSearchQuery(e.target.value)} style={{ width: '100%', padding: '0.75rem 0.75rem 0.75rem 2.5rem', borderRadius: '0.5rem', border: '1px solid rgba(239, 68, 68, 0.3)', backgroundColor: 'rgba(255,255,255,0.05)', color: 'white' }} />
togglePanel('filter')} whileHover={{ scale: 1.02 }} style={{ backgroundColor: openPanel === 'filter' ? '#EF4444' : 'rgba(255,255,255,0.1)', color: 'white', padding: '0.75rem 1.2rem', borderRadius: '0.5rem', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontWeight: '600' }}> Filters togglePanel('scoring')} whileHover={{ scale: 1.02 }} style={{ backgroundColor: openPanel === 'scoring' ? '#EF4444' : 'rgba(255,255,255,0.1)', color: 'white', padding: '0.75rem 1.2rem', borderRadius: '0.5rem', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontWeight: '600' }}> Scoring { setSearchQuery(''); setFilters({ sortBy: 'Match Score', status: 'All', minScore: 0, languages: [], positions: [] }); }} whileHover={{ scale: 1.02 }} style={{ backgroundColor: 'rgba(255,255,255,0.1)', color: 'white', padding: '0.75rem 1rem', borderRadius: '0.5rem', border: '1px solid rgba(255,255,255,0.2)', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '6px' }}> Clear
Sort By
{['Match Score', 'Experience', 'Name', 'Date'].map(opt => ( ))}
{openPanel === 'filter' && } {openPanel === 'scoring' && setScoringConfig(defaultScoring)} onClose={() => setOpenPanel(null)} />}
{/* Results Table */}

Applications ({filteredApplicants.length})

{selectedIds.length > 0 && ( {selectedIds.length} Selected )}
{isLoading ? (
) : ( <>
{paginatedApplicants.map((app) => { const isSelected = selectedIds.includes(app.id); return ( toggleSelectRow(app.id)} > ); })}
0 && selectedIds.length === paginatedApplicants.length} onChange={toggleSelectAll} /> Applicant Experience Job Title Score Status Action
{ e.stopPropagation(); toggleSelectRow(app.id); }} />
{app.name}

{app.name}

{app.email}

{app.experience > 0 ? `${app.experience} years` : 'Fresher'} {app.jobTitle} 80 ? '#34d399' : '#fbbf24' }}>{app.score || 0} {app.status}
{totalPages > 1 && (
Showing {((currentPage - 1) * itemsPerPage) + 1}-{Math.min(currentPage * itemsPerPage, filteredApplicants.length)} of {filteredApplicants.length}
{currentPage}
)} )}
{/* Render the Drawer specifically for the sorting page */} {isDrawerOpen && ( setIsDrawerOpen(false)} candidate={drawerCandidate} /> )}
); }