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"}
|
{" "}{agents.length} {agents.length === 1 ? "agent" : "agents"}
|
||||||
</p>
|
</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>
|
<div className="section-label">Org</div>
|
||||||
{employees[0] ? (
|
<OrgChart boardChair={employees[0] ?? null} agents={agents} />
|
||||||
<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>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="section-label" style={{ marginBottom: 10 }}>Roster</div>
|
<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 }) {
|
function OrgChart({ boardChair, agents }: { boardChair: EmployeeRecord | null; agents: AgentRecord[] }) {
|
||||||
if (slots <= 1) {
|
const childrenByManager = useMemo(() => {
|
||||||
return (
|
const known = new Set(agents.map((agent) => agent.id));
|
||||||
<svg width="100%" height="36" style={{ display: "block", marginTop: 18 }} aria-hidden="true">
|
const map = new Map<string, AgentRecord[]>();
|
||||||
<line x1="50%" x2="50%" y1="0" y2="36" stroke="var(--border-strong)" strokeWidth="1" />
|
for (const agent of agents) {
|
||||||
</svg>
|
// 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) ?? [];
|
||||||
const count = Math.min(4, slots);
|
list.push(agent);
|
||||||
const step = 100 / (count * 2);
|
map.set(key, list);
|
||||||
const positions = Array.from({ length: count }, (_, i) => step * (2 * i + 1));
|
}
|
||||||
const left = positions[0];
|
return map;
|
||||||
const right = positions[positions.length - 1];
|
}, [agents]);
|
||||||
|
|
||||||
|
const roots = childrenByManager.get("__root__") ?? [];
|
||||||
|
if (!boardChair && roots.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg width="100%" height="36" style={{ display: "block", marginTop: 18 }} aria-hidden="true">
|
<div className="org-tree">
|
||||||
<line x1="50%" x2="50%" y1="0" y2="14" stroke="var(--border-strong)" strokeWidth="1" />
|
<ul>
|
||||||
<line x1={`${left}%`} x2={`${right}%`} y1="14" y2="14" stroke="var(--border-strong)" strokeWidth="1" />
|
<li>
|
||||||
{positions.map((pos) => (
|
{boardChair ? <OrgCard name={boardChair.displayName} role={boardChair.role} human /> : <OrgCard name="Org" role="Reports" />}
|
||||||
<line key={pos} x1={`${pos}%`} x2={`${pos}%`} y1="14" y2="36" stroke="var(--border-strong)" strokeWidth="1" />
|
{roots.length > 0 && (
|
||||||
))}
|
<ul>
|
||||||
</svg>
|
{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;
|
gap: 2px;
|
||||||
min-height: 0;
|
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