update hired page
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import {
|
||||
Activity, AlertTriangle, ArrowLeftRight, BellRing, Eye, Filter, Gauge, GitBranch,
|
||||
Layers, Lightbulb, ScrollText, ShieldAlert, Target, TrendingUp, UserCheck, Users,
|
||||
Activity, AlertTriangle, ArrowLeftRight, Award, BellRing, Clock, Eye, Filter, Gauge,
|
||||
GitBranch, Layers, Lightbulb, Lock, ScrollText, ShieldAlert, ShieldCheck, Star, Target,
|
||||
TrendingUp, UserCheck, Users,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
actions, answer, doc, funnel, heading, insights, kpis, list, meters, note, status, table,
|
||||
text, timeline,
|
||||
} from '../blocks';
|
||||
import { plural, verb } from '../insights';
|
||||
import { pct, plural, verb } from '../insights';
|
||||
import { toFICO } from '@/lib/talentHome';
|
||||
|
||||
/**
|
||||
* Admin capabilities — Control Center, Candidates Analysis, Activity.
|
||||
@@ -1351,3 +1353,463 @@ export const ACTIVITY_CAPABILITIES = [
|
||||
{ id: 'unusual-activity', label: 'Unusual Activity', icon: AlertTriangle, run: unusualActivity },
|
||||
{ id: 'security-insights', label: 'Security Insights', icon: ShieldAlert, run: securityInsights },
|
||||
];
|
||||
|
||||
/* ── KROW Forge ─────────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Forge capabilities — workforce skill verification.
|
||||
*
|
||||
* The Forge questions are not hiring questions: they are about proof. What is
|
||||
* verified, what can be proven next, and what is gating the rest. Every figure
|
||||
* comes from `f.forge`, which is derived with the same unlock gate the page
|
||||
* renders from.
|
||||
*/
|
||||
|
||||
const forgeProgress = (f) => {
|
||||
const g = f.forge;
|
||||
return doc(
|
||||
heading('Forge progress', `${plural(g.courses.length, 'challenge')} in the library`),
|
||||
kpis([
|
||||
{ label: 'Verified skills', value: g.verified.length, sub: g.verified.length ? 'proven' : 'nothing proven yet' },
|
||||
{ label: 'In progress', value: g.inProgress.length, sub: 'challenges started' },
|
||||
{ label: 'Ready to start', value: g.ready.length, sub: 'unlocked now' },
|
||||
{ label: 'XP available', value: g.xpAvailable, sub: `across ${plural(g.courses.length - g.completed.length, 'challenge')}` },
|
||||
]),
|
||||
g.verified.length
|
||||
? list(g.verified.map((b) => `**${b.name}** — ${b.level || 'Standard'}, verified`))
|
||||
: text('No skill is verified yet. The first one comes from completing a challenge and submitting proof for Owliver to grade.'),
|
||||
g.byDifficulty.length ? heading('Library by difficulty') : null,
|
||||
meters(g.byDifficulty.map((d) => ({
|
||||
label: d.level.charAt(0).toUpperCase() + d.level.slice(1),
|
||||
value: d.count,
|
||||
max: g.courses.length,
|
||||
}))),
|
||||
g.locked.length
|
||||
? note(`${plural(g.locked.length, 'challenge')} ${verb(g.locked.length, 'is', 'are')} still gated by real-work requirements.`)
|
||||
: null
|
||||
);
|
||||
};
|
||||
|
||||
const forgeNextChallenge = (f) => {
|
||||
const g = f.forge;
|
||||
if (!g.next) return answer('Every active challenge is complete — there is nothing left to start.');
|
||||
|
||||
const c = g.next;
|
||||
const gate = g.blocked.find((b) => b.course.id === c.id);
|
||||
|
||||
return doc(
|
||||
heading(c.title, `${c.proof_skill || c.category} · ${c.difficulty}`),
|
||||
text(c.description),
|
||||
kpis([
|
||||
{ label: 'Duration', value: `${c.estimated_minutes} min` },
|
||||
{ label: 'XP', value: c.xp },
|
||||
{ label: 'Earns', value: c.badge_reward || '—' },
|
||||
{ label: 'State', value: gate ? 'Locked' : 'Ready', tone: gate ? 'warning' : 'success' },
|
||||
]),
|
||||
gate
|
||||
? insights([{
|
||||
tone: 'warning',
|
||||
title: 'Blocked for now',
|
||||
body: `${gate.reasons.join('. ')}. Those are real-work requirements, so they clear through shifts rather than through study.`,
|
||||
}])
|
||||
: insights([{
|
||||
tone: 'success',
|
||||
title: 'Ready to start',
|
||||
body: `Nothing is gating this one. ${plural(c.estimated_minutes, 'minute')} of work is worth ${c.xp} XP${c.badge_reward ? ` and the ${c.badge_reward} badge` : ''}.`,
|
||||
}]),
|
||||
g.ready.length > 1
|
||||
? actions(g.ready.slice(1, 3).map((alt) => ({
|
||||
title: alt.title,
|
||||
body: `${alt.difficulty} · ${alt.estimated_minutes} min · ${alt.xp} XP. An alternative if this one does not fit today.`,
|
||||
})))
|
||||
: null
|
||||
);
|
||||
};
|
||||
|
||||
const forgeBlockers = (f) => {
|
||||
const g = f.forge;
|
||||
if (!g.locked.length) {
|
||||
return answer(`Nothing is locked — all ${plural(g.ready.length, 'remaining challenge')} can be started now.`);
|
||||
}
|
||||
|
||||
return doc(
|
||||
heading(`${plural(g.locked.length, 'challenge')} blocked`, 'and what clears each one'),
|
||||
table(
|
||||
[
|
||||
{ key: 'challenge', label: 'Challenge' },
|
||||
{ key: 'skill', label: 'Skill' },
|
||||
{ key: 'requirement', label: 'Blocked by' },
|
||||
],
|
||||
g.blocked.map(({ course, reasons }) => ({
|
||||
challenge: course.title,
|
||||
skill: course.proof_skill || course.category,
|
||||
requirement: reasons[0] || '—',
|
||||
}))
|
||||
),
|
||||
insights(g.blocked.slice(0, 2).map(({ course, reasons }) => ({
|
||||
tone: 'warning',
|
||||
title: course.title,
|
||||
body: `${reasons.join('. ')}. Worth ${course.xp} XP${course.badge_reward ? ` and the ${course.badge_reward} badge` : ''} once it opens.`,
|
||||
}))),
|
||||
note('Requirements are measured against the profile record, so they update as shifts and reliability are logged.')
|
||||
);
|
||||
};
|
||||
|
||||
const forgeReadyNow = (f) => {
|
||||
const g = f.forge;
|
||||
if (!g.ready.length) {
|
||||
return doc(
|
||||
text('Nothing is unlocked right now.'),
|
||||
g.locked.length ? text(`${plural(g.locked.length, 'challenge')} ${verb(g.locked.length, 'is', 'are')} waiting on real-work requirements.`) : null
|
||||
);
|
||||
}
|
||||
|
||||
return doc(
|
||||
heading(`${plural(g.ready.length, 'challenge')} ready to start`),
|
||||
table(
|
||||
[
|
||||
{ key: 'challenge', label: 'Challenge' },
|
||||
{ key: 'difficulty', label: 'Difficulty' },
|
||||
{ key: 'minutes', label: 'Min', align: 'right' },
|
||||
{ key: 'xp', label: 'XP', align: 'right' },
|
||||
],
|
||||
g.ready.map((c) => ({
|
||||
challenge: c.title,
|
||||
difficulty: c.difficulty,
|
||||
minutes: c.estimated_minutes,
|
||||
xp: c.xp,
|
||||
}))
|
||||
),
|
||||
text(
|
||||
`Cheapest first: **${[...g.ready].sort((a, b) => a.estimated_minutes - b.estimated_minutes)[0].title}**. `
|
||||
+ `Highest value: **${[...g.ready].sort((a, b) => b.xp - a.xp)[0].title}**.`
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const forgeSkillMap = (f) => {
|
||||
const g = f.forge;
|
||||
const skills = g.courses.map((c) => ({
|
||||
skill: c.proof_skill || c.category || 'Workforce skill',
|
||||
state: g.verified.some((b) => b.course_id === c.id)
|
||||
? 'Verified'
|
||||
: g.locked.some((l) => l.id === c.id) ? 'Locked' : 'Ready',
|
||||
xp: c.xp,
|
||||
}));
|
||||
|
||||
return doc(
|
||||
heading('Skills this library can verify', plural(skills.length, 'skill')),
|
||||
table(
|
||||
[
|
||||
{ key: 'skill', label: 'Skill' },
|
||||
{ key: 'state', label: 'State' },
|
||||
{ key: 'xp', label: 'XP', align: 'right' },
|
||||
],
|
||||
skills
|
||||
),
|
||||
text(
|
||||
g.verified.length
|
||||
? `${plural(g.verified.length, 'skill')} already carries evidence an employer can read.`
|
||||
: 'None of these carry evidence yet. Each verified skill adds a record an employer can read against the profile.'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/* ── Talent pool ────────────────────────────────────────────────────────── */
|
||||
|
||||
const talentPoolSummary = (f) => {
|
||||
const t = f.talent;
|
||||
return doc(
|
||||
heading('Talent pool', `${plural(f.profiles.length, 'registered worker')}`),
|
||||
kpis([
|
||||
{ label: 'Profiles', value: f.profiles.length, sub: `${t.available.length} available` },
|
||||
{ label: 'Elite', value: f.elite.length, sub: '90+ career score' },
|
||||
{ label: 'Average score', value: t.avgScore ? toFICO(t.avgScore) : '—', sub: 'scored profiles only' },
|
||||
{ label: 'Unscored', value: f.unscoredProfiles.length, sub: 'cannot be ranked', tone: f.unscoredProfiles.length ? 'warning' : undefined },
|
||||
]),
|
||||
heading('Score bands'),
|
||||
meters(t.bands.map((b) => ({ label: b.label, value: b.count, max: f.profiles.length }))),
|
||||
text(
|
||||
f.unscoredProfiles.length
|
||||
? `${plural(f.unscoredProfiles.length, 'profile')} ${verb(f.unscoredProfiles.length, 'has', 'have')} no career score, so ${verb(f.unscoredProfiles.length, 'it is', 'they are')} invisible to ranking and matching.`
|
||||
: 'Every profile carries a career score, so the whole pool is rankable.'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const talentPriorities = (f) => {
|
||||
const t = f.talent;
|
||||
if (!f.profiles.length) return answer('No talent profiles registered yet.');
|
||||
|
||||
return doc(
|
||||
heading('Who to prioritize', 'by career score, availability and verification'),
|
||||
table(
|
||||
[
|
||||
{ key: 'name', label: 'Worker' },
|
||||
{ key: 'score', label: 'Score', align: 'right' },
|
||||
{ key: 'experience', label: 'Years', align: 'right' },
|
||||
{ key: 'availability', label: 'Available' },
|
||||
],
|
||||
t.ranked.slice(0, 6).map((p) => ({
|
||||
name: p.full_name || p.email,
|
||||
/* The page shows career score on the 300–850 scale, so this must too —
|
||||
the same worker cannot be 94 here and 817 in the row beside it. */
|
||||
score: p.krow_score ? toFICO(p.krow_score) : '—',
|
||||
experience: p.experience_years ?? '—',
|
||||
availability: (p.availability || []).length ? (p.availability || []).join(', ') : '—',
|
||||
}))
|
||||
),
|
||||
insights([
|
||||
t.top[0] && {
|
||||
tone: 'success',
|
||||
title: `${t.top[0].full_name || t.top[0].email} leads the pool`,
|
||||
body: `Career score ${toFICO(t.top[0].krow_score || 0)}${t.top[0].experience_years ? `, ${plural(t.top[0].experience_years, 'year')} of experience` : ''}.`,
|
||||
},
|
||||
t.available.length < f.profiles.length && {
|
||||
tone: 'info',
|
||||
title: `${t.available.length} of ${f.profiles.length} list availability`,
|
||||
body: 'The rest cannot be matched to a shift until availability is on the record.',
|
||||
},
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
const talentVerification = (f) => {
|
||||
const t = f.talent;
|
||||
if (!t.unverified.length) {
|
||||
return answer(`All ${plural(f.profiles.length, 'profile')} carry at least one verified skill.`);
|
||||
}
|
||||
|
||||
return doc(
|
||||
heading(`${plural(t.unverified.length, 'profile')} without verified skills`),
|
||||
table(
|
||||
[
|
||||
{ key: 'name', label: 'Worker' },
|
||||
{ key: 'score', label: 'Score', align: 'right' },
|
||||
{ key: 'skills', label: 'Claimed skills', align: 'right' },
|
||||
],
|
||||
t.unverified.slice(0, 8).map((p) => ({
|
||||
name: p.full_name || p.email,
|
||||
score: p.krow_score ? toFICO(p.krow_score) : '—',
|
||||
skills: (p.skills || []).length,
|
||||
}))
|
||||
),
|
||||
insights([{
|
||||
tone: 'warning',
|
||||
title: 'Claimed is not proven',
|
||||
body: 'These profiles carry skills nobody has verified. A Forge challenge turns each claim into evidence an employer can read.',
|
||||
}]),
|
||||
note(`${pct(t.unverified.length, f.profiles.length)}% of the pool is unverified.`)
|
||||
);
|
||||
};
|
||||
|
||||
const talentAvailability = (f) => {
|
||||
const t = f.talent;
|
||||
const buckets = Object.values(f.profiles.reduce((acc, p) => {
|
||||
(p.availability || ['Not stated']).forEach((slot) => {
|
||||
acc[slot] ||= { slot, count: 0 };
|
||||
acc[slot].count += 1;
|
||||
});
|
||||
return acc;
|
||||
}, {})).sort((a, b) => b.count - a.count);
|
||||
|
||||
return doc(
|
||||
heading('Availability across the pool', `${t.available.length} of ${f.profiles.length} stated`),
|
||||
meters(buckets.map((b) => ({ label: b.slot, value: b.count, max: f.profiles.length }))),
|
||||
text(
|
||||
buckets[0]
|
||||
? `Deepest cover is **${buckets[0].slot}** at ${plural(buckets[0].count, 'worker')}. Thinnest is **${buckets[buckets.length - 1].slot}**.`
|
||||
: 'No availability recorded yet.'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/* ── Hired history ──────────────────────────────────────────────────────── */
|
||||
|
||||
const hiringOutcomes = (f) => {
|
||||
const h = f.hiring;
|
||||
if (!h.hires.length) return answer('Nobody has been hired yet, so there is no history to read.');
|
||||
|
||||
return doc(
|
||||
heading('Hiring outcomes', plural(h.hires.length, 'hire')),
|
||||
kpis([
|
||||
{ label: 'Hires', value: h.hires.length, sub: `${h.onboarding.length} onboarding` },
|
||||
{ label: 'Average quality', value: h.avgQuality || '—', sub: 'AI score at hire' },
|
||||
{ label: 'Average speed', value: h.avgSpeed ? `${h.avgSpeed}d` : '—', sub: 'apply to hire' },
|
||||
{ label: 'Rated', value: `${h.rated.length}/${h.hires.length}`, sub: 'client rating on file', tone: h.unrated.length ? 'warning' : undefined },
|
||||
]),
|
||||
heading('By department'),
|
||||
table(
|
||||
[
|
||||
{ key: 'name', label: 'Department' },
|
||||
{ key: 'count', label: 'Hires', align: 'right' },
|
||||
{ key: 'avgScore', label: 'Avg score', align: 'right' },
|
||||
{ key: 'avgDays', label: 'Avg days', align: 'right' },
|
||||
],
|
||||
h.byDepartment.map((d) => ({
|
||||
name: d.name,
|
||||
count: d.count,
|
||||
avgScore: d.avgScore || '—',
|
||||
avgDays: d.avgDays || '—',
|
||||
}))
|
||||
),
|
||||
h.unrated.length
|
||||
? note(`${plural(h.unrated.length, 'hire')} ${verb(h.unrated.length, 'has', 'have')} no client rating yet, so outcome quality rests on the ${h.rated.length} that do.`)
|
||||
: null
|
||||
);
|
||||
};
|
||||
|
||||
const hiringStrongest = (f) => {
|
||||
const h = f.hiring;
|
||||
if (!h.hires.length) return answer('No hires on record yet.');
|
||||
|
||||
const ranked = [...h.hires].sort((a, b) => (b.score || 0) - (a.score || 0));
|
||||
|
||||
return doc(
|
||||
heading('Strongest hires', 'by score at the point of hire'),
|
||||
table(
|
||||
[
|
||||
{ key: 'name', label: 'Hire' },
|
||||
{ key: 'role', label: 'Role' },
|
||||
{ key: 'department', label: 'Department' },
|
||||
{ key: 'score', label: 'Score', align: 'right' },
|
||||
{ key: 'rating', label: 'Rating', align: 'right' },
|
||||
],
|
||||
ranked.slice(0, 6).map((x) => ({
|
||||
name: x.name,
|
||||
role: x.role,
|
||||
department: x.department,
|
||||
score: x.score || '—',
|
||||
rating: x.client_rating || '—',
|
||||
}))
|
||||
),
|
||||
insights([
|
||||
h.strongest && {
|
||||
tone: 'success',
|
||||
title: `${h.strongest.name} scored highest at ${h.strongest.score}`,
|
||||
body: `${h.strongest.role} in ${h.strongest.department}${h.strongest.client_rating ? `, rated ${h.strongest.client_rating} since` : ', not yet rated by the client'}.`,
|
||||
},
|
||||
h.byRoleTitle[0] && {
|
||||
tone: 'info',
|
||||
title: `${h.byRoleTitle[0].name} is the most-hired role`,
|
||||
body: `${plural(h.byRoleTitle[0].count, 'hire')}, averaging ${h.byRoleTitle[0].avgScore || 0} at hire.`,
|
||||
},
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
const hiringPatterns = (f) => {
|
||||
const h = f.hiring;
|
||||
if (!h.hires.length) return answer('No hires on record, so there is no pattern to read.');
|
||||
|
||||
const fastest = [...h.byRoleTitle].filter((r) => r.avgDays).sort((a, b) => a.avgDays - b.avgDays)[0];
|
||||
const slowest = [...h.byRoleTitle].filter((r) => r.avgDays).sort((a, b) => b.avgDays - a.avgDays)[0];
|
||||
|
||||
return doc(
|
||||
heading('What stands out', `${plural(h.hires.length, 'hire')} across ${plural(h.byDepartment.length, 'department')}`),
|
||||
meters(h.byDepartment.map((d) => ({
|
||||
label: d.name,
|
||||
value: d.count,
|
||||
max: Math.max(...h.byDepartment.map((x) => x.count), 1),
|
||||
}))),
|
||||
insights([
|
||||
h.byDepartment[0] && {
|
||||
tone: 'info',
|
||||
title: `${h.byDepartment[0].name} accounts for the most hires`,
|
||||
body: `${plural(h.byDepartment[0].count, 'hire')} of ${h.hires.length} — ${pct(h.byDepartment[0].count, h.hires.length)}% of everyone placed.`,
|
||||
},
|
||||
fastest && slowest && fastest.name !== slowest.name && {
|
||||
tone: 'warning',
|
||||
title: `${slowest.name} takes ${slowest.avgDays - fastest.avgDays} days longer than ${fastest.name}`,
|
||||
body: `${slowest.avgDays} days against ${fastest.avgDays}. The gap is where the process, not the candidate, is the constraint.`,
|
||||
},
|
||||
h.unrated.length && {
|
||||
tone: 'warning',
|
||||
title: `${plural(h.unrated.length, 'hire')} without a client rating`,
|
||||
body: 'Outcome quality cannot be judged on hires nobody has rated.',
|
||||
},
|
||||
]),
|
||||
actions([
|
||||
h.unrated.length ? { title: 'Chase the missing ratings', body: `${plural(h.unrated.length, 'placement')} ${verb(h.unrated.length, 'has', 'have')} no client feedback on file.` } : null,
|
||||
h.avgSpeed ? { title: 'Compare speed against quality', body: `Average ${h.avgSpeed} days to hire at an average score of ${h.avgQuality}. Faster is only better while the score holds.` } : null,
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
const hiringRecentHires = (f) => {
|
||||
const h = f.hiring;
|
||||
if (!h.hires.length) return answer('No hires on record yet.');
|
||||
|
||||
const recent = [...h.hires]
|
||||
.filter((x) => x.hire_date)
|
||||
.sort((a, b) => new Date(b.hire_date).getTime() - new Date(a.hire_date).getTime())
|
||||
.slice(0, 6);
|
||||
|
||||
return doc(
|
||||
heading('Recent hires', 'most recent first'),
|
||||
timeline(recent.map((x) => ({
|
||||
title: `${x.name} — ${x.role}`,
|
||||
description: `${x.department}${x.score ? ` · scored ${x.score}` : ''}${x.timeToHire ? ` · ${plural(x.timeToHire, 'day')} to hire` : ''}`,
|
||||
timestamp: x.hire_date,
|
||||
tone: x.status === 'onboarding' ? 'warning' : 'success',
|
||||
}))),
|
||||
h.onboarding.length
|
||||
? note(`${plural(h.onboarding.length, 'hire')} ${verb(h.onboarding.length, 'is', 'are')} still onboarding.`)
|
||||
: null
|
||||
);
|
||||
};
|
||||
|
||||
/* ── Free-text responders for the three new contexts ────────────────────── */
|
||||
|
||||
export const respondForge = (question, f) => {
|
||||
const q = question.toLowerCase();
|
||||
if (has(q, 'block', 'locked', 'lock', 'requirement', 'prerequisite', 'why can', 'unlock')) return forgeBlockers(f);
|
||||
if (has(q, 'next', 'should i', 'which challenge', 'recommend', 'best')) return forgeNextChallenge(f);
|
||||
if (has(q, 'ready', 'start now', 'available', 'can i start', 'active challenge')) return forgeReadyNow(f);
|
||||
if (has(q, 'skill', 'verified', 'proof', 'evidence', 'badge')) return forgeSkillMap(f);
|
||||
if (has(q, 'summar', 'progress', 'how am i', 'xp', 'overview')) return forgeProgress(f);
|
||||
return forgeProgress(f);
|
||||
};
|
||||
|
||||
export const respondTalentPool = (question, f) => {
|
||||
const q = question.toLowerCase();
|
||||
if (has(q, 'verif', 'unverified', 'proof', 'evidence', 'badge')) return talentVerification(f);
|
||||
if (has(q, 'available', 'availability', 'shift', 'weekend', 'evening')) return talentAvailability(f);
|
||||
if (has(q, 'priorit', 'strongest', 'best', 'top', 'who should')) return talentPriorities(f);
|
||||
if (has(q, 'unscored', 'no score', 'missing', 'gap')) return talentPoolSummary(f);
|
||||
if (has(q, 'summar', 'overview', 'pool', 'how many')) return talentPoolSummary(f);
|
||||
return talentPriorities(f);
|
||||
};
|
||||
|
||||
export const respondHiredHistory = (question, f) => {
|
||||
const q = question.toLowerCase();
|
||||
if (has(q, 'pattern', 'stand out', 'trend', 'insight', 'attention', 'concern')) return hiringPatterns(f);
|
||||
if (has(q, 'strongest', 'best', 'top', 'quality', 'highest')) return hiringStrongest(f);
|
||||
if (has(q, 'recent', 'latest', 'summar', 'who did we hire')) return hiringRecentHires(f);
|
||||
if (has(q, 'department', 'position', 'role', 'team', 'most hires')) return hiringOutcomes(f);
|
||||
return hiringOutcomes(f);
|
||||
};
|
||||
|
||||
/* ── Capability manifests for the three new contexts ────────────────────── */
|
||||
|
||||
export const FORGE_CAPABILITIES = [
|
||||
{ id: 'forge-progress', label: 'Forge Progress', icon: Gauge, run: forgeProgress },
|
||||
{ id: 'forge-next', label: 'Next Challenge', icon: Target, run: forgeNextChallenge },
|
||||
{ id: 'forge-ready', label: 'Ready To Start', icon: UserCheck, run: forgeReadyNow },
|
||||
{ id: 'forge-blockers', label: 'What Is Blocking Me', icon: Lock, run: forgeBlockers },
|
||||
{ id: 'forge-skills', label: 'Skill Map', icon: ShieldCheck, run: forgeSkillMap },
|
||||
];
|
||||
|
||||
export const TALENT_POOL_CAPABILITIES = [
|
||||
{ id: 'talent-priorities', label: 'Who To Prioritize', icon: Star, run: talentPriorities },
|
||||
{ id: 'talent-summary', label: 'Pool Summary', icon: Users, run: talentPoolSummary },
|
||||
{ id: 'talent-verification', label: 'Verification Gaps', icon: ShieldCheck, run: talentVerification },
|
||||
{ id: 'talent-availability', label: 'Availability', icon: Clock, run: talentAvailability },
|
||||
];
|
||||
|
||||
export const HIRED_HISTORY_CAPABILITIES = [
|
||||
{ id: 'hiring-outcomes', label: 'Hiring Outcomes', icon: Award, run: hiringOutcomes },
|
||||
{ id: 'hiring-strongest', label: 'Strongest Hires', icon: TrendingUp, run: hiringStrongest },
|
||||
{ id: 'hiring-patterns', label: 'What Stands Out', icon: Layers, run: hiringPatterns },
|
||||
{ id: 'hiring-recent', label: 'Recent Hires', icon: ScrollText, run: hiringRecentHires },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user