update hired page

This commit is contained in:
2026-08-11 19:17:40 +05:30
parent 8a256a4de7
commit 25c4ee4a6a
23 changed files with 2921 additions and 380 deletions

View File

@@ -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,