update hired page
This commit is contained in:
@@ -9,6 +9,9 @@
|
||||
* transport, no dependency on any AI product.
|
||||
*/
|
||||
|
||||
import { isUnlocked } from '@/lib/provingGround';
|
||||
import { getScoreBand } from '@/lib/talentHome';
|
||||
|
||||
export const pct = (n, d) => (d ? Math.round((n / d) * 100) : 0);
|
||||
|
||||
export const plural = (n, word, irregular) =>
|
||||
@@ -174,6 +177,107 @@ export function buildFacts({
|
||||
};
|
||||
})();
|
||||
|
||||
/* ── KROW Forge ──────────────────────────────────────────────────────────
|
||||
Derived with the same gate (`isUnlocked`) and the same completion record the
|
||||
Forge page renders from, so the panel and the page cannot disagree about
|
||||
what is ready, what is blocked, or why. `profile` is the signed-in worker's
|
||||
record when one is already in cache; without it the counts are the library's
|
||||
rather than the person's, which is still true — just less personal. */
|
||||
const forgeCourses = courses.filter((c) => c.status === 'active');
|
||||
const forgeCompletedIds = new Set((profile?.completed_courses || []).map((c) => c.course_id));
|
||||
const forgeGate = (course) => isUnlocked(course, profile || {});
|
||||
const forgeUnfinished = forgeCourses.filter((c) => !forgeCompletedIds.has(c.id));
|
||||
const forgeReady = forgeUnfinished.filter((c) => forgeGate(c).unlocked);
|
||||
const forgeLocked = forgeUnfinished.filter((c) => !forgeGate(c).unlocked);
|
||||
const forge = {
|
||||
courses: forgeCourses,
|
||||
verified: profile?.earned_badges || [],
|
||||
completed: (profile?.completed_courses || []).filter((c) => c.status !== 'in_progress'),
|
||||
inProgress: (profile?.completed_courses || []).filter((c) => c.status === 'in_progress'),
|
||||
ready: forgeReady,
|
||||
locked: forgeLocked,
|
||||
/* The page features the first unfinished challenge whether or not it is
|
||||
unlocked, so the panel names the same one — a chip that recommends a
|
||||
different challenge than the one on screen reads as a second opinion. */
|
||||
next: forgeUnfinished[0] || null,
|
||||
xpAvailable: forgeUnfinished.reduce((sum, c) => sum + (c.xp || 0), 0),
|
||||
/* Every blocked challenge with the reasons that block it, in page order. */
|
||||
blocked: forgeLocked.map((course) => ({ course, reasons: forgeGate(course).reasons })),
|
||||
byDifficulty: ['beginner', 'intermediate', 'advanced'].map((level) => ({
|
||||
level,
|
||||
count: forgeCourses.filter((c) => c.difficulty === level).length,
|
||||
})).filter((d) => d.count),
|
||||
};
|
||||
|
||||
/* ── Talent pool ─────────────────────────────────────────────────────────
|
||||
The same score bands the Talent Pool page labels profiles with. */
|
||||
const rankedProfiles = [...profiles].sort((a, b) => (b.krow_score || 0) - (a.krow_score || 0));
|
||||
const talent = {
|
||||
ranked: rankedProfiles,
|
||||
top: rankedProfiles.slice(0, 3),
|
||||
available: profiles.filter((p) => (p.availability || []).length > 0),
|
||||
unverified: profiles.filter(
|
||||
(p) => !(p.earned_badges || []).length
|
||||
&& !(p.capabilities || []).some((c) => c.status === 'verified')
|
||||
),
|
||||
experienced: profiles.filter((p) => (p.experience_years || 0) >= 3),
|
||||
avgScore: avg(profiles.filter((p) => p.krow_score > 0).map((p) => p.krow_score)),
|
||||
bands: Object.values(profiles.reduce((acc, p) => {
|
||||
const { label } = getScoreBand(p.krow_score || 0);
|
||||
acc[label] ||= { label, count: 0 };
|
||||
acc[label].count += 1;
|
||||
return acc;
|
||||
}, {})).sort((a, b) => b.count - a.count),
|
||||
};
|
||||
|
||||
/* ── Hired history ───────────────────────────────────────────────────────
|
||||
Staff joined to the application and posting they came from — the same join
|
||||
the Hired History page performs, so department, speed and score match. */
|
||||
const hires = staff.map((s) => {
|
||||
const app = applications.find((a) => a.id === s.application_id);
|
||||
const posting = postings.find((p) => p.id === s.job_posting_id);
|
||||
return {
|
||||
...s,
|
||||
department: posting?.role_category || '—',
|
||||
timeToHire: app
|
||||
? Math.max(1, Math.round(
|
||||
(new Date(app.updated_date).getTime() - new Date(app.created_date).getTime()) / DAY_MS
|
||||
))
|
||||
: null,
|
||||
score: s.ai_score || app?.ai_score || 0,
|
||||
};
|
||||
});
|
||||
|
||||
const groupHires = (key) => Object.values(hires.reduce((acc, h) => {
|
||||
const name = h[key] || 'Unspecified';
|
||||
acc[name] ||= { name, count: 0, scores: [], days: [], ratings: [] };
|
||||
acc[name].count += 1;
|
||||
if (h.score) acc[name].scores.push(h.score);
|
||||
if (h.timeToHire) acc[name].days.push(h.timeToHire);
|
||||
if (h.client_rating) acc[name].ratings.push(h.client_rating);
|
||||
return acc;
|
||||
}, {})).map((e) => ({
|
||||
name: e.name,
|
||||
count: e.count,
|
||||
avgScore: avg(e.scores),
|
||||
avgDays: avg(e.days),
|
||||
avgRating: e.ratings.length
|
||||
? Number((e.ratings.reduce((a, b) => a + b, 0) / e.ratings.length).toFixed(1))
|
||||
: null,
|
||||
})).sort((a, b) => b.count - a.count);
|
||||
|
||||
const hiring = {
|
||||
hires,
|
||||
byDepartment: groupHires('department'),
|
||||
byRoleTitle: groupHires('role'),
|
||||
unrated: hires.filter((h) => !h.client_rating),
|
||||
rated: hires.filter((h) => h.client_rating),
|
||||
avgQuality: avg(hires.map((h) => h.score).filter(Boolean)),
|
||||
avgSpeed: avg(hires.map((h) => h.timeToHire).filter(Boolean)),
|
||||
strongest: [...hires].sort((a, b) => (b.score || 0) - (a.score || 0))[0] || null,
|
||||
onboarding: hires.filter((h) => h.status === 'onboarding'),
|
||||
};
|
||||
|
||||
return {
|
||||
today,
|
||||
|
||||
@@ -248,9 +352,12 @@ export function buildFacts({
|
||||
eventCounts,
|
||||
activitySignals,
|
||||
|
||||
/* Signed-in worker (talent context only) */
|
||||
/* Signed-in worker, and the pages built on these records */
|
||||
profile,
|
||||
courses,
|
||||
forge,
|
||||
talent,
|
||||
hiring,
|
||||
|
||||
/* Economics — mirrors ImpactMetrics */
|
||||
costSaved: scored.length * 45 + interviews.length * 80,
|
||||
|
||||
Reference in New Issue
Block a user