"use client"; import { useState, useRef } from "react"; import { trpc } from "~/lib/trpc/client.js"; import { parseSkillMatrixWorkbook, matchRoleName } from "~/lib/skillMatrixParser.js"; import type { SkillEntry } from "@planarchy/shared"; interface Props { resourceId: string; isOwner: boolean; // true = self-service, false = manager import onClose: () => void; onSuccess: () => void; } type PreviewState = { skills: SkillEntry[]; employeeInfo: { roleId?: string; portfolioUrl?: string; }; matchedRoleName?: string; }; export function SkillMatrixUpload({ resourceId, isOwner, onClose, onSuccess }: Props) { const [preview, setPreview] = useState(null); const [parseError, setParseError] = useState(null); const [submitting, setSubmitting] = useState(false); const fileRef = useRef(null); const { data: roles } = trpc.role.list.useQuery({ isActive: true }, { staleTime: 60_000 }); const selfMutation = trpc.resource.importSkillMatrix.useMutation({ onSuccess: () => { setSubmitting(false); onSuccess(); }, onError: (err) => { setSubmitting(false); setParseError(err.message); }, }); const managerMutation = trpc.resource.importSkillMatrixForResource.useMutation({ onSuccess: () => { setSubmitting(false); onSuccess(); }, onError: (err) => { setSubmitting(false); setParseError(err.message); }, }); async function handleFile(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; setParseError(null); setPreview(null); try { const buffer = await file.arrayBuffer(); const parsed = await parseSkillMatrixWorkbook(buffer); // Fuzzy match areaOfExpertise → roleId let roleId: string | undefined; let matchedRoleName: string | undefined; if (parsed.employeeInfo.areaOfExpertise && roles) { const roleNames = roles.map((r) => r.name); const matched = matchRoleName(parsed.employeeInfo.areaOfExpertise, roleNames); if (matched) { const role = roles.find((r) => r.name === matched); roleId = role?.id; matchedRoleName = matched; } } setPreview({ skills: parsed.skills, employeeInfo: { ...(roleId !== undefined ? { roleId } : {}), ...(parsed.employeeInfo.portfolioUrl !== undefined ? { portfolioUrl: parsed.employeeInfo.portfolioUrl } : {}), }, ...(matchedRoleName !== undefined ? { matchedRoleName } : {}), }); } catch (err) { setParseError(String(err instanceof Error ? err.message : err)); } } function handleConfirm() { if (!preview) return; setSubmitting(true); const payload = { skills: preview.skills, employeeInfo: { roleId: preview.employeeInfo.roleId, portfolioUrl: preview.employeeInfo.portfolioUrl, }, }; if (isOwner) { selfMutation.mutate(payload); } else { managerMutation.mutate({ resourceId, ...payload }); } } const mainSkills = preview?.skills.filter((s) => s.isMainSkill) ?? []; return (
{ if (e.target === e.currentTarget) onClose(); }} >
{/* Header */}

Update Skill Matrix

{/* File picker */} {!preview && (
fileRef.current?.click()} >

Click to select skill matrix file

.xlsx accepted

)} {parseError && (
{parseError}
)} {/* Preview */} {preview && (
{preview.skills.length}
Skills found
{mainSkills.length}
Main skills
{mainSkills.length > 0 && (

Main skills:

{mainSkills.map((s) => ( ★ {s.skill} ))}
)} {preview.matchedRoleName && (

Area of expertise matched to plANARCHY role:{" "} {preview.matchedRoleName}

)} {preview.employeeInfo.portfolioUrl && (

Portfolio URL:{" "} {preview.employeeInfo.portfolioUrl}

)}

This will replace all existing skills for this resource.

)}
); }