chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import * as XLSX from "xlsx";
|
||||
import type { SkillEntry } from "@planarchy/shared";
|
||||
|
||||
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<string, string>[]): 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<string, string>[], mainSkillSet: Set<string>): 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 function parseSkillMatrixWorkbook(data: ArrayBuffer): ParsedSkillMatrix {
|
||||
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<Record<string, string>>(employeeSheet, { raw: false, defval: "" })
|
||||
: [];
|
||||
|
||||
const softwareRows = softwareSheet
|
||||
? XLSX.utils.sheet_to_json<Record<string, string>>(softwareSheet, { raw: false, defval: "" })
|
||||
: [];
|
||||
|
||||
const technicalRows = technicalSheet
|
||||
? XLSX.utils.sheet_to_json<Record<string, string>>(technicalSheet, { raw: false, defval: "" })
|
||||
: [];
|
||||
|
||||
const employeeInfo = parseEmployeeInfo(employeeRows);
|
||||
|
||||
// Track main skills across both sheets (max 2 total)
|
||||
const mainSkillSet = new Set<string>();
|
||||
const softwareSkills = parseSkillSheet(softwareRows, mainSkillSet);
|
||||
const technicalSkills = parseSkillSheet(technicalRows, mainSkillSet);
|
||||
|
||||
// Merge: deduplicate by skill name (prefer higher proficiency)
|
||||
const skillMap = new Map<string, SkillEntry>();
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user