import type { SkillEntry } from "@capakraken/shared"; let _xlsx: typeof import("xlsx") | null = null; async function getXLSX() { if (!_xlsx) { _xlsx = await import("xlsx"); } return _xlsx; } export interface ParsedEmployeeInfo { displayName?: string; areaOfExpertise?: string; yearsOfExperience?: number; portfolioUrl?: string; } export interface ParsedSkillMatrix { employeeInfo: ParsedEmployeeInfo; skills: SkillEntry[]; } // Maps Excel proficiency (1-4) → plANARCHY proficiency (2-5) function mapProficiency(raw: string): 1 | 2 | 3 | 4 | 5 | null { const n = parseInt(raw, 10); if (isNaN(n) || n === 0) return null; if (n === 1) return 2; if (n === 2) return 3; if (n === 3) return 4; if (n === 4) return 5; return null; } function parseEmployeeInfo(rows: Record[]): ParsedEmployeeInfo { const info: ParsedEmployeeInfo = {}; for (const row of rows) { const item = (row["item"] ?? "").toLowerCase().trim(); const value = (row["property"] ?? "").trim(); if (!value || value === "please select") continue; if (item.includes("full name")) { info.displayName = value; } else if (item.includes("area of expertise") || item.includes("main focus")) { info.areaOfExpertise = value; } else if (item.includes("years of experience")) { const n = parseFloat(value); if (!isNaN(n)) info.yearsOfExperience = Math.round(n); } else if (item.includes("portfolio") || item.includes("url") || item.includes("linkedin") || item.includes("artstation")) { if (value.startsWith("http")) info.portfolioUrl = value; } } return info; } function parseSkillSheet(rows: Record[], mainSkillSet: Set): SkillEntry[] { const skills: SkillEntry[] = []; let mainSkillCount = 0; for (const row of rows) { const category = (row["category"] ?? "").trim(); const item = (row["item"] ?? "").trim(); const property = (row["property"] ?? "").trim(); const mainSkillRaw = (row["main skillset"] ?? "").trim(); if (!item) continue; const proficiency = mapProficiency(property); if (proficiency === null) continue; // skip 0 / no experience const isMainSkillCandidate = mainSkillRaw === "1" || mainSkillRaw === "2"; const isMainSkill = isMainSkillCandidate && mainSkillCount < 2; if (isMainSkill) { mainSkillSet.add(item); mainSkillCount++; } skills.push({ skill: item, ...(category ? { category } : {}), proficiency, ...(isMainSkillCandidate ? { isMainSkill: true } : {}), }); } return skills; } /** * Parse a skill matrix workbook (xlsx ArrayBuffer) into structured data. * Returns ParsedSkillMatrix with employeeInfo and merged skills array. */ export async function parseSkillMatrixWorkbook(data: ArrayBuffer): Promise { const XLSX = await getXLSX(); const workbook = XLSX.read(new Uint8Array(data), { type: "array" }); const employeeSheet = workbook.Sheets["Employee Information"]; const softwareSheet = workbook.Sheets["Software Skills"]; const technicalSheet = workbook.Sheets["Technical Skillset"]; const employeeRows = employeeSheet ? XLSX.utils.sheet_to_json>(employeeSheet, { raw: false, defval: "" }) : []; const softwareRows = softwareSheet ? XLSX.utils.sheet_to_json>(softwareSheet, { raw: false, defval: "" }) : []; const technicalRows = technicalSheet ? XLSX.utils.sheet_to_json>(technicalSheet, { raw: false, defval: "" }) : []; const employeeInfo = parseEmployeeInfo(employeeRows); // Track main skills across both sheets (max 2 total) const mainSkillSet = new Set(); const softwareSkills = parseSkillSheet(softwareRows, mainSkillSet); const technicalSkills = parseSkillSheet(technicalRows, mainSkillSet); // Merge: deduplicate by skill name (prefer higher proficiency) const skillMap = new Map(); for (const s of [...softwareSkills, ...technicalSkills]) { const existing = skillMap.get(s.skill); if (!existing || s.proficiency > existing.proficiency) { skillMap.set(s.skill, s); } } return { employeeInfo, skills: Array.from(skillMap.values()), }; } /** * Fuzzy match an areaOfExpertise string against a list of role names. * Returns the best matching role name or null. */ export function matchRoleName(areaOfExpertise: string, roleNames: string[]): string | null { if (!areaOfExpertise) return null; const needle = areaOfExpertise.toLowerCase().trim(); // Exact match first const exact = roleNames.find((r) => r.toLowerCase() === needle); if (exact) return exact; // Partial match (needle contains role name or vice versa) const partial = roleNames.find( (r) => needle.includes(r.toLowerCase()) || r.toLowerCase().includes(needle), ); return partial ?? null; }