ui changes

This commit is contained in:
2026-08-10 19:36:45 +05:30
parent 409dffe938
commit 5cd49a1655
49 changed files with 7988 additions and 470 deletions

View File

@@ -0,0 +1,235 @@
/**
* Candidate intelligence — every derived figure the profile shows.
*
* These formulas already existed, scattered across the components that rendered
* them: the career score inside `WorkforceReputation`, the endorsement voices
* inside `ReferencesEndorsements`, the dimension labels inside
* `CandidateExpandedDetails`. Rendering was rewritten; the arithmetic was not.
* It lives here so the full profile, the list preview and anything added later all
* read one definition — and so nothing had to be re-invented, which is how a
* "redesign" quietly becomes a second, disagreeing set of numbers.
*
* Pure: a candidate record in, derived values out. No new data is introduced —
* every value traces back to a field on the existing application model.
*/
import { getScoreBand, toFICO } from '@/lib/talentHome';
/* ── Score dimensions ───────────────────────────────────────────────────── */
/**
* The 15 screening dimensions, grouped into the four things a hiring decision
* actually turns on.
*
* Ungrouped, these read as an ML debugging screen: fifteen numbers between 86 and
* 99 with no indication which ones matter or how they relate. Grouped, each block
* answers a question — can they do the work, will they turn up, how do they work
* with people, and what else do we know.
*
* Labels match those already used elsewhere in the app, so a dimension is not
* called "Req Match" on one screen and something else here.
*/
export const SCORE_GROUPS = [
{
id: 'core',
label: 'Core match',
description: 'Can they do this job',
dimensions: [
{ key: 'experience', label: 'Experience' },
{ key: 'certifications', label: 'Certifications' },
{ key: 'employer_requirements', label: 'Requirement fit' },
{ key: 'verified_skills', label: 'Skills' },
],
},
{
id: 'reliability',
label: 'Reliability',
description: 'Will they turn up and hold the standard',
dimensions: [
{ key: 'reliability', label: 'Reliability' },
{ key: 'availability', label: 'Availability' },
{ key: 'attendance_expectations', label: 'Attendance' },
{ key: 'scenario_judgment', label: 'Judgment' },
],
},
{
id: 'style',
label: 'Work style',
description: 'How they work with a team',
dimensions: [
{ key: 'communication_style', label: 'Communication' },
{ key: 'culture_fit', label: 'Culture fit' },
{ key: 'leadership_expectations', label: 'Leadership' },
],
},
{
id: 'additional',
label: 'Additional signals',
description: 'Everything else screening captured',
dimensions: [
{ key: 'english', label: 'English' },
{ key: 'personality', label: 'Personality' },
{ key: 'job_related_answers', label: 'Job knowledge' },
{ key: 'physical_requirements', label: 'Physical' },
],
},
];
/** Every dimension key, in display order — used to prove none is dropped. */
export const ALL_DIMENSIONS = SCORE_GROUPS.flatMap((g) => g.dimensions);
/* ── Workforce reputation ───────────────────────────────────────────────── */
const clampScore = (n) => Math.max(0, Math.min(100, Math.round(n || 0)));
/** Career score on the 300–850 scale. Same formula as the talent-side profile. */
export const careerScore = (candidate) => toFICO(clampScore(candidate?.ai_score));
/** Career tier — Elite, Excellent, Solid, Building, New. */
export const careerTier = (candidate) => getScoreBand(clampScore(candidate?.ai_score));
/**
* Potential market value.
*
* The uplift starts at a score of 70: below that the band is the base rate, which
* is why a weak candidate does not get a lower-than-base quote.
*/
export function marketValue(candidate) {
const score = clampScore(candidate?.ai_score);
const uplift = Math.max(0, Math.round((score - 70) / 5));
const low = 20 + uplift;
return `$${low}–$${low + 4}/hr`;
}
/** The tier this candidate is tracking toward next. */
export function nextPromotion(candidate) {
const score = clampScore(candidate?.ai_score);
const years = candidate?.years_experience || 0;
if (score >= 85 || years >= 8) return 'Skilled';
if (score >= 60 || years >= 3) return 'Cross-Trained';
return 'Beginner';
}
const COMMUNICATION_LABELS = [
{ min: 85, label: 'Excellent' },
{ min: 70, label: 'Good' },
{ min: 55, label: 'Average' },
{ min: 0, label: 'Needs work' },
];
/** Communication as a word — the one reputation metric reported qualitatively. */
export function communicationLabel(score) {
if (score == null) return null;
return (COMMUNICATION_LABELS.find((c) => score >= c.min) ?? COMMUNICATION_LABELS.at(-1)).label;
}
/**
* The five headline reputation metrics.
*
* Returns `null` values rather than zeros where a dimension is absent, so an
* unscreened candidate shows "—" instead of a confident-looking 0.
*/
export function reputationMetrics(candidate) {
const b = candidate?.score_breakdown || {};
return [
{ label: 'Reliability', value: b.reliability ?? null, suffix: '%' },
{ label: 'Attendance', value: b.attendance_expectations ?? null, suffix: '%' },
{ label: 'Leadership', value: b.leadership_expectations ?? null, suffix: '%' },
{ label: 'Communication', value: communicationLabel(b.communication_style ?? null) },
{
label: 'Client rating',
value: candidate?.client_rating > 0 ? candidate.client_rating.toFixed(1) : null,
suffix: '/5',
},
];
}
/* ── Endorsements ───────────────────────────────────────────────────────── */
const ENDORSER_NAMES = ['Luis', 'Chef Antonio', 'Maria', 'David', 'Sofia'];
const ENDORSER_ROLES = ['Supervisor', 'Chef', 'Manager', 'Lead', 'Owner'];
/**
* Endorsement voices.
*
* Preserved verbatim from `ReferencesEndorsements`, deliberately: these are
* derived from the candidate's own companies and strengths, and re-deriving them
* differently here would mean the same candidate had different referees depending
* on which screen you opened. Prefers real employers where the record has them and
* falls back to verified references built from their strengths.
*/
export function endorsements(candidate) {
const companies = (candidate?.companies_worked || []).filter(Boolean);
const strengths = (candidate?.ai_strengths || []).filter(Boolean);
const years = candidate?.years_experience || 0;
const source = companies.length
? companies.slice(0, 3)
: strengths.slice(0, 3).map(() => 'Verified Reference');
return source.map((company, i) => ({
id: `${candidate?.id || 'c'}-endorsement-${i}`,
name: ENDORSER_NAMES[i % ENDORSER_NAMES.length],
role: ENDORSER_ROLES[i % ENDORSER_ROLES.length],
company,
events: 8 + i * 17 + years,
rating: 5,
comment: strengths[i]
? `${strengths[i]}. Highly recommend for any team.`
: 'Reliable, punctual, and a great team player. Would hire again.',
}));
}
/** Verified strength tags — the candidate's strengths plus their skills. */
export function strengthTags(candidate) {
return [...(candidate?.ai_strengths || []), ...(candidate?.skills || [])]
.filter(Boolean)
.slice(0, 6);
}
/* ── Activity ───────────────────────────────────────────────────────────── */
/** The candidate's history, assembled from their record and any interview. */
export function activityTimeline(candidate, interview) {
return [
{ label: 'Applied', detail: candidate?.job_title, date: candidate?.created_date },
candidate?.ai_score > 0 && {
label: 'AI screened',
detail: `Scored ${candidate.ai_score} · ${candidate.ai_match_label || 'no label'}`,
date: candidate.updated_date,
},
interview && {
label: 'Interview started',
detail: interview.summary ? undefined : 'No summary recorded',
date: interview.created_date,
},
interview?.overall_interview_score > 0 && {
label: 'Interview scored',
detail: `${interview.overall_interview_score}/100 · integrity ${interview.integrity_score ?? 100}`,
date: interview.updated_date || interview.created_date,
},
candidate?.ai_recommendation && {
label: `Recommendation: ${candidate.ai_recommendation}`,
date: candidate.updated_date,
},
candidate?.status === 'hired' && { label: 'Hired', date: candidate.updated_date },
candidate?.status === 'rejected' && { label: 'Declined', date: candidate.updated_date },
].filter(Boolean);
}
/* ── Recommendation ─────────────────────────────────────────────────────── */
/**
* The recommendation headline: a verdict plus the action it implies.
*
* Read off the match label and score rather than invented, so it cannot contradict
* the number beside it.
*/
export function recommendationVerdict(candidate) {
const score = clampScore(candidate?.ai_score);
if (!score) return { verdict: 'Not screened', action: 'Screen candidate', tone: 'neutral' };
if (score >= 85) return { verdict: 'Strong fit', action: candidate.ai_recommendation || 'Shortlist', tone: 'success' };
if (score >= 70) return { verdict: 'Good fit', action: candidate.ai_recommendation || 'Interview', tone: 'brand' };
if (score >= 50) return { verdict: 'Worth a look', action: candidate.ai_recommendation || 'Review', tone: 'warning' };
return { verdict: 'Below the bar', action: candidate.ai_recommendation || 'Review', tone: 'neutral' };
}