Files
krow_talent_app/src/components/krow/WorkforceReputation.jsx
2026-08-17 17:33:56 +05:30

254 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import { Shield, Star, TrendingUp } from 'lucide-react';
import { Gauge, gaugeClasses } from '@mui/x-charts/Gauge';
import { Badge } from '@/components/ui/badge';
import { getScoreBand } from '@/lib/talentHome';
import ReferencesEndorsements from '@/components/krow/ReferencesEndorsements';
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';
}
/**
* The score, its band, and what both are worth — one block, read top to bottom.
*
* The gauge is the anchor rather than an ornament beside the numbers, so it is
* sized to be the first thing seen and carries the score inside it: label, value
* and range stacked in the arc's own empty middle, which is the only place where
* "834" and "300–850" read as one statement instead of two.
*
* Everything to the right is interpretation — where this person sits and where
* they go next — kept at a subordinate weight so the eye lands on the score
* first and travels outward.
*/
const GAUGE_W = 184;
function CareerScorePrimary({ application }) {
const score = careerScoreFromApp(application);
const band = getScoreBand(Math.round(application?.ai_score || 0));
return (
<div className="grid gap-x-7 gap-y-5 sm:grid-cols-[auto_minmax(0,1fr)] sm:items-center">
{/* Layer 1 — the primary signal */}
<div className="flex flex-col items-center gap-2.5">
<div
className="relative shrink-0"
style={{ width: GAUGE_W, height: Math.round(GAUGE_W * 0.577) }}
>
<Gauge
value={score}
valueMin={FICO_MIN}
valueMax={FICO_MAX}
startAngle={-90}
endAngle={90}
innerRadius="89%"
outerRadius="100%"
cornerRadius="50%"
text={() => null}
skipAnimation
aria-hidden="true"
sx={{
width: GAUGE_W,
height: GAUGE_W,
marginTop: `-${Math.round(GAUGE_W * 0.215)}px`,
/* Brand blue for the score it earned, a hairline-weight neutral
for the rest of the range — the same pairing the rest of the
product uses for "achieved against available". */
[`& .${gaugeClasses.valueArc}`]: { fill: '#0838E0' },
[`& .${gaugeClasses.referenceArc}`]: { fill: 'hsl(var(--border))' },
}}
/>
{/* The arc's middle is empty by construction; the score belongs in it.
Stacked with room between each line so the label never crowds the
number it is labelling. */}
<div
className="pointer-events-none absolute inset-x-0 bottom-0 flex flex-col items-center justify-end pb-1"
role="img"
aria-label={`Career score ${score} of a ${FICO_MIN} to ${FICO_MAX} range. Level: ${band.label}.`}
>
<span className="text-[9px] font-semibold uppercase leading-none tracking-[0.08em] text-ink-4">
Career score
</span>
<span className="mt-1.5 font-heading text-[32px] font-bold leading-none tabular-nums text-krow-blue">
{score}
</span>
<span className="mt-1.5 text-[10px] font-medium leading-none tabular-nums text-ink-4">
{FICO_MIN}–{FICO_MAX}
</span>
</div>
</div>
{/* Layer 2 — the band this score falls in. A status marker, sized like
one: the colour is the band's own, carried by a dot rather than by a
filled pill the width of the column. */}
<Badge variant="outline" size="lg" className="gap-1.5">
<span
className="h-1.5 w-1.5 rounded-full"
style={{ backgroundColor: band.color }}
aria-hidden="true"
/>
{band.label}
</Badge>
</div>
{/* Layer 2 — what the score is worth, and what comes next. */}
<dl className="grid gap-x-6 gap-y-4 min-[420px]:grid-cols-2 sm:grid-cols-1 lg:grid-cols-2">
<div>
<dt className="text-[10px] font-semibold uppercase tracking-[0.06em] text-ink-4">
Potential market value
</dt>
<dd className="mt-1 font-heading text-body-lg font-bold text-ink-1">
{marketRangeFromApp(application)}
</dd>
</div>
<div>
<dt className="text-[10px] font-semibold uppercase tracking-[0.06em] text-ink-4">
Next promotion
</dt>
<dd className="mt-1 font-heading text-body font-semibold text-ink-1">
{nextTierFromApp(application)}
</dd>
</div>
</dl>
</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-[#0838E0]',
'bg-[#333F48]',
'bg-[#062BAF]',
];
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();
};
/**
* A supporting signal — evidence for the score, not a headline of its own.
*
* Label and value on one line, no border, no tile: five bordered boxes under the
* gauge read as five more metrics competing with it, which is exactly the
* hierarchy this card is meant to establish. Values share one ink colour so no
* single signal shouts louder than the rest.
*/
/** @param {any} props */
function SupportingSignal({ label, value, suffix = '', icon: Icon = null }) {
const shown = value != null && value !== '' ? value : '—';
return (
<div className="flex items-baseline justify-between gap-3">
<dt className="truncate text-caption text-ink-3">{label}</dt>
<dd className="flex shrink-0 items-center font-heading text-body-sm font-bold tabular-nums text-ink-1">
{Icon && <Icon className="mr-1 h-3 w-3 text-krow-yellow" fill="currentColor" aria-hidden="true" />}
<span>
{shown}
{suffix && shown !== '—' && <span className="text-caption font-semibold">{suffix}</span>}
</span>
</dd>
</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-border bg-surface px-4 py-4">
<div className="mb-4 flex items-center gap-2">
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-krow-blue">
<Shield className="h-4 w-4 text-white" strokeWidth={2.5} />
</div>
<h4 className="flex items-center gap-1.5 text-[12px] font-bold tracking-wide text-ink-1">
WORKFORCE REPUTATION
<TrendingUp className="h-3.5 w-3.5 text-krow-blue" />
</h4>
</div>
<CareerScorePrimary application={application} />
{/* Layer 3 — supporting evidence. A hairline is enough to separate it from
the score; another container would make it a second card. */}
<dl className="mt-4 grid grid-cols-1 gap-x-8 gap-y-2 border-t border-border pt-3 min-[420px]:grid-cols-2 lg:grid-cols-3">
<SupportingSignal label="Reliability" value={reliability} suffix="%" />
<SupportingSignal label="Attendance" value={attendance} suffix="%" />
<SupportingSignal label="Leadership" value={leadership} suffix="%" />
<SupportingSignal label="Communication" value={commText} />
<SupportingSignal
label="Client rating"
value={clientRating > 0 ? clientRating.toFixed(1) : null}
icon={Star}
/>
</dl>
{companies.length > 0 && (
<div className="mt-3 border-t border-border pt-3">
<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>
);
}