126 lines
5.0 KiB
JavaScript
126 lines
5.0 KiB
JavaScript
// 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);
|
||
} |