update ui changes

This commit is contained in:
2026-08-10 19:51:49 +05:30
parent 5cd49a1655
commit 7427359967
13 changed files with 388 additions and 271 deletions

View File

@@ -1,6 +1,6 @@
import {
Activity, AlertTriangle, ArrowLeftRight, BellRing, Eye, Filter, Gauge, GitBranch,
Lightbulb, ScrollText, ShieldAlert, TrendingUp, Users,
Lightbulb, ScrollText, ShieldAlert, Target, TrendingUp, UserCheck, Users,
} from 'lucide-react';
import {
actions, answer, doc, funnel, heading, insights, kpis, list, meters, note, status, table,
@@ -871,6 +871,234 @@ const securityInsights = (f) => {
);
};
/* ── Positions ──────────────────────────────────────────────────────────── */
/**
* Position strength — which roles are actually working.
*
* Ranked by qualified conversion rather than raw applicants: a role with twelve
* applications and nobody above the bar is in worse shape than one with two who
* both clear it, and sorting by volume hides exactly that.
*/
const positionStrength = (f) => {
if (!f.openPositions.length) return answer('There are no open positions to compare.');
const rows = f.byRole
.map((r) => ({ ...r, rate: r.applied ? Math.round((r.qualified / r.applied) * 100) : 0 }))
.sort((a, b) => b.rate - a.rate || b.applied - a.applied);
const best = rows.find((r) => r.applied > 0);
return doc(
heading('Pipeline strength by position', `${plural(rows.length, 'open role')}`),
table(
[
{ key: 'title', label: 'Role' },
{ key: 'applied', label: 'Applied', align: 'right' },
{ key: 'qualified', label: '70+', align: 'right' },
{ key: 'rate', label: 'Qualified', align: 'right' },
],
rows.map((r) => ({
title: r.title,
applied: r.applied ? r.applied : { value: 0, tone: 'risk' },
qualified: r.qualified,
rate: { value: `${r.rate}%`, tone: r.rate >= 50 ? 'success' : r.rate > 0 ? 'warning' : 'risk' },
}))
),
insights([
best && {
tone: 'success',
title: `${best.title} has the strongest pipeline`,
body: `${best.qualified} of ${best.applied} applicants clear the bar — ${best.rate}%. If you are filling one role first, this is the one that can actually close.`,
},
f.starvedPositions.length && {
tone: 'risk',
title: `${plural(f.starvedPositions.length, 'role')} with no supply at all`,
body: `${f.starvedPositions.map((p) => p.title).join(', ')}. No amount of screening improves a role nobody has applied to.`,
},
])
);
};
/** Which roles need a person, and why. */
const positionsNeedingAttention = (f) => {
const items = [
f.starvedPositions.length && {
tone: 'risk',
title: `${plural(f.starvedPositions.length, 'role')} with no applicants`,
body: `${f.starvedPositions.map((p) => p.title).join(', ')}. Usually a pay band or distribution problem rather than a hiring one.`,
},
f.singleCandidateRoles.length && {
tone: 'warning',
title: `${plural(f.singleCandidateRoles.length, 'role')} resting on one viable candidate`,
body: `${f.singleCandidateRoles.map((e) => e.posting.title).join(', ')}. One withdrawal reopens the search.`,
},
f.unscreened.length && {
tone: 'warning',
title: `${plural(f.unscreened.length, 'applicant')} waiting on screening`,
body: 'Spread across the open roles. Until they are scored the funnels below them are incomplete.',
},
f.stalled.length && {
tone: 'risk',
title: `${plural(f.stalled.length, 'strong candidate')} not actioned`,
body: `${f.stalled.map((a) => a.applicant_name).join(', ')} scored 80 or above and ${verb(f.stalled.length, 'is', 'are')} still waiting.`,
},
].filter(Boolean);
if (!items.length) {
return answer('Every open role has candidates, everyone is screened, and no role depends on a single person. Nothing needs intervention.');
}
return doc(
heading(`${plural(items.length, 'position issue')}`, 'Ordered by cost of leaving it'),
insights(items),
note('Ranked on supply and screening state. A role can look healthy here and still be hard to fill for reasons the platform cannot see.')
);
};
/** Which role to fill first. */
const hiringPriority = (f) => {
const rows = f.byRole
.map((r) => ({
...r,
ready: r.qualified,
rate: r.applied ? Math.round((r.qualified / r.applied) * 100) : 0,
}))
.sort((a, b) => b.ready - a.ready || b.rate - a.rate);
if (!rows.length) return answer('No open positions to prioritise.');
const first = rows[0];
return doc(
heading('What to fill first'),
text(
first.ready
? `**${first.title}** — ${plural(first.ready, 'candidate')} already clear the bar, so this is the role where a decision converts into a hire today rather than starting a search.`
: 'No role currently has a candidate above the bar, so the first move is screening rather than hiring.'
),
heading('Ranked by candidates ready now'),
table(
[
{ key: 'title', label: 'Role' },
{ key: 'ready', label: 'Ready', align: 'right' },
{ key: 'applied', label: 'Applied', align: 'right' },
{ key: 'hired', label: 'Hired', align: 'right' },
],
rows.map((r) => ({
title: r.title,
ready: r.ready ? { value: r.ready, tone: 'success' } : '0',
applied: r.applied,
hired: r.hired || '—',
}))
)
);
};
/* ── Candidates list ────────────────────────────────────────────────────── */
/** Who is waiting on a decision, most consequential first. */
const candidatesNeedingAttention = (f) => {
const items = [
f.stalled.length && {
tone: 'risk',
title: `${plural(f.stalled.length, 'candidate')} at 80+ waiting on a decision`,
body: `${f.stalled.map((a) => a.applicant_name).join(', ')}. Screened, strong, and still sitting at AI Screened — the delay, not the assessment, is what loses these people.`,
},
f.unscreened.length && {
tone: 'warning',
title: `${plural(f.unscreened.length, 'candidate')} with no score`,
body: 'Cannot be ranked or compared until screening runs, so they are invisible to every sort on this page.',
},
f.missingCredentials.length && {
tone: 'warning',
title: `${plural(f.missingCredentials.length, 'scored candidate')} with no credentials`,
body: 'Compliance exposure if placed on a role that requires certification.',
},
f.narrowAvailability.length && {
tone: 'info',
title: `${plural(f.narrowAvailability.length, 'candidate')} with narrow availability`,
body: 'One slot or none recorded — the placements most likely to fall through on the day.',
},
].filter(Boolean);
if (!items.length) {
return answer('Nobody is waiting. Every candidate is scored, credentialed, and either actioned or already moving.');
}
return doc(
heading(`${plural(items.length, 'thing')} to look at`),
insights(items),
text(`Start with ${items[0].title.toLowerCase()} — it is the one where waiting costs the most.`)
);
};
/** Who is ready to interview right now. */
const interviewReady = (f) => {
const ready = f.ranked.filter(
(a) => a.ai_score >= 70 && ['ai_screened', 'shortlisted'].includes(a.status)
);
if (!ready.length) {
return answer('Nobody is interview-ready: no scored candidate above 70 is sitting at a stage where an interview is the next step.');
}
return doc(
heading('Ready to interview', `${plural(ready.length, 'candidate')} scoring 70 or above`),
table(
[
{ key: 'name', label: 'Candidate' },
{ key: 'role', label: 'Applied for' },
{ key: 'score', label: 'Score', align: 'right' },
{ key: 'stage', label: 'Stage', align: 'right' },
],
ready.map((a) => ({
name: a.applicant_name,
role: a.job_title,
score: { value: a.ai_score, tone: a.ai_score >= 80 ? 'success' : 'info' },
stage: { badge: String(a.status).replace(/_/g, ' '), tone: 'neutral' },
}))
),
note('Screened and above the bar. Availability still needs checking against the event calendar.')
);
};
/** The pipeline as it stands, by stage and score band. */
const candidatePipelineSummary = (f) => {
if (!f.total) return answer('No candidates in the pipeline yet.');
const band = (min, max) =>
f.scored.filter((a) => a.ai_score >= min && (max == null || a.ai_score < max)).length;
return doc(
heading('Candidate pipeline', `${plural(f.total, 'candidate')}`),
kpis([
{ label: 'Scored', value: f.scored.length, sub: `${f.standardizedPct}% coverage` },
{ label: 'At 80+', value: band(80), tone: 'success' },
{ label: 'Interviewing', value: f.interviewing.length },
{ label: 'Hired', value: f.hired.length, tone: 'info' },
]),
heading('By score band'),
table(
[{ key: 'band', label: 'Band' }, { key: 'count', label: 'Candidates', align: 'right' }],
[
{ band: 'Strong (80+)', count: { value: band(80), tone: 'success' } },
{ band: 'Good (60–79)', count: band(60, 80) },
{ band: 'Review (40–59)', count: { value: band(40, 60), tone: 'warning' } },
{ band: 'Low (under 40)', count: band(1, 40) },
{ band: 'Not scored', count: { value: f.unscreened.length, tone: 'risk' } },
]
),
heading('By stage'),
funnel(f.funnel.map((stage, i) => ({
label: stage.label,
count: stage.count,
rate: i === 0 ? null : f.transitions[i - 1]?.rate,
lost: i === 0 ? 0 : f.transitions[i - 1]?.lost,
})))
);
};
/* ── Free-text responders ───────────────────────────────────────────────── */
/**
@@ -893,6 +1121,27 @@ export const respondControlCenter = (question, f) => {
return platformHealth(f);
};
export const respondPositions = (question, f) => {
const q = question.toLowerCase();
if (has(q, 'attention', 'urgent', 'problem', 'wrong', 'risk')) return positionsNeedingAttention(f);
if (has(q, 'first', 'priorit', 'which position', 'should i hire')) return hiringPriority(f);
if (has(q, 'bottleneck', 'funnel', 'drop', 'conversion')) return pipelineHealth(f);
if (has(q, 'waiting', 'review', 'screen')) return candidatesNeedingAttention(f);
if (has(q, 'activity', 'summar', 'operation', 'velocity')) return hiringOperations(f);
return positionStrength(f);
};
export const respondCandidateList = (question, f) => {
const q = question.toLowerCase();
if (has(q, 'attention', 'waiting', 'need', 'stuck')) return candidatesNeedingAttention(f);
if (has(q, 'interview', 'ready')) return interviewReady(f);
if (has(q, 'no score', 'unscored', 'not scored', 'missing')) return screeningGaps(f);
if (has(q, 'risk', 'flag', 'concern', 'compliance')) return candidateRiskAnalysis(f);
if (has(q, 'strongest', 'best', 'top', 'compare')) return topCandidates(f);
if (has(q, 'summar', 'pipeline', 'overview')) return candidatePipelineSummary(f);
return topCandidates(f);
};
export const respondAdminCandidates = (question, f) => {
const q = question.toLowerCase();
if (has(q, 'gap', 'missing', 'incomplete', 'coverage', 'unscored')) return screeningGaps(f);
@@ -921,6 +1170,24 @@ export const CONTROL_CENTER_CAPABILITIES = [
{ id: 'recommendations', label: 'Recommendations', icon: Lightbulb, run: recommendations },
];
export const POSITIONS_CAPABILITIES = [
{ id: 'position-strength', label: 'Pipeline Strength', icon: TrendingUp, run: positionStrength },
{ id: 'positions-attention', label: 'Needs Attention', icon: BellRing, run: positionsNeedingAttention },
{ id: 'hiring-priority', label: 'What To Fill First', icon: Target, run: hiringPriority },
{ id: 'pipeline-health', label: 'Hiring Bottlenecks', icon: GitBranch, run: pipelineHealth },
{ id: 'hiring-operations', label: 'Hiring Activity', icon: Activity, run: hiringOperations },
{ id: 'candidates-waiting', label: 'Candidates Waiting', icon: Users, run: candidatesNeedingAttention },
];
export const CANDIDATE_LIST_CAPABILITIES = [
{ id: 'candidates-attention', label: 'Who Needs Attention', icon: BellRing, run: candidatesNeedingAttention },
{ id: 'top-candidates', label: 'Strongest Candidates', icon: ArrowLeftRight, run: topCandidates },
{ id: 'interview-ready', label: 'Interview Ready', icon: UserCheck, run: interviewReady },
{ id: 'screening-gaps', label: 'Unscored Candidates', icon: Filter, run: screeningGaps },
{ id: 'pipeline-summary', label: 'Pipeline Summary', icon: GitBranch, run: candidatePipelineSummary },
{ id: 'candidate-risk', label: 'Candidate Risks', icon: AlertTriangle, run: candidateRiskAnalysis },
];
export const ADMIN_CANDIDATE_CAPABILITIES = [
{ id: 'candidate-risk', label: 'Candidate Risk', icon: AlertTriangle, run: candidateRiskAnalysis },
{ id: 'recruitment-insights', label: 'Recruitment Insights', icon: TrendingUp, run: recruitmentInsights },