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 */}
{application.applicant_name}
{/* 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. */} {onAction && canTakeAction && ( )} 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} /> )}
); }