"use client"; import { useState, useRef } from "react"; import { trpc } from "~/lib/trpc/client.js"; import { parseSkillMatrixWorkbook, matchRoleName } from "~/lib/skillMatrixParser.js"; import { assertSpreadsheetFile } from "~/lib/excel.js"; import type { SkillEntry } from "@capakraken/shared"; interface ParsedEntry { fileName: string; candidateEid: string; // guessed from filename (no extension, lowercased) selectedEid: string; skills: SkillEntry[]; employeeInfo: Record; matchedRoleName: string | null; status: "pending" | "matched" | "unmatched"; } export function BatchSkillImport() { const [entries, setEntries] = useState([]); const [result, setResult] = useState<{ updated: number; notFound: number } | null>(null); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const fileRef = useRef(null); const { data: roles } = trpc.role.list.useQuery({ isActive: true }, { staleTime: 60_000 }); const { data: resources } = trpc.resource.directory.useQuery( { isActive: true, limit: 500 }, { staleTime: 60_000 }, ); const batchMutation = trpc.resource.batchImportSkillMatrices.useMutation({ onSuccess: (data) => { setResult(data); setSubmitting(false); }, onError: (err) => { setError(err.message); setSubmitting(false); }, }); async function handleFiles(e: React.ChangeEvent) { const files = Array.from(e.target.files ?? []); setResult(null); setError(null); const roleNames = (roles ?? []).map((r) => r.name); const resourceList = (resources?.resources ?? []) as SimpleResource[]; const parsed: ParsedEntry[] = await Promise.all( files.map(async (file) => { const baseName = file.name.replace(/\.[^.]+$/, ""); // Guess EID: try matching against resource displayName or eid const candidateEid = baseName.toLowerCase().replace(/\s+/g, "."); const matchedResource = resourceList.find( (r) => r.eid.toLowerCase() === candidateEid || r.displayName.toLowerCase().replace(/\s+/g, ".") === candidateEid || r.displayName.toLowerCase() === baseName.toLowerCase(), ); try { assertSpreadsheetFile(file, { allowCsv: false, contextLabel: "skill matrix import" }); const buffer = await file.arrayBuffer(); const result = await parseSkillMatrixWorkbook(buffer); let roleId: string | undefined; let matchedRoleName: string | undefined; if (result.employeeInfo.areaOfExpertise) { const matched = matchRoleName(result.employeeInfo.areaOfExpertise, roleNames); if (matched) { const role = (roles ?? []).find((r) => r.name === matched); roleId = role?.id; matchedRoleName = matched; } } const empInfo: Record = {}; if (roleId) empInfo["roleId"] = roleId; if (result.employeeInfo.portfolioUrl) empInfo["portfolioUrl"] = result.employeeInfo.portfolioUrl; return { fileName: file.name, candidateEid, selectedEid: matchedResource?.eid ?? candidateEid, skills: result.skills, employeeInfo: empInfo, matchedRoleName: matchedRoleName ?? null, status: matchedResource ? "matched" : "unmatched", } satisfies ParsedEntry; } catch { return { fileName: file.name, candidateEid, selectedEid: matchedResource?.eid ?? "", skills: [], employeeInfo: {}, matchedRoleName: null, status: "unmatched", } satisfies ParsedEntry; } }), ); setEntries(parsed); } function updateEid(idx: number, eid: string) { setEntries((prev) => prev.map((e, i) => i === idx ? { ...e, selectedEid: eid, status: eid ? "matched" : "unmatched", } : e, ), ); } function handleImport() { const toImport = entries.filter((e) => e.selectedEid && e.skills.length > 0); if (toImport.length === 0) return; setSubmitting(true); batchMutation.mutate({ entries: toImport.map((e) => ({ eid: e.selectedEid, skills: e.skills, employeeInfo: { ...(e.employeeInfo["roleId"] ? { roleId: e.employeeInfo["roleId"] } : {}), ...(e.employeeInfo["portfolioUrl"] ? { portfolioUrl: e.employeeInfo["portfolioUrl"] } : {}), }, })), }); } type SimpleResource = { eid: string; displayName: string }; const resourceList = (resources?.resources ?? []) as SimpleResource[]; const matched = entries.filter((e) => e.status === "matched").length; const unmatched = entries.filter((e) => e.status === "unmatched").length; return (

Batch Skill Matrix Import

Upload multiple skill matrix files at once. Files are matched to resources by filename.

{/* Upload area */}
fileRef.current?.click()} >

Click to select multiple .xlsx files

Name files after resource EID or display name for automatic matching

{/* Summary */} {entries.length > 0 && (
{matched} matched
{unmatched} unmatched (select EID manually)
)} {/* Entries table */} {entries.length > 0 && (
{entries.map((entry, idx) => ( ))}
File Resource EID Skills Role Match Status
{entry.fileName} {entry.status === "matched" ? ( {entry.selectedEid} ) : ( )} {entry.skills.length} {entry.matchedRoleName ?? "—"} {entry.status}
)} {error && (
{error}
)} {result && (
Import complete: {result.updated} updated, {result.notFound} not found.
)} {entries.length > 0 && !result && ( )}
); }