import React, { useMemo, useState } from 'react'; import { Button, SearchInput } from '@/components/ds'; import { useApplications, useInterviews, useJobPostings, useUpdateApplication, useHireCandidate } from '@/lib/krowHooks'; import { base44 } from '@/api/base44Client'; import { toast } from 'react-hot-toast'; import { AdminPage, Toolbar } from '@/pages/admin/_shell'; import { FilterSelect } from '@/pages/admin/Positions'; import CandidateCard from '@/components/krow/CandidateCard'; import AIInterviewModal from '@/components/krow/AIInterviewModal'; import ScheduleInterviewModal from '@/components/krow/ScheduleInterviewModal'; import MessageCandidateModal from '@/components/krow/MessageCandidateModal'; const SCORE_BANDS = { all: () => true, top: (s) => s >= 80, strong: (s) => s >= 60 && s < 80, weak: (s) => s > 0 && s < 60, unscored: (s) => !s, }; const SORTS = { score: { label: 'Highest score', compare: (a, b) => (b.ai_score || 0) - (a.ai_score || 0) }, recent: { label: 'Recent activity', compare: (a, b) => new Date(b.updated_date) - new Date(a.updated_date) }, name: { label: 'Name A–Z', compare: (a, b) => a.applicant_name.localeCompare(b.applicant_name) }, experience: { label: 'Most experience', compare: (a, b) => (b.years_experience || 0) - (a.years_experience || 0) }, }; export default function AdminCandidates() { const { data: applications = [], isLoading } = useApplications(); const { data: postings = [] } = useJobPostings(); const { data: interviews = [] } = useInterviews(); const updateApp = useUpdateApplication(); const hireCandidate = useHireCandidate(); const [search, setSearch] = useState(''); const [position, setPosition] = useState('all'); const [stage, setStage] = useState('all'); const [band, setBand] = useState('all'); const [sort, setSort] = useState('score'); const [interviewApp, setInterviewApp] = useState(null); const [scheduleApp, setScheduleApp] = useState(null); const [messageApp, setMessageApp] = useState(null); const handleDecline = async (app) => { try { await updateApp.mutateAsync({ id: app.id, data: { status: 'rejected' } }); toast.success(`${app.applicant_name} declined`); } catch (e) { toast.error('Failed to decline candidate'); } }; const handleDelete = async (app) => { try { await base44.entities.JobApplication.delete(app.id); toast.success(`${app.applicant_name} deleted`); } catch (e) { toast.error('Failed to delete candidate'); } }; const handleHire = async (app) => { try { const job = postings.find(p => p.id === app.job_posting_id); await hireCandidate.mutateAsync({ application: app, job }); toast.success(`${app.applicant_name} hired!`); } catch (e) { toast.error('Failed to hire candidate'); } }; const handleAction = (app) => { if (app.status === 'interview') { setInterviewApp(app); } else { setScheduleApp(app); } }; const positions = useMemo( () => [...new Set(applications.map((a) => a.job_title).filter(Boolean))].sort(), [applications] ); const filtered = useMemo(() => { const q = search.toLowerCase(); return applications .filter((a) => { if (q && !`${a.applicant_name} ${a.email} ${a.job_title} ${(a.skills || []).join(' ')}`.toLowerCase().includes(q)) return false; if (position !== 'all' && a.job_title !== position) return false; if (stage !== 'all' && a.stage !== stage) return false; if (band !== 'all' && !SCORE_BANDS[band](a.ai_score)) return false; return true; }) .sort(SORTS[sort].compare); }, [applications, search, position, stage, band, sort]); const isFiltered = Boolean(search) || [position, stage, band].some((v) => v !== 'all'); const clearFilters = () => { setSearch(''); setPosition('all'); setStage('all'); setBand('all'); }; const postingById = useMemo(() => Object.fromEntries(postings.map(p => [p.id, p])), [postings]); const resolveTitle = (a) => a.job_title || postingById[a.job_posting_id]?.title || ''; return ( } filters={ <> ({ value: p, label: p }))]} /> ({ value, label: s.label }))} /> } meta={`${filtered.length} of ${applications.length} candidates${isFiltered ? ' · filtered' : ''}`} /> {isLoading ? (
{[...Array(5)].map((_, i) => (
))}
) : filtered.length === 0 ? (

No candidates match your filters

{isFiltered && ( )}
) : (
{filtered.map((app, idx) => ( ))}
)} {/* Modals */} {interviewApp && ( setInterviewApp(null)} application={interviewApp} job={postings.find(p => p.id === interviewApp.job_posting_id)} /> )} {scheduleApp && ( setScheduleApp(null)} application={scheduleApp} /> )} {messageApp && ( setMessageApp(null)} application={messageApp} /> )} ); }