iris_backend / src /components /Adminfront /TopPerformers.jsx
Muhammed Sameer
new feature implemented
ff85727
import React, { useEffect, useState, useMemo } from 'react';
import { supabase } from '../../supabaseClient';
import { motion, AnimatePresence } from 'framer-motion';
export default function TopPerformers() {
// --- STATE ---
const [candidates, setCandidates] = useState([]);
const [loading, setLoading] = useState(true);
const [showConfig, setShowConfig] = useState(false);
// Config State
const [config, setConfig] = useState({
skillsWeight: 5,
experienceWeight: 5,
projectWeight: 5,
});
const handleConfigChange = (key, e) => {
setConfig({ ...config, [key]: parseInt(e.target.value) });
};
// --- SUPABASE DATA FETCHING ---
useEffect(() => {
const fetchCandidates = async () => {
try {
// ✅ FIX: Removed comments inside the string
const { data, error } = await supabase
.from('applications')
.select(`
id,
match_score,
experience,
profiles (
id,
full_name,
avatar_url,
technical_skills,
projects,
experience_years,
summary
),
jobs ( title )
`)
.limit(10);
if (error) throw error;
setCandidates(data || []);
} catch (error) {
console.error("Fetch error:", error);
} finally {
setLoading(false);
}
};
fetchCandidates();
}, []);
// --- ⚡️ LOGIC ENGINE ---
const processedCandidates = useMemo(() => {
if (!candidates.length) return [];
return candidates.map(candidate => {
const profile = candidate.profiles || {};
const job = candidate.jobs || {};
// 1. EXPERIENCE - Try multiple sources
const expVal = parseFloat(candidate.experience) || parseFloat(profile.experience_years) || 0;
const expScore = Math.min(expVal * 10, 100);
// 2. SKILLS - Use match_score with fallback to skill count
let skillScore = parseFloat(candidate.match_score) || 0;
if (skillScore === 0) {
// Fallback: count technical skills
const skillsText = profile.technical_skills || "";
const skillCount = typeof skillsText === 'string' && skillsText.trim().length > 0
? skillsText.split(',').length
: 0;
skillScore = Math.min(skillCount * 10, 100); // Each skill = 10 points
}
// Display Logic Only
const skillsText = profile.technical_skills || "";
const skillCountForDisplay = typeof skillsText === 'string' && skillsText.trim().length > 0
? skillsText.split(',').length
: 0;
// 3. PROJECTS (JSON Array -> Number)
const projectsData = profile.projects;
const projCount = Array.isArray(projectsData) ? projectsData.length : 0;
const projScore = Math.min(projCount * 20, 100);
// 4. WEIGHTED FORMULA - BOOST MODEL
// ✅ FIXED: Each weight acts as a boost multiplier
// Weight of 5 = 1x, weight of 10 = 2x amplification
// This ensures increasing a weight ALWAYS increases the score
const skillBoost = skillScore * (1 + config.skillsWeight / 10);
const expBoost = expScore * (1 + config.experienceWeight / 10);
const projBoost = projScore * (1 + config.projectWeight / 10);
const rawScore = Math.min((skillBoost + expBoost + projBoost) / 3, 100);
return {
...candidate,
id: candidate.id,
name: profile.full_name || 'Candidate',
avatar: profile.avatar_url,
role: job.title || 'Applicant',
summary: profile.summary,
finalScore: Math.round(rawScore),
displayExp: expVal,
displayProj: projCount,
displaySkills: skillCountForDisplay
};
})
.sort((a, b) => b.finalScore - a.finalScore)
.slice(0, 5);
}, [candidates, config]);
// --- STYLES ---
const containerStyle = {
backgroundColor: 'rgba(239, 68, 68, 0.05)',
border: '1px solid rgba(239, 68, 68, 0.2)',
borderRadius: '1rem',
padding: '1.5rem',
color: 'white',
height: '100%',
fontFamily: 'sans-serif'
};
const configBoxStyle = {
backgroundColor: 'rgba(0, 0, 0, 0.3)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '0.75rem',
padding: '1.25rem',
marginBottom: '1.5rem',
marginTop: '0.5rem'
};
const labelRowStyle = {
display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem',
fontSize: '0.85rem', fontWeight: '500', color: '#D1D5DB'
};
const getSliderStyle = (value, max) => {
const percentage = (value / max) * 100;
return {
width: '100%', height: '6px', borderRadius: '3px',
background: `linear-gradient(to right, #EF4444 0%, #EF4444 ${percentage}%, #374151 ${percentage}%, #374151 100%)`,
appearance: 'none', outline: 'none', cursor: 'pointer'
};
};
return (
<div style={containerStyle}>
{/* HEADER */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem' }}>
<h2 style={{ fontSize: '1.25rem', fontWeight: 'bold', margin: 0 }}>Top Performers</h2>
<button
onClick={() => setShowConfig(!showConfig)}
style={{
background: 'none', border: 'none',
color: showConfig ? '#EF4444' : '#9CA3AF',
cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '6px',
fontSize: '0.9rem', fontWeight: '500'
}}
>
<span style={{ fontSize: '1.1rem' }}></span>
{showConfig ? "Hide" : "Config"}
</button>
</div>
<p style={{ color: '#9CA3AF', fontSize: '0.85rem', marginBottom: '1.5rem', marginTop: '0.25rem' }}>
Calculated based on Match Score, Experience & Projects
</p>
{/* CONFIG PANEL */}
<AnimatePresence>
{showConfig && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
style={{ overflow: 'hidden' }}
>
<div style={configBoxStyle}>
<h3 style={{ margin: '0 0 1rem 0', fontSize: '0.9rem', color: '#F3F4F6' }}>Scoring Priorities</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<div>
<div style={labelRowStyle}>
<span>Skills Priority</span> <span style={{ color: '#EF4444' }}>{config.skillsWeight}</span>
</div>
<input type="range" max="10" value={config.skillsWeight} onChange={(e) => handleConfigChange('skillsWeight', e)} style={getSliderStyle(config.skillsWeight, 10)} className="custom-range" />
</div>
<div>
<div style={labelRowStyle}>
<span>Experience Priority</span> <span style={{ color: '#EF4444' }}>{config.experienceWeight}</span>
</div>
<input type="range" max="10" value={config.experienceWeight} onChange={(e) => handleConfigChange('experienceWeight', e)} style={getSliderStyle(config.experienceWeight, 10)} className="custom-range" />
</div>
<div>
<div style={labelRowStyle}>
<span>Projects Priority</span> <span style={{ color: '#EF4444' }}>{config.projectWeight}</span>
</div>
<input type="range" max="10" value={config.projectWeight} onChange={(e) => handleConfigChange('projectWeight', e)} style={getSliderStyle(config.projectWeight, 10)} className="custom-range" />
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* CANDIDATES LIST */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<AnimatePresence>
{loading ? (
<p style={{ color: '#6B7280', textAlign: 'center' }}>Loading...</p>
) : processedCandidates.length > 0 ? (
processedCandidates.map((item) => {
const exp = item.displayExp > 0 ? `${item.displayExp} yrs` : 'Fresher';
return (
<motion.div
key={item.id}
layout
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
>
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
{/* Avatar */}
{item.avatar ? (
<img src={item.avatar} alt={item.name} style={{ width: '45px', height: '45px', borderRadius: '50%', objectFit: 'cover', border: '2px solid #374151' }} />
) : (
<div style={{ width: '45px', height: '45px', borderRadius: '50%', backgroundColor: '#374151', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'white', fontWeight: 'bold' }}>
{item.name.charAt(0)}
</div>
)}
{/* Text Info */}
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span style={{ fontWeight: '600', fontSize: '1rem' }}>{item.name}</span>
<span style={{ backgroundColor: 'rgba(255,255,255,0.1)', fontSize: '0.7rem', padding: '2px 6px', borderRadius: '4px', color: '#D1D5DB' }}>
Score: {item.finalScore}
</span>
</div>
<div style={{ fontSize: '0.75rem', color: '#9CA3AF', marginTop: '2px' }}>
{item.role} • {exp} • <span style={{ color: '#FCA5A5' }}>{item.displayProj} Proj</span> • {item.displaySkills} Skills
</div>
</div>
</div>
</motion.div>
);
})
) : (
<p style={{ color: '#6B7280', textAlign: 'center' }}>No candidates found</p>
)}
</AnimatePresence>
</div>
<style>{`
.custom-range::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px; height: 14px;
border-radius: 50%;
background: white;
border: 2px solid #EF4444;
cursor: pointer;
margin-top: -4px;
}
`}</style>
</div>
);
}