Initial commit
This commit is contained in:
322
src/components/krow/TalentDetailModal.jsx
Normal file
322
src/components/krow/TalentDetailModal.jsx
Normal file
@@ -0,0 +1,322 @@
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Mail, Phone, Briefcase, Star, Award, BadgeCheck, Fingerprint, X, Calendar, UserCheck, Sparkles, Brain, MessageSquare } from 'lucide-react';
|
||||
import { Image } from '@/components/ui/image';
|
||||
import CareerScoreGauge from '@/components/krow/talent/CareerScoreGauge';
|
||||
import ExperienceEndorsements, { verifiedExperience } from '@/components/krow/talent/ExperienceEndorsements';
|
||||
import EliteTalentScore from '@/components/krow/talent/EliteTalentScore';
|
||||
|
||||
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 getInitials(name) {
|
||||
return (name || '').split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase() || '??';
|
||||
}
|
||||
|
||||
function getKrowIdCode(profile) {
|
||||
if (profile?.id) {
|
||||
const hex = profile.id.replace(/[^a-f0-9]/gi, '');
|
||||
if (hex.length >= 6) return hex.slice(0, 6).toUpperCase();
|
||||
}
|
||||
let h = 0;
|
||||
const seed = profile?.full_name || 'KROW';
|
||||
for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) >>> 0;
|
||||
return (h & 0xFFFFFF).toString(16).padStart(6, '0').toUpperCase();
|
||||
}
|
||||
|
||||
function formatDate(d) {
|
||||
try {
|
||||
return new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
} catch { return ''; }
|
||||
}
|
||||
|
||||
const BADGE_LEVEL_STYLES = {
|
||||
bronze: 'bg-[#FEF3C7] text-[#92400E] border-[#FCD34D]',
|
||||
silver: 'bg-[#F3F4F6] text-[#4B5563] border-[#D1D5DB]',
|
||||
gold: 'bg-[#FEF9E7] text-[#92400E] border-[#F9E547]',
|
||||
platinum: 'bg-[#EFF6FF] text-[#1E40AF] border-[#BFDBFE]',
|
||||
};
|
||||
|
||||
function Section({ label, children }) {
|
||||
if (!children) return null;
|
||||
return (
|
||||
<div>
|
||||
<p className="text-[10px] font-bold text-[#9CA3AF] tracking-wider mb-2">{label}</p>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PillList({ items, className = 'bg-[#F0F4F8] text-[#0047AB]' }) {
|
||||
if (!items || items.length === 0) return null;
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{items.map((it, i) => (
|
||||
<span key={i} className={`px-2.5 py-1 rounded-full text-[11px] font-medium ${className}`}>{it}</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ScoreFactor({ label, value }) {
|
||||
return (
|
||||
<div className="inline-flex items-center justify-between gap-2 px-3 py-1.5 rounded-full border border-[#E5E7EB] bg-white">
|
||||
<span className="text-[11px] text-[#6B7280] font-medium">{label}</span>
|
||||
<span className="text-[12px] font-bold text-[#2563EB]">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TalentDetailModal({ open, onClose, profile, onContact, onSchedule, onHire, matchScore, matchReasons }) {
|
||||
if (!profile) return null;
|
||||
const badges = profile.earned_badges || [];
|
||||
const capabilities = (profile.capabilities || []).filter((c) => c.status === 'verified' || c.status === 'pending');
|
||||
const experience = profile.experience || [];
|
||||
const strengths = profile.strengths || [];
|
||||
|
||||
const verifiedExperienceList = verifiedExperience(profile).length > 0;
|
||||
const krowCode = getKrowIdCode(profile);
|
||||
const reportDate = formatDate(profile.updated_date || new Date());
|
||||
const openToWork = profile.status === 'active' && (profile.availability?.length > 0 || !profile.current_position);
|
||||
|
||||
const reliability = Math.round(profile.reliability_score || 0);
|
||||
const attendance = Math.round(profile.attendance_score || 0);
|
||||
const performance = Math.round(profile.performance_score || 0);
|
||||
const leadership = Math.round(profile.leadership_potential || 0);
|
||||
const aiInterview = Math.round(profile.ai_interview_score || 0);
|
||||
const xp = Math.round(profile.xp || 0);
|
||||
|
||||
const identityVerified = capabilities.length > 0 || badges.length > 0;
|
||||
const identityTag = identityVerified ? 'Verified' : 'Pending';
|
||||
const identityClass = identityVerified ? 'bg-[#EFF6FF] text-[#1E40AF]' : 'bg-[#FFF7ED] text-[#B45309]';
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto p-0">
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{profile.full_name} — KROW Identity</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* KROW ID header — clean white, no blue band */}
|
||||
<div className="relative p-5 sm:p-6 bg-gradient-to-b from-[#F5F7FB] to-white">
|
||||
<button onClick={onClose} className="absolute top-4 right-4 text-[#9CA3AF] hover:text-[#111827]">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-16 h-16 rounded-full overflow-hidden ring-2 ring-[#E5E7EB] shrink-0 bg-[#E5E7EB] flex items-center justify-center">
|
||||
{profile.selfie_url ? (
|
||||
<Image
|
||||
src={profile.selfie_url}
|
||||
alt={profile.full_name}
|
||||
className="w-full h-full object-cover"
|
||||
fittingType="fill"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-[20px] font-bold text-[#1D4ED8]">{getInitials(profile.full_name)}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 pt-0.5">
|
||||
<div className="inline-flex items-center gap-1.5 text-[10px] font-bold tracking-wide text-[#2563EB] uppercase">
|
||||
<Fingerprint className="w-3 h-3" /> KROW ID · {krowCode}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
||||
<h2 className="text-[22px] font-bold leading-tight text-[#111827] truncate">{profile.full_name}</h2>
|
||||
{identityVerified && (
|
||||
<span className={`text-[10px] font-semibold px-2 py-0.5 rounded-full ${identityClass}`}>{identityTag}</span>
|
||||
)}
|
||||
{openToWork && (
|
||||
<span className="text-[12px] text-[#6B7280]">· Open to work</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[11px] text-[#6B7280] mt-1">
|
||||
Career Report · {reportDate}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-5 sm:px-6 pb-5 sm:pb-6 space-y-5">
|
||||
{/* Quick stats banner — pill capsules, single line */}
|
||||
<div className="flex flex-nowrap gap-1.5 overflow-x-auto no-scrollbar">
|
||||
{profile.experience_years > 0 && (
|
||||
<Stat icon={<Briefcase className="w-3 h-3" />} label="Experience" value={`${profile.experience_years} yr`} />
|
||||
)}
|
||||
{profile.shifts_completed > 0 && (
|
||||
<Stat icon={<Briefcase className="w-3 h-3" />} label="Shifts" value={profile.shifts_completed} />
|
||||
)}
|
||||
{(profile.client_rating > 0 || profile.supervisor_rating > 0) && (
|
||||
<Stat icon={<Star className="w-3 h-3" />} label="Rating" value={`${((profile.client_rating + profile.supervisor_rating) / 2).toFixed(1)}`} valueClass="text-[#F59E0B]" />
|
||||
)}
|
||||
{profile.desired_position && (
|
||||
<Stat icon={<Briefcase className="w-3 h-3" />} label="Role" value={profile.desired_position} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Match banner */}
|
||||
{matchScore != null && (
|
||||
<div className="rounded-xl border border-[#DBEAFE] bg-[#EFF6FF] p-4">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<Sparkles className="w-4 h-4 text-[#0A39DF]" />
|
||||
<span className="text-[13px] font-bold text-[#0A39DF]">Match Score: {matchScore}/100</span>
|
||||
</div>
|
||||
{matchReasons?.length > 0 && (
|
||||
<ul className="text-[12px] text-[#374151] list-disc list-inside space-y-0.5">
|
||||
{matchReasons.map((r, i) => <li key={i}>{r}</li>)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Career Score */}
|
||||
<div className="bg-white border border-[#E5E7EB] rounded-2xl py-7 px-4 flex flex-col items-center">
|
||||
<CareerScoreGauge profile={profile} />
|
||||
</div>
|
||||
|
||||
{/* Elite Talent scorecard — dotted-leader metric list */}
|
||||
<EliteTalentScore profile={profile} />
|
||||
|
||||
<div className="bg-white border border-[#E5E7EB] rounded-2xl p-4 space-y-4">
|
||||
<Section label="SKILLS">
|
||||
<PillList items={profile.skills} />
|
||||
</Section>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 pt-3 border-t border-[#F3F4F6]">
|
||||
<Section label="LANGUAGES">
|
||||
<PillList items={profile.languages} />
|
||||
</Section>
|
||||
<Section label="AVAILABILITY">
|
||||
<PillList items={profile.availability} className="bg-[#F3F4F6] text-[#374151]" />
|
||||
</Section>
|
||||
<Section label="CERTIFICATIONS">
|
||||
<PillList items={profile.certifications} className="bg-[#F3F4F6] text-[#374151]" />
|
||||
</Section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Badges */}
|
||||
{badges.length > 0 && (
|
||||
<Section label="BADGES">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{badges.map((b, i) => (
|
||||
<span key={i} className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-[12px] font-medium ${BADGE_LEVEL_STYLES[b.level] || BADGE_LEVEL_STYLES.bronze}`}>
|
||||
<Award className="w-3.5 h-3.5" />
|
||||
{b.name}
|
||||
{b.level && <span className="opacity-70 text-[10px] capitalize">· {b.level}</span>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{/* Endorsements / Verified Capabilities */}
|
||||
{capabilities.length > 0 && (
|
||||
<Section label="VERIFIED CAPABILITIES (ENDORSEMENTS)">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{capabilities.map((c, i) => (
|
||||
<span key={i} className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[12px] ${c.status === 'verified' ? 'bg-[#F0F4F8] text-[#1E56C1]' : 'bg-[#F3F4F6] text-[#6B7280]'}`}>
|
||||
<BadgeCheck className="w-3.5 h-3.5" />
|
||||
<span className="font-semibold">{c.skill}</span>
|
||||
{c.level && <span className="text-[11px] text-[#5D759B]">· {c.level}</span>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{/* Consolidated Experience & Endorsements — jobs + client endorsements in one list */}
|
||||
{verifiedExperience(profile).length > 0 && (
|
||||
<Section label="EXPERIENCE & ENDORSEMENTS">
|
||||
<ExperienceEndorsements profile={profile} />
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{/* Personality + strengths */}
|
||||
{(profile.personality || strengths.length > 0 || profile.communication_style) && (
|
||||
<div className="bg-white border border-[#E5E7EB] rounded-2xl p-4 space-y-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{profile.personality && (
|
||||
<div className="flex gap-2.5">
|
||||
<span className="w-7 h-7 rounded-lg bg-[#EFF6FF] flex items-center justify-center shrink-0">
|
||||
<Brain className="w-3.5 h-3.5 text-[#0A39DF]" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] font-bold text-[#9CA3AF] tracking-wider mb-1">PERSONALITY</p>
|
||||
<p className="text-[12px] text-[#374151] leading-relaxed">{profile.personality}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{profile.communication_style && (
|
||||
<div className="flex gap-2.5">
|
||||
<span className="w-7 h-7 rounded-lg bg-[#EFF6FF] flex items-center justify-center shrink-0">
|
||||
<MessageSquare className="w-3.5 h-3.5 text-[#0A39DF]" />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] font-bold text-[#9CA3AF] tracking-wider mb-1">COMMUNICATION STYLE</p>
|
||||
<p className="text-[12px] text-[#374151] leading-relaxed">{profile.communication_style}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{strengths.length > 0 && (
|
||||
<div className="pt-3 border-t border-[#F3F4F6]">
|
||||
<p className="text-[10px] font-bold text-[#9CA3AF] tracking-wider mb-2">STRENGTHS</p>
|
||||
<PillList items={strengths} className="bg-[#EFF6FF] text-[#0A39DF]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Contact info + actions */}
|
||||
<div className="border-t border-[#E5E7EB] pt-4">
|
||||
<div className="flex items-center gap-4 text-[12px] text-[#6B7280] flex-wrap mb-3">
|
||||
{profile.email && <span className="inline-flex items-center gap-1"><Mail className="w-3.5 h-3.5" /> {profile.email}</span>}
|
||||
{profile.phone && <span className="inline-flex items-center gap-1"><Phone className="w-3.5 h-3.5" /> {profile.phone}</span>}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{onContact && (
|
||||
<button onClick={() => onContact(profile)} className="inline-flex items-center gap-1.5 bg-[#0A39DF] hover:bg-[#082EB4] text-white text-[13px] font-medium px-4 py-2 rounded-lg transition-colors">
|
||||
<Mail className="w-4 h-4" /> Contact
|
||||
</button>
|
||||
)}
|
||||
{onSchedule && (
|
||||
<button onClick={() => onSchedule(profile)} className="inline-flex items-center gap-1.5 border border-[#E5E7EB] hover:bg-[#F9FAFB] text-[#374151] text-[13px] font-medium px-4 py-2 rounded-lg transition-colors">
|
||||
<Calendar className="w-4 h-4" /> Schedule Interview
|
||||
</button>
|
||||
)}
|
||||
{onHire && (
|
||||
<button onClick={() => onHire(profile)} className="inline-flex items-center gap-1.5 border border-[#16A34A] text-[#16A34A] hover:bg-[#16A34A] hover:text-white text-[13px] font-medium px-4 py-2 rounded-lg transition-colors">
|
||||
<UserCheck className="w-4 h-4" /> Quick Hire
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function Stat({ label, value, icon, valueClass = 'text-[#5D759B]' }) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full bg-[#F0F4F8] text-[#1E56C1] shrink-0">
|
||||
{icon}
|
||||
<span className="text-[11px] font-semibold">{label}</span>
|
||||
<span className="text-[11px] text-[#5D759B]">·</span>
|
||||
<span className={`text-[11px] font-medium truncate max-w-[90px] ${valueClass}`}>{value}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user