import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { cn } from '@/lib/utils';
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu';
import { MessageSquare, Phone, Calendar, X, Trash2, ChevronDown, UserCheck, Briefcase, ArrowRight } from 'lucide-react';
import { Image } from '@/components/ui/image';
import { getScoreBand } from '@/lib/talentHome';
import CandidateExpandedDetails from '@/components/krow/CandidateExpandedDetails';
import ScheduleInterviewModal from '@/components/krow/ScheduleInterviewModal';
import MessageCandidateModal from '@/components/krow/MessageCandidateModal';
import { toast } from 'react-hot-toast';
import { base44 } from '@/api/base44Client';
const STATUS_STYLES = {
hired: { badge: 'bg-[#E8F0FE] text-[#1A56DB]', label: 'Hired', card: 'bg-white border-[#E5E7EB]' },
ai_screened: { badge: 'bg-[#E8F0FE] text-[#1A56DB]', label: 'AI Screened', card: 'bg-white border-[#E5E7EB]' },
shortlisted: { badge: 'bg-[#E8F0FE] text-[#1A56DB]', label: 'Shortlisted', card: 'bg-white border-[#E5E7EB]' },
interview: { badge: 'bg-[#E8F0FE] text-[#1A56DB]', label: 'Interviewing', card: 'bg-white border-[#E5E7EB]' },
applied: { badge: 'bg-[#F3F4F6] text-[#6B7280]', label: 'Applied', card: 'bg-white border-[#E5E7EB]' },
rejected: { badge: 'bg-[#FEE2E2] text-[#991B1B]', label: 'Rejected', card: 'bg-white border-[#E5E7EB]' },
};
function getInitials(name) {
return name?.split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase() || '??';
}
const PORTRAIT_PHOTOS = [
'photo-1500648767731-7d3514ad8b31',
'photo-1531123897727-8f129e1688ce',
'photo-1573496359142-b8d87734a5a2',
'photo-1487412720507-e7ab37603c6f',
'photo-1502685104226-ee3a97a2f2c8',
'photo-1544005313-94ddf0286df2',
'photo-1494790108377-be9c29b29330',
'photo-1438761681033-6461ffad8d80',
'photo-1517841905240-472988babdf9',
'photo-1463453091185-61582044d556',
];
function getPortraitSeed(name) {
let h = 0;
for (let i = 0; i < (name || '').length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;
return PORTRAIT_PHOTOS[h % PORTRAIT_PHOTOS.length];
}
function getScoreColor(score) {
if (score > 0) return '#0838E0';
return '#EF4444';
}
function ScoreRing({ score }) {
const color = getScoreColor(score);
const radius = 24;
const circumference = 2 * Math.PI * radius;
const offset = circumference - (score / 100) * circumference;
return (
{score}
);
}
export default function CandidateCard({ application, jobTitle, rank, onAction, onMessage, onCall, onSchedule, onDecline, onDelete, onHire }) {
const navigate = useNavigate();
const position = jobTitle || application.job_title || '';
const status = application.status || 'applied';
const style = STATUS_STYLES[status] || STATUS_STYLES.applied;
const score = application.ai_score || 0;
const canTakeAction = status !== 'hired' && status !== 'rejected';
const [expanded, setExpanded] = useState(false);
const [internalMessageOpen, setInternalMessageOpen] = useState(false);
const [internalScheduleOpen, setInternalScheduleOpen] = useState(false);
const handleMessage = (e) => {
e?.stopPropagation();
if (onMessage) {
onMessage(application);
} else {
setInternalMessageOpen(true);
}
};
const handleCall = (e) => {
e?.stopPropagation();
if (onCall) {
onCall(application);
} else if (application.phone) {
window.location.href = `tel:${application.phone}`;
} else {
setInternalMessageOpen(true);
}
};
const handleSchedule = (e) => {
e?.stopPropagation();
if (onSchedule) {
onSchedule(application);
} else {
setInternalScheduleOpen(true);
}
};
const handleDecline = async (e) => {
e?.stopPropagation();
if (onDecline) {
onDecline(application);
} else {
try {
await base44.entities.JobApplication.update(application.id, { status: 'rejected' });
toast.success(`${application.applicant_name} declined`);
} catch (err) {
toast.error('Failed to decline candidate');
}
}
};
const handleDelete = async (e) => {
e?.stopPropagation();
if (onDelete) {
onDelete(application);
} else {
try {
await base44.entities.JobApplication.delete(application.id);
toast.success(`${application.applicant_name} deleted`);
} catch (err) {
toast.error('Failed to delete candidate');
}
}
};
return (
setExpanded(!expanded)}
>
{/* Rank */}
{rank}
{/* Avatar — 88-110px target size */}
{/* Info */}
{application.applicant_name}
{style.label}
{score > 0 && (() => {
const band = getScoreBand(score);
return (
{band.label}
);
})()}
{position && (
{position}
)}
{application.email && (
{application.email}
)}
{application.phone && (
{application.phone}
)}
{application.years_experience != null && (
{application.years_experience}yr exp
)}
{application.ai_match_label && (
+ {application.ai_match_label}
)}
{/* Score */}
{status !== 'applied' &&
}
{/* Actions */}
e.stopPropagation()}>
{/* The full record, at the route that already serves it.
Clicking the row expands a summary in place, which is the right
default while comparing people — but the complete profile had no
way in from this list at all, so the same candidate opened from a
recommendation and from here led to two different depths of the
same person. One link, one route, one profile. */}
navigate(`/admin/candidates/${application.id}`)}
className="hidden sm:inline-flex items-center gap-1 rounded-lg px-2.5 py-1.5 text-[12px] font-semibold text-[#0838E0]
transition-colors hover:bg-[#EEF3FE] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0838E0]/40"
>
View profile
{onAction && canTakeAction && (
onAction(application)}
className="px-3 py-1.5 rounded-lg bg-[#0838E0] hover:bg-[#062BAF] text-white text-[12px] font-medium transition-colors whitespace-nowrap"
>
{status === 'interview' ? 'AI Interview' : 'Interview'}
)}
Actions
Message
Call
Set up interview
{onHire && (
{ e.stopPropagation(); onHire(application); }} className="cursor-pointer py-2 px-3 focus:bg-[#F3F4F6] rounded-lg">
Hire candidate
)}
Decline candidate
Delete candidate
{expanded &&
}
{internalScheduleOpen && (
setInternalScheduleOpen(false)} application={application} />
)}
{internalMessageOpen && (
setInternalMessageOpen(false)} application={application} />
)}
);
}