import { useState } from "react"; import { ArrowRight, Check, UserPlus, X, } from "@phosphor-icons/react"; import type { AgentRecord, ApprovalRecord } from "@shared/types"; import { getAgentInitials } from "../lib/formatters"; import { ActionButton, EmptyState, Panel, StatusPill } from "./ui"; interface HiringPipelineProps { agents: AgentRecord[]; approvals: ApprovalRecord[]; onRefresh: () => Promise; onNavigateToAgent: (agentId: string) => void; } type HiringStage = "requisition" | "pending_approval" | "approved" | "onboarding" | "active" | "rejected"; interface HiringCandidate { agent: AgentRecord | null; approval: ApprovalRecord | null; stage: HiringStage; } function getHiringStage(agent: AgentRecord, approval: ApprovalRecord | null): HiringStage { if (agent.status === "pending_approval") { if (!approval) return "requisition"; if (approval.state === "pending") return "pending_approval"; if (approval.state === "rejected") return "rejected"; if (approval.state === "revision_requested") return "requisition"; return "approved"; } if (agent.status === "idle" && approval?.state === "approved") { return "onboarding"; } return "active"; } function stageLabel(stage: HiringStage): string { switch (stage) { case "requisition": return "Requisition"; case "pending_approval": return "Pending Approval"; case "approved": return "Approved"; case "onboarding": return "Onboarding"; case "active": return "Active"; case "rejected": return "Rejected"; } } function stageColor(stage: HiringStage): string { switch (stage) { case "requisition": return "bg-[color:var(--warn-soft)] text-[color:var(--warn)]"; case "pending_approval": return "bg-[color:var(--warn-soft)] text-[color:var(--warn)]"; case "approved": return "bg-[color:var(--success-soft)] text-[color:var(--success)]"; case "onboarding": return "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"; case "active": return "bg-[color:var(--success-soft)] text-[color:var(--success)]"; case "rejected": return "bg-[color:var(--danger-soft)] text-[color:var(--danger)]"; } } const STAGE_ORDER: HiringStage[] = ["requisition", "pending_approval", "approved", "onboarding", "active", "rejected"]; export function HiringPipeline({ agents, approvals, onRefresh, onNavigateToAgent }: HiringPipelineProps) { const [filterStage, setFilterStage] = useState("all"); const hireApprovals = approvals.filter(a => a.type === "hire_agent"); const candidates: HiringCandidate[] = agents.map(agent => { const approval = hireApprovals.find(a => { if (a.relatedAgentId === agent.id) return true; if (!a.payloadJson) return false; try { return JSON.parse(a.payloadJson).agentId === agent.id; } catch { return false; } }) ?? null; return { agent, approval, stage: getHiringStage(agent, approval), }; }); const pendingHireApprovals = hireApprovals.filter(a => { return a.state === "pending" && !candidates.some(c => c.approval?.id === a.id); }); const allCandidates = [ ...candidates, ...pendingHireApprovals.map(a => ({ agent: null, approval: a, stage: "pending_approval" as HiringStage, })), ].sort((a, b) => STAGE_ORDER.indexOf(a.stage) - STAGE_ORDER.indexOf(b.stage)); const filteredCandidates = filterStage === "all" ? allCandidates : allCandidates.filter(c => c.stage === filterStage); const stageCounts = STAGE_ORDER.reduce((acc, stage) => { acc[stage] = allCandidates.filter(c => c.stage === stage).length; return acc; }, {} as Record); const pendingCount = stageCounts.requisition + stageCounts.pending_approval; const activeHires = allCandidates.filter(c => c.stage !== "active" && c.stage !== "rejected").length; return (
{STAGE_ORDER.filter(s => s !== "rejected").map((stage, idx) => (
{idx < STAGE_ORDER.length - 2 && ( )}
))} {stageCounts.rejected > 0 && ( )}
{pendingCount > 0 && (
{allCandidates .filter(c => c.stage === "requisition" || c.stage === "pending_approval") .map((candidate) => { const a = candidate.approval; const ag = candidate.agent; return (
{ag?.name ?? "Proposed Agent"} {stageLabel(candidate.stage)}
{ag ? `${ag.role} — ${ag.title}` : a?.payloadSummary}
{ag?.department && (
Dept: {ag.department} | Budget: ${ag.budgetMonthlyUsd}/mo | Connector: {ag.connectorId}
)} {a?.payloadSummary && (
{a.payloadSummary}
)} {a?.impactSummary && (
{a.impactSummary}
)} {ag?.capabilities && (
{ag.capabilities}
)}
{a && a.state === "pending" && ( <> { await window.agentCompany.decideApproval({ companyId: a.companyId, approvalId: a.id, state: "approved", decisionNote: "Hire approved via hiring pipeline", }); await onRefresh(); }} /> { await window.agentCompany.decideApproval({ companyId: a.companyId, approvalId: a.id, state: "rejected", decisionNote: "Hire rejected via hiring pipeline", }); await onRefresh(); }} /> )}
); })}
)} setFilterStage("all")} /> ) : undefined } > {filteredCandidates.length === 0 ? ( ) : (
{filteredCandidates.map((candidate) => { const ag = candidate.agent; if (!ag) return null; const manager = agents.find(a => a.id === ag.reportsTo); const requestedBy = candidate.approval?.requestedByAgentId ? agents.find(a => a.id === candidate.approval!.requestedByAgentId) : null; return (
onNavigateToAgent(ag.id)} >
{getAgentInitials(ag.name)}
{ag.name} {stageLabel(candidate.stage)}
{ag.role} — {ag.title}{ag.department ? ` | ${ag.department}` : ""}
{manager ? `Reports to ${manager.name}` : "Top level"}
{requestedBy && (
Proposed by {requestedBy.name}
)}
${ag.budgetMonthlyUsd}/mo
); })}
)}
Total Agents
{agents.filter(a => a.status !== "terminated").length}
Pending Hires
{agents.filter(a => a.status === "pending_approval").length}
Departments
{new Set(agents.filter(a => a.department && a.status !== "terminated").map(a => a.department)).size}
Monthly Payroll
${agents.filter(a => a.status !== "terminated" && a.status !== "pending_approval").reduce((s, a) => s + a.budgetMonthlyUsd, 0).toLocaleString()}
{(["executive", "engineering", "product", "design", "marketing", "sales", "hr", "finance", "legal", "customer_support", "operations", "research"] as const).map(dept => { const deptAgents = agents.filter(a => a.department === dept && a.status !== "terminated"); const pending = deptAgents.filter(a => a.status === "pending_approval").length; const active = deptAgents.filter(a => a.status !== "pending_approval").length; if (active === 0 && pending === 0) return null; return (
{dept.replace("_", " ")}
{active} active {pending > 0 && ( <> +{pending} pending )}
); })}
); }