Initial commit

This commit is contained in:
2026-08-07 23:53:15 +05:30
commit 90f14a73b2
222 changed files with 35000 additions and 0 deletions

176
src/pages/Candidates.jsx Normal file
View File

@@ -0,0 +1,176 @@
import React, { useState } from 'react';
import { useApplications, useJobPostings, useScreenAllCandidates, useUpdateApplication } from '@/lib/krowHooks';
import { base44 } from '@/api/base44Client';
import CandidateCard from '@/components/krow/CandidateCard';
import AIInterviewModal from '@/components/krow/AIInterviewModal';
import ScheduleInterviewModal from '@/components/krow/ScheduleInterviewModal';
import MessageCandidateModal from '@/components/krow/MessageCandidateModal';
import { toast } from 'react-hot-toast';
import { AssistantTrigger } from '@/components/ai-assistant';
export default function Candidates() {
const { data: applications = [], isLoading } = useApplications();
const { data: postings = [] } = useJobPostings();
const screenAll = useScreenAllCandidates();
const [search, setSearch] = useState('');
const [stageFilter, setStageFilter] = useState('all');
const [scoreFilter, setScoreFilter] = useState('all');
const [positionFilter, setPositionFilter] = useState('all');
const [interviewApp, setInterviewApp] = useState(null);
const [scheduleApp, setScheduleApp] = useState(null);
const [messageApp, setMessageApp] = useState(null);
const updateApp = useUpdateApplication();
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 postingById = Object.fromEntries(postings.map(p => [p.id, p]));
const resolveTitle = (a) => a.job_title || postingById[a.job_posting_id]?.title || '';
const sorted = [...applications].sort((a, b) => (b.ai_score || 0) - (a.ai_score || 0));
const positionOptions = Array.from(new Set(applications.map(resolveTitle).filter(Boolean))).sort();
const filtered = sorted.filter(a => {
const title = resolveTitle(a).toLowerCase();
const q = search.toLowerCase();
const matchesSearch = !search ||
a.applicant_name?.toLowerCase().includes(q) ||
a.email?.toLowerCase().includes(q) ||
title.includes(q);
const matchesStage = stageFilter === 'all' || a.status === stageFilter;
const matchesPosition = positionFilter === 'all' || resolveTitle(a) === positionFilter;
const matchesScore = scoreFilter === 'all' ||
(scoreFilter === 'high' && a.ai_score >= 80) ||
(scoreFilter === 'mid' && a.ai_score >= 60 && a.ai_score < 80) ||
(scoreFilter === 'low' && a.ai_score > 0 && a.ai_score < 60);
return matchesSearch && matchesStage && matchesPosition && matchesScore;
});
const handleAction = (app) => {
if (app.status === 'interview') {
setInterviewApp(app);
} else {
setScheduleApp(app);
}
};
const handleScreenAll = async () => {
const unscreened = applications.filter(a => a.status === 'applied');
if (unscreened.length === 0) {
toast('All candidates have been screened');
return;
}
const firstJob = postings.find(p => p.id === unscreened[0].job_posting_id);
try {
await screenAll.mutateAsync({ applications: unscreened, job: firstJob });
toast.success(`Screened ${unscreened.length} candidates`);
} catch (e) {
toast.error('Screening failed');
}
};
return (
<div className="space-y-5">
<div className="flex items-center justify-between">
<div>
<h1 className="text-[22px] font-bold text-[#111827]">All Candidates</h1>
</div>
<div className="flex items-center gap-3">
<span className="text-[13px] text-[#6B7280]">{applications.length} candidates</span>
<button
onClick={handleScreenAll}
disabled={screenAll.isPending}
className="inline-flex items-center gap-1.5 bg-[#0A39DF] hover:bg-[#082EB4] disabled:opacity-50 text-white text-[13px] font-medium px-3 py-2 rounded-lg transition-colors"
>
{screenAll.isPending ? 'Screening...' : 'AI Screen All'}
</button>
<AssistantTrigger />
</div>
</div>
{/* Filters */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3">
<div className="relative flex-1">
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search candidates..."
className="w-full h-10 pl-4 pr-4 rounded-lg border border-[#E5E7EB] bg-white text-[13px] focus:border-[#0A39DF] focus:outline-none"
/>
</div>
<select value={stageFilter} onChange={(e) => setStageFilter(e.target.value)} className="h-10 px-4 rounded-lg border border-[#E5E7EB] bg-white text-[13px] focus:border-[#0A39DF] focus:outline-none">
<option value="all">All Stages</option>
<option value="applied">Applied</option>
<option value="ai_screened">AI Screened</option>
<option value="interview">Interviewing</option>
<option value="hired">Hired</option>
</select>
<select value={positionFilter} onChange={(e) => setPositionFilter(e.target.value)} className="h-10 px-4 rounded-lg border border-[#E5E7EB] bg-white text-[13px] focus:border-[#0A39DF] focus:outline-none max-w-[200px] truncate">
<option value="all">All Positions</option>
{positionOptions.map(p => <option key={p} value={p}>{p}</option>)}
</select>
<select value={scoreFilter} onChange={(e) => setScoreFilter(e.target.value)} className="h-10 px-4 rounded-lg border border-[#E5E7EB] bg-white text-[13px] focus:border-[#0A39DF] focus:outline-none">
<option value="all">All Scores</option>
<option value="high">80+ (Top Talent)</option>
<option value="mid">60-79 (Strong Fit)</option>
<option value="low">0-59 (Possible/Not Fit)</option>
</select>
</div>
{/* List */}
{isLoading ? (
<div className="space-y-3">
{[...Array(5)].map((_, i) => <div key={i} className="h-20 bg-white border border-[#E5E7EB] rounded-xl animate-pulse" />)}
</div>
) : filtered.length === 0 ? (
<div className="text-center py-16">
<p className="text-[14px] text-[#6B7280]">No candidates match your filters</p>
</div>
) : (
<div className="space-y-3">
{filtered.map((app, idx) => (
<CandidateCard
key={app.id}
application={app}
jobTitle={resolveTitle(app)}
rank={idx + 1}
onAction={handleAction}
onMessage={setMessageApp}
onCall={setMessageApp}
onSchedule={setScheduleApp}
onDecline={handleDecline}
onDelete={handleDelete}
/>
))}
</div>
)}
{/* Modals */}
{interviewApp && (
<AIInterviewModal open={true} onClose={() => setInterviewApp(null)} application={interviewApp} job={postings.find(p => p.id === interviewApp.job_posting_id)} />
)}
{scheduleApp && (
<ScheduleInterviewModal open={true} onClose={() => setScheduleApp(null)} application={scheduleApp} />
)}
{messageApp && (
<MessageCandidateModal open={true} onClose={() => setMessageApp(null)} application={messageApp} />
)}
</div>
);
}