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 => (
))}
{/* 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 }) => (
);
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 (
{/* 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 ? (
) : (
<>
|
0 && selectedIds.length === paginatedApplicants.length}
onChange={toggleSelectAll}
/>
|
Applicant |
Experience |
Job Title |
Score |
Status |
Action |
{paginatedApplicants.map((app) => {
const isSelected = selectedIds.includes(app.id);
return (
toggleSelectRow(app.id)}
>
{ e.stopPropagation(); toggleSelectRow(app.id); }}
/>
|
|
{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}
/>
)}
);
}