Add hiring pipeline with four-stage board view
HiringPipeline component shows agents across stages: Pending Approval, Approved, Active, and Rejected. Each stage column shows agent cards with role, department, and approval timing. Stage assignment is derived from the agent's lifecycle and associated hire approval state.
This commit is contained in:
1 parent
2c12f68b4e
commit
55842d12a1
2 files changed
+78
No files matched your search
@@ -3,6 +3,7 @@ import { Avatar, Button, Field, Icon, IconButton } from "../shared/ui";
|
||||
import { TopbarActions } from "../shared/TopbarActions";
|
||||
import { Dialog } from "../shared/Dialog";
|
||||
import { OrgChartTree } from "./team/OrgChartTree";
|
||||
import { HiringPipeline } from "./team/HiringPipeline";
|
||||
import type { AgentRecord, EmployeeRecord, ProviderRecord } from "../../../shared/domain";
|
||||
import type { PageProps } from "./types";
|
||||
import { activeOrgId, money } from "./types";
|
||||
@@ -112,6 +113,8 @@ export function TeamPage({ snapshot, actions }: PageProps) {
|
||||
<OrgChartTree boardChair={employees[0] ?? null} agents={agents} onSelectAgent={setEditAgentId} />
|
||||
</section>
|
||||
|
||||
<HiringPipeline agents={agents} approvals={snapshot.approvals.filter((a) => a.orgId === orgId)} />
|
||||
|
||||
<div className="section-label" style={{ marginBottom: 10 }}>Roster</div>
|
||||
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Avatar, Pill } from "../../shared/ui";
|
||||
import type { AgentRecord, ApprovalRecord } from "../../../../shared/domain";
|
||||
import { formatRelative } from "../types";
|
||||
|
||||
interface PipelineStage {
|
||||
id: string;
|
||||
label: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const STAGES: PipelineStage[] = [
|
||||
{ id: "pending_approval", label: "Pending approval", color: "var(--amber)" },
|
||||
{ id: "approved", label: "Approved", color: "var(--green)" },
|
||||
{ id: "active", label: "Active", color: "var(--accent)" },
|
||||
{ id: "rejected", label: "Rejected", color: "var(--red)" },
|
||||
];
|
||||
|
||||
interface PipelineEntry {
|
||||
agent: AgentRecord;
|
||||
stage: string;
|
||||
approval: ApprovalRecord | null;
|
||||
}
|
||||
|
||||
function resolveStage(agent: AgentRecord, approvals: ApprovalRecord[]): PipelineEntry {
|
||||
const hireApproval = approvals.find(
|
||||
(a) => a.sourceEntityType === "agent" && a.sourceEntityId === agent.id && a.kind === "agent_hire",
|
||||
) ?? null;
|
||||
|
||||
if (hireApproval?.state === "pending") return { agent, stage: "pending_approval", approval: hireApproval };
|
||||
if (hireApproval?.state === "declined") return { agent, stage: "rejected", approval: hireApproval };
|
||||
if (agent.lifecycle === "active") return { agent, stage: "active", approval: hireApproval };
|
||||
return { agent, stage: "approved", approval: hireApproval };
|
||||
}
|
||||
|
||||
export function HiringPipeline({ agents, approvals }: { agents: AgentRecord[]; approvals: ApprovalRecord[] }) {
|
||||
const entries = agents.map((a) => resolveStage(a, approvals));
|
||||
|
||||
return (
|
||||
<section style={{ marginBottom: 24 }}>
|
||||
<div className="section-label" style={{ marginBottom: 12 }}>Hiring pipeline</div>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, minmax(0, 1fr))", gap: 12 }}>
|
||||
{STAGES.map((stage) => {
|
||||
const stageEntries = entries.filter((e) => e.stage === stage.id);
|
||||
return (
|
||||
<div key={stage.id}>
|
||||
<div className="row" style={{ gap: 6, marginBottom: 8 }}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: 2, background: stage.color }} />
|
||||
<span style={{ fontSize: 12, fontWeight: 540 }}>{stage.label}</span>
|
||||
<span className="num" style={{ fontSize: 12, color: "var(--text-3)" }}>{stageEntries.length}</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||
{stageEntries.map((entry) => (
|
||||
<div key={entry.agent.id} className="card" style={{ padding: 10 }}>
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
<Avatar name={entry.agent.name} size="sm" bot />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 12.5, fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{entry.agent.name}</div>
|
||||
<div style={{ fontSize: 11, color: "var(--text-3)" }}>{entry.agent.role} · {entry.agent.department}</div>
|
||||
</div>
|
||||
</div>
|
||||
{entry.approval && (
|
||||
<div style={{ marginTop: 6, fontSize: 11, color: "var(--text-3)" }}>
|
||||
{formatRelative(entry.approval.createdAt)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user