Initial commit
This commit is contained in:
188
src/components/krow/WorkforceReputation.jsx
Normal file
188
src/components/krow/WorkforceReputation.jsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import React from 'react';
|
||||
import { Shield, Star, TrendingUp } from 'lucide-react';
|
||||
import { getScoreBand } from '@/lib/talentHome';
|
||||
import ReferencesEndorsements from '@/components/krow/ReferencesEndorsements';
|
||||
|
||||
const R = 44;
|
||||
const STROKE = 8;
|
||||
const CIRC = 2 * Math.PI * R;
|
||||
const FICO_MIN = 300;
|
||||
const FICO_MAX = 850;
|
||||
|
||||
function careerScoreFromApp(app) {
|
||||
const s = Math.max(0, Math.min(100, Math.round(app?.ai_score || 0)));
|
||||
return 300 + Math.round((s / 100) * 550);
|
||||
}
|
||||
|
||||
function marketRangeFromApp(app) {
|
||||
const s = Math.max(0, Math.min(100, Math.round(app?.ai_score || 0)));
|
||||
const uplift = Math.max(0, Math.round((s - 70) / 5));
|
||||
const low = 20 + uplift;
|
||||
return `$${low}-$${low + 4}/hr`;
|
||||
}
|
||||
|
||||
function nextTierFromApp(app) {
|
||||
const s = Math.round(app?.ai_score || 0);
|
||||
const yrs = app?.years_experience || 0;
|
||||
if (s >= 85 || yrs >= 8) return 'Skilled';
|
||||
if (s >= 60 || yrs >= 3) return 'Cross-Trained';
|
||||
return 'Beginner';
|
||||
}
|
||||
|
||||
function CareerScoreMini({ application }) {
|
||||
const score = careerScoreFromApp(application);
|
||||
const pct = Math.max(0, Math.min(1, (score - FICO_MIN) / (FICO_MAX - FICO_MIN)));
|
||||
const dash = pct * CIRC;
|
||||
const band = getScoreBand(Math.round(application?.ai_score || 0));
|
||||
return (
|
||||
<div className="flex items-center gap-5 bg-white/70 rounded-xl border border-[#E5E7EB] px-4 py-3">
|
||||
<div className="flex flex-col items-center shrink-0">
|
||||
<div className="relative" style={{ width: R * 2 + STROKE, height: R * 2 + STROKE }}>
|
||||
<svg width={R * 2 + STROKE} height={R * 2 + STROKE} className="-rotate-90">
|
||||
<circle cx={R + STROKE / 2} cy={R + STROKE / 2} r={R} fill="none" stroke="#EFF6FF" strokeWidth={STROKE} />
|
||||
<circle
|
||||
cx={R + STROKE / 2}
|
||||
cy={R + STROKE / 2}
|
||||
r={R}
|
||||
fill="none"
|
||||
stroke="#0A39DF"
|
||||
strokeWidth={STROKE}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={`${dash} ${CIRC}`}
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<span className="text-[8px] uppercase tracking-wide text-[#94A3B8] font-medium">Career Score</span>
|
||||
<span className="text-[22px] font-bold text-[#0A39DF] leading-none">{score}</span>
|
||||
<span className="text-[8px] text-[#94A3B8] font-medium">300-850</span>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className="mt-2 inline-flex items-center justify-center text-[11px] font-bold text-white px-3 py-1 rounded-full w-full"
|
||||
style={{ backgroundColor: band.color }}
|
||||
>
|
||||
{band.label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 space-y-3">
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-wide text-[#9CA3AF] font-medium">Potential Market Value</p>
|
||||
<p className="text-[16px] font-bold text-[#111827]">{marketRangeFromApp(application)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-wide text-[#9CA3AF] font-medium">Next Promotion</p>
|
||||
<p className="text-[14px] font-semibold text-[#0F172A]">{nextTierFromApp(application)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const COMM_LABELS = [
|
||||
{ min: 85, label: 'Excellent' },
|
||||
{ min: 70, label: 'Good' },
|
||||
{ min: 55, label: 'Average' },
|
||||
{ min: 0, label: 'Needs Work' },
|
||||
];
|
||||
|
||||
const commLabel = (score) => {
|
||||
const v = score || 0;
|
||||
return (COMM_LABELS.find((c) => v >= c.min) || COMM_LABELS[COMM_LABELS.length - 1]).label;
|
||||
};
|
||||
|
||||
const COMPANY_COLORS = [
|
||||
'bg-[#0A39DF]',
|
||||
'bg-[#333F48]',
|
||||
'bg-[#082EB4]',
|
||||
];
|
||||
|
||||
const colorFor = (name) => {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||
return COMPANY_COLORS[Math.abs(hash) % COMPANY_COLORS.length];
|
||||
};
|
||||
|
||||
const initialsFor = (name) => {
|
||||
const parts = name.trim().split(/\s+/);
|
||||
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
||||
return (parts[0][0] + parts[1][0]).toUpperCase();
|
||||
};
|
||||
|
||||
function StatTile({ label, value, suffix, accent }) {
|
||||
const text = value != null && value !== '' ? value : '—';
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-1 px-2 py-3 rounded-xl bg-white/70 border border-[#E5E7EB]">
|
||||
<div className="text-[18px] font-bold leading-none" style={{ color: accent }}>
|
||||
{text}
|
||||
{suffix && <span className="text-[12px] font-semibold ml-0.5">{suffix}</span>}
|
||||
</div>
|
||||
<div className="text-[10px] text-[#6B7280] tracking-wide font-medium">{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WorkforceReputation({ application }) {
|
||||
const breakdown = application?.score_breakdown || {};
|
||||
const companies = application?.companies_worked || [];
|
||||
const clientRating = application?.client_rating || 0;
|
||||
|
||||
const reliability = breakdown.reliability;
|
||||
const attendance = breakdown.attendance_expectations;
|
||||
const leadership = breakdown.leadership_expectations;
|
||||
const communication = breakdown.communication_style;
|
||||
const commText = communication != null ? commLabel(communication) : null;
|
||||
|
||||
const hasScores = [reliability, attendance, leadership, communication].some((v) => v != null);
|
||||
const hasData = hasScores || companies.length > 0 || clientRating > 0 || (application?.ai_score || 0) > 0;
|
||||
if (!hasData) return null;
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-[#E5E7EB] bg-[#F8FAFC] p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="w-7 h-7 rounded-lg bg-[#0A39DF] flex items-center justify-center">
|
||||
<Shield className="w-4 h-4 text-white" strokeWidth={2.5} />
|
||||
</div>
|
||||
<h4 className="text-[12px] font-bold text-[#111827] tracking-wide flex items-center gap-1.5">
|
||||
WORKFORCE REPUTATION
|
||||
<TrendingUp className="w-3.5 h-3.5 text-[#0A39DF]" />
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<CareerScoreMini application={application} />
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-5 gap-2">
|
||||
<StatTile label="Reliability" value={reliability != null ? reliability : null} suffix="%" accent="#0A39DF" />
|
||||
<StatTile label="Attendance" value={attendance != null ? attendance : null} suffix="%" accent="#059669" />
|
||||
<StatTile label="Leadership" value={leadership != null ? leadership : null} suffix="%" accent="#0A39DF" />
|
||||
<StatTile label="Communication" value={commText} accent="#F59E0B" />
|
||||
<div className="flex flex-col items-center justify-center gap-1 px-2 py-3 rounded-xl bg-white/70 border border-[#E5E7EB]">
|
||||
<div className="flex items-center gap-0.5">
|
||||
<Star className="w-3.5 h-3.5 text-[#F9E547] fill-[#F9E547]" />
|
||||
<span className="text-[18px] font-bold leading-none text-[#111827]">
|
||||
{clientRating > 0 ? clientRating.toFixed(1) : '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-[#6B7280] tracking-wide font-medium">Client Rating</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{companies.length > 0 && (
|
||||
<div className="mt-3 pt-3 border-t border-[#E5E7EB]/80">
|
||||
<p className="text-[10px] font-bold text-[#9CA3AF] tracking-wider mb-2">WORKED</p>
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
{companies.map((c, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<div className={`w-7 h-7 rounded-lg ${colorFor(c)} flex items-center justify-center text-white text-[11px] font-bold shadow-sm`}>
|
||||
{initialsFor(c)}
|
||||
</div>
|
||||
<span className="text-[12px] font-semibold text-[#374151]">{c}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ReferencesEndorsements application={application} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user