Files
krow_talent_app/src/lib/krowScore.js
2026-08-07 23:53:15 +05:30

126 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Engine 6 — KROW Score Engine (formula-based, auto-recalculated)
// Reliability = Attendance×.30 + Performance×.25 + Education×.10 + ClientReviews×.15 + SupervisorReviews×.10 + Growth×.05 + Experience×.05
// KROW Score = Reliability×.40 + AI Interview×.25 + Education×.15 + Growth×.10 + Experience×.10
const clamp = (n) => Math.max(0, Math.min(100, n));
export function computeKrowScore(profile) {
const attendance = clamp(profile.attendance_score ?? 100);
const performance = clamp(profile.performance_score ?? 0);
const education = clamp(
(profile.completed_courses?.length || 0) * 12 +
(profile.earned_badges?.length || 0) * 8
);
const clientReviews = profile.client_rating ? clamp((profile.client_rating / 5) * 100) : 0;
const supervisorReviews = profile.supervisor_rating ? clamp((profile.supervisor_rating / 5) * 100) : 0;
const growth = clamp((profile.xp || 0) / 10);
const experience = clamp(
(profile.experience_years || 0) * 10 + (profile.experience?.length || 0) * 5
);
const reliability = Math.round(
attendance * 0.30 + performance * 0.25 + education * 0.10 +
clientReviews * 0.15 + supervisorReviews * 0.10 + growth * 0.05 + experience * 0.05
);
const krow_score = Math.round(
reliability * 0.40 +
clamp(profile.ai_interview_score || 0) * 0.25 +
education * 0.15 +
growth * 0.10 +
experience * 0.10
);
return {
krow_score,
reliability,
breakdown: { attendance, performance, education, clientReviews, supervisorReviews, growth, experience },
};
}
const CORE_FIELDS = ['full_name', 'email', 'phone', 'address', 'current_position', 'desired_position', 'career_goals', 'transportation', 'resume_url'];
export function computeProfileCompletion(profile) {
let filled = 0;
for (const f of CORE_FIELDS) if (profile[f]) filled++;
if (profile.languages?.length) filled++;
if (profile.availability?.length) filled++;
if (profile.skills?.length) filled++;
if (profile.experience?.length) filled++;
if (profile.completed_courses?.length) filled++;
if (profile.earned_badges?.length) filled++;
if (profile.ai_interview_score > 0) filled++;
const total = CORE_FIELDS.length + 5;
return Math.round((filled / total) * 100);
}
/** Returns only the recomputed fields — merge into an update patch */
export function recalcProfilePatch(profile) {
const { krow_score, reliability, breakdown } = computeKrowScore(profile);
return {
krow_score,
reliability_score: reliability,
profile_completion: computeProfileCompletion(profile),
score_breakdown: breakdown,
};
}
// Engine 11 (light) — recommendation helpers
export function recommendNextCourse(profile, learningPaths = [], courses = []) {
const p = profile || {};
const completedIds = new Set((p.completed_courses || []).map(c => c.course_id));
const desired = (p.desired_position || '').toLowerCase();
const path =
(desired && learningPaths.find(p =>
p.name?.toLowerCase().includes(desired) ||
p.target_role?.toLowerCase().includes(desired)
)) || learningPaths[0];
if (path?.steps?.length) {
const nextStep = path.steps.find(s => s.course_id && !completedIds.has(s.course_id));
const course = courses.find(c => c.id === nextStep?.course_id);
if (course) return { course, path };
}
const uncompleted = courses.filter(c => c.status === 'active' && !completedIds.has(c.id));
uncompleted.sort((a, b) => (b.xp || 0) - (a.xp || 0));
return uncompleted[0] ? { course: uncompleted[0], path: null } : { course: null, path };
}
export function computeJobMatch(profile, job) {
const p = profile || {};
let score = 0;
const desired = (p.desired_position || '').toLowerCase();
if (desired && (job.title?.toLowerCase().includes(desired) || job.role_category?.toLowerCase().includes(desired))) score += 35;
const profileSkills = new Set((p.skills || []).map(s => s.toLowerCase()));
const profileCerts = new Set((p.earned_badges || []).map(b => b.name?.toLowerCase()).filter(Boolean));
const reqSkills = (job.qualifications || []).map(q => q.toLowerCase());
const reqCerts = (job.certifications_required || []).map(c => c.toLowerCase());
let skillHits = 0;
for (const s of reqSkills) {
for (const ps of profileSkills) { if (s.includes(ps) || ps.includes(s)) { skillHits++; break; } }
}
if (reqSkills.length) score += Math.min(35, (skillHits / reqSkills.length) * 35);
let certHits = 0;
for (const c of reqCerts) {
for (const pc of profileCerts) { if (c.includes(pc) || pc.includes(c)) { certHits++; break; } }
}
if (reqCerts.length) score += Math.min(20, (certHits / reqCerts.length) * 20);
if ((p.experience_years || 0) >= (job.min_experience_years || 0)) score += 10;
return Math.min(100, Math.round(score));
}
export function recommendJobs(profile, jobPostings = [], limit = 3) {
return jobPostings
.filter(j => j.status === 'active')
.map(j => ({ job: j, match: computeJobMatch(profile, j) }))
.filter(r => r.match > 0)
.sort((a, b) => b.match - a.match)
.slice(0, limit);
}