Render the team org chart as a hierarchical reporting tree
This commit is contained in:
1 parent
7dfe652eda
commit
dd0bb5dc58
2 files changed
+90
-40
No files matched your search
@@ -81,27 +81,9 @@ export function TeamPage({ snapshot, actions }: PageProps) {
|
||||
{" "}{agents.length} {agents.length === 1 ? "agent" : "agents"}
|
||||
</p>
|
||||
|
||||
<section className="card" style={{ padding: 32, marginBottom: 28 }}>
|
||||
<section className="card" style={{ padding: 32, marginBottom: 28, overflowX: "auto" }}>
|
||||
<div className="section-label">Org</div>
|
||||
{employees[0] ? (
|
||||
<div style={{ display: "flex", justifyContent: "center", marginTop: 18, marginBottom: 12 }}>
|
||||
<OrgCard name={employees[0].displayName} role={employees[0].role} human />
|
||||
</div>
|
||||
) : null}
|
||||
{agents.length > 0 && <OrgConnector slots={agents.length} />}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: 14,
|
||||
marginTop: 18,
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{agents.map((agent) => (
|
||||
<OrgCard key={agent.id} name={agent.name} role={agent.role} />
|
||||
))}
|
||||
</div>
|
||||
<OrgChart boardChair={employees[0] ?? null} agents={agents} />
|
||||
</section>
|
||||
|
||||
<div className="section-label" style={{ marginBottom: 10 }}>Roster</div>
|
||||
@@ -210,27 +192,50 @@ function OrgCard({ name, role, human = false }: { name: string; role: string; hu
|
||||
);
|
||||
}
|
||||
|
||||
function OrgConnector({ slots }: { slots: number }) {
|
||||
if (slots <= 1) {
|
||||
return (
|
||||
<svg width="100%" height="36" style={{ display: "block", marginTop: 18 }} aria-hidden="true">
|
||||
<line x1="50%" x2="50%" y1="0" y2="36" stroke="var(--border-strong)" strokeWidth="1" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
const count = Math.min(4, slots);
|
||||
const step = 100 / (count * 2);
|
||||
const positions = Array.from({ length: count }, (_, i) => step * (2 * i + 1));
|
||||
const left = positions[0];
|
||||
const right = positions[positions.length - 1];
|
||||
function OrgChart({ boardChair, agents }: { boardChair: EmployeeRecord | null; agents: AgentRecord[] }) {
|
||||
const childrenByManager = useMemo(() => {
|
||||
const known = new Set(agents.map((agent) => agent.id));
|
||||
const map = new Map<string, AgentRecord[]>();
|
||||
for (const agent of agents) {
|
||||
// Agents whose manager is missing (or the top-level CEO) hang off the root.
|
||||
const key = agent.managerAgentId && known.has(agent.managerAgentId) ? agent.managerAgentId : "__root__";
|
||||
const list = map.get(key) ?? [];
|
||||
list.push(agent);
|
||||
map.set(key, list);
|
||||
}
|
||||
return map;
|
||||
}, [agents]);
|
||||
|
||||
const roots = childrenByManager.get("__root__") ?? [];
|
||||
if (!boardChair && roots.length === 0) return null;
|
||||
|
||||
return (
|
||||
<svg width="100%" height="36" style={{ display: "block", marginTop: 18 }} aria-hidden="true">
|
||||
<line x1="50%" x2="50%" y1="0" y2="14" stroke="var(--border-strong)" strokeWidth="1" />
|
||||
<line x1={`${left}%`} x2={`${right}%`} y1="14" y2="14" stroke="var(--border-strong)" strokeWidth="1" />
|
||||
{positions.map((pos) => (
|
||||
<line key={pos} x1={`${pos}%`} x2={`${pos}%`} y1="14" y2="36" stroke="var(--border-strong)" strokeWidth="1" />
|
||||
))}
|
||||
</svg>
|
||||
<div className="org-tree">
|
||||
<ul>
|
||||
<li>
|
||||
{boardChair ? <OrgCard name={boardChair.displayName} role={boardChair.role} human /> : <OrgCard name="Org" role="Reports" />}
|
||||
{roots.length > 0 && (
|
||||
<ul>
|
||||
{roots.map((agent) => <OrgNode key={agent.id} agent={agent} childrenByManager={childrenByManager} />)}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OrgNode({ agent, childrenByManager }: { agent: AgentRecord; childrenByManager: Map<string, AgentRecord[]> }) {
|
||||
const reports = childrenByManager.get(agent.id) ?? [];
|
||||
return (
|
||||
<li>
|
||||
<OrgCard name={agent.name} role={agent.title || agent.role} />
|
||||
{reports.length > 0 && (
|
||||
<ul>
|
||||
{reports.map((report) => <OrgNode key={report.id} agent={report} childrenByManager={childrenByManager} />)}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -385,3 +385,48 @@ a { color: inherit; }
|
||||
gap: 2px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Hierarchical org chart: a real reporting tree with connector lines. */
|
||||
.org-tree { display: inline-block; min-width: 100%; text-align: center; margin-top: 18px; }
|
||||
.org-tree ul {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 22px 0 0 0;
|
||||
list-style: none;
|
||||
}
|
||||
.org-tree li {
|
||||
position: relative;
|
||||
padding: 22px 8px 0 8px;
|
||||
list-style: none;
|
||||
}
|
||||
.org-tree li::before, .org-tree li::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 50%;
|
||||
width: 50%;
|
||||
height: 22px;
|
||||
border-top: 1px solid var(--border-strong);
|
||||
}
|
||||
.org-tree li::after {
|
||||
right: auto;
|
||||
left: 50%;
|
||||
border-left: 1px solid var(--border-strong);
|
||||
}
|
||||
.org-tree li:only-child::before, .org-tree li:only-child::after { display: none; }
|
||||
.org-tree li:only-child { padding-top: 22px; }
|
||||
.org-tree li:first-child::before, .org-tree li:last-child::after { border: 0 none; }
|
||||
.org-tree li:last-child::before { border-right: 1px solid var(--border-strong); }
|
||||
.org-tree ul ul::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 22px;
|
||||
border-left: 1px solid var(--border-strong);
|
||||
}
|
||||
.org-tree > ul { padding-top: 0; }
|
||||
.org-tree > ul > li:only-child { padding-top: 0; }
|
||||
Reference in new issue
Block a user