feat(team): add TeamHeaderSummary strip + delete unreachable orgchart section

Two S-level audit items landed together because they are the same
"first-second orientation" concern on the team page:

S1 — Empty OrgChart was the first thing the board saw; the canvas
     carried no summary data. Now a compact summary strip sits above
     the chart showing Members / Running / Attention / Pending hires /
     This-month budget (spent/cap/%). All five numbers computed from
     the existing snapshot — no extra IPC.
S9 — Activity and budget context previously required visiting three
     separate pages. The summary strip collapses the minimum-useful
     subset into a single glance; the full drill-down paths (Work,
     Approvals, Costs) remain one click away.

S10 — The `orgchart` SectionId was a dead rendering branch:
       - Not in the sidebar (useUIController sidebarGroups)
       - Not in the command palette (paletteOnlyNavDefs)
       - Not reachable from any navigateTo() call in renderer code
     The only callers were an automation post-hire notification and a
     unit test — both pointed at "orgchart" as a navigation target.
     Routed both to "agents" instead (which renders the OrgChart at
     the top anyway, plus the new summary strip and the roster), so
     clicking the "Agent Hired" notification now lands on the team
     page with the new hire highlighted.

Removed:
- `orgchart` from SectionId enum (src/shared/types/enums.ts)
- `section === "orgchart"` render branch (OrganizationSections.tsx)
- `nav.orgchart` i18n key (zh + en)

Added:
- src/renderer/components/organization/TeamHeaderSummary.tsx
- team.summary.* i18n keys (zh + en)

Covers S1 / S9 / S10.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-17 19:57:58 -04:00
1 parent ad00a91754
commit 36bed1b0f1
7 files changed
+99 -10

No files matched your search

@@ -123,7 +123,7 @@ export function handleApprovalResolved(
title: localizeNotification(hireLocale, "Agent Hired", "新员工入职"),
body: localizeNotification(hireLocale, `${agent.name} joined as ${agent.role}`, `${agent.name} 已加入,职位:${agent.role}`),
urgency: "informational",
navigation: { section: "orgchart", entityId: agent.id },
navigation: { section: "agents", entityId: agent.id },
batchKey: "agent_hired",
});
@@ -0,0 +1,81 @@
import type { AgentRecord, ApprovalRecord } from "@shared/types";
import { formatMoney } from "../../lib/formatters";
import { useT } from "../../i18n";
interface TeamHeaderSummaryProps {
agents: AgentRecord[];
pendingApprovals: ApprovalRecord[];
}
/**
* Compact summary strip above the org chart. Answers the "what's going on
* with the team right now?" question without requiring the user to scan
* the chart + roster + approvals pages separately.
*
* Shows: total members, how many are actively running, how many need
* attention (paused/error/pending_approval), pending hire approvals,
* and aggregate monthly budget burn. Computed from existing snapshot
* data — no extra IPC round-trip.
*/
export function TeamHeaderSummary({ agents, pendingApprovals }: TeamHeaderSummaryProps) {
const t = useT();
const active = agents.filter((a) => a.status !== "terminated");
const running = active.filter((a) => a.status === "running").length;
const attention = active.filter(
(a) => a.status === "paused" || a.status === "error" || a.status === "pending_approval",
).length;
const pendingHires = pendingApprovals.filter((a) => a.type === "hire_agent" && a.state === "pending").length;
const totalBudget = active.reduce((sum, a) => sum + (a.budgetMonthlyUsd ?? 0), 0);
const totalSpent = active.reduce((sum, a) => sum + (a.spentMonthlyUsd ?? 0), 0);
const budgetPct = totalBudget > 0 ? Math.round((totalSpent / totalBudget) * 100) : 0;
const items: Array<{ label: string; value: string; emphasis?: "warn" | "muted" }> = [
{ label: t("team.summary.members"), value: String(active.length) },
{ label: t("team.summary.running"), value: String(running) },
{
label: t("team.summary.attention"),
value: String(attention),
emphasis: attention > 0 ? "warn" : "muted",
},
{
label: t("team.summary.pendingHires"),
value: String(pendingHires),
emphasis: pendingHires > 0 ? "warn" : "muted",
},
{
label: t("team.summary.budget"),
value:
totalBudget > 0
? t("team.summary.budgetFormat")
.replace("{spent}", formatMoney(totalSpent))
.replace("{budget}", formatMoney(totalBudget))
.replace("{pct}", String(budgetPct))
: formatMoney(totalSpent),
},
];
return (
<div className="flex flex-wrap items-stretch gap-x-6 gap-y-2 rounded-lg border border-border bg-card px-4 py-3">
{items.map((item) => (
<div key={item.label} className="flex min-w-0 flex-col">
<span className="text-[10px] font-semibold uppercase tracking-[0.06em] text-muted-foreground">
{item.label}
</span>
<span
className={`text-[16px] font-semibold leading-tight ${
item.emphasis === "warn"
? "text-warning"
: item.emphasis === "muted"
? "text-muted-foreground"
: "text-foreground"
}`}
>
{item.value}
</span>
</div>
))}
</div>
);
}
+6 -1
View File
@@ -6,7 +6,6 @@ export const en: Record<string, string> = {
"nav.performance": "Performance",
"nav.runs": "Runs",
"nav.agents": "Team",
"nav.orgchart": "Org Chart",
"nav.hiring": "Hiring",
"nav.communication": "Messages",
"nav.goals": "Goals",
@@ -340,6 +339,12 @@ export const en: Record<string, string> = {
// ── Organization page ──
"org.agentRoster": "Team members",
"team.summary.members": "Members",
"team.summary.running": "Running",
"team.summary.attention": "Attention",
"team.summary.pendingHires": "Pending hires",
"team.summary.budget": "This month",
"team.summary.budgetFormat": "{spent} / {budget} · {pct}%",
"org.newHireRequest": "New hire request",
"org.editAgent": "Edit",
"org.editAgentTitle": "Edit {name} ({role})",
+6 -1
View File
@@ -6,7 +6,6 @@ export const zh: Record<string, string> = {
"nav.performance": "绩效",
"nav.runs": "运行",
"nav.agents": "团队",
"nav.orgchart": "组织架构",
"nav.hiring": "招聘",
"nav.communication": "消息",
"nav.goals": "目标",
@@ -340,6 +339,12 @@ export const zh: Record<string, string> = {
// ── 组织页面 ──
"org.agentRoster": "团队成员",
"team.summary.members": "成员",
"team.summary.running": "运行中",
"team.summary.attention": "需关注",
"team.summary.pendingHires": "待审招聘",
"team.summary.budget": "本月预算",
"team.summary.budgetFormat": "{spent} / {budget} · {pct}%",
"org.newHireRequest": "新招聘申请",
"org.editAgent": "编辑",
"org.editAgentTitle": "编辑 {name}({role})",
@@ -12,6 +12,7 @@ import { GoalCreator } from "../../components/organization/GoalCreator";
import { GoalEditor } from "../../components/organization/GoalEditor";
import { GoalList } from "../../components/organization/GoalList";
import { OrgChart } from "../../components/organization/OrgChart";
import { TeamHeaderSummary } from "../../components/organization/TeamHeaderSummary";
import { ProjectCreator } from "../../components/organization/ProjectCreator";
import { ProjectEditor } from "../../components/organization/ProjectEditor";
import { ProjectList } from "../../components/organization/ProjectList";
@@ -35,6 +36,7 @@ export function OrganizationSections({ section }: OrganizationSectionsProps) {
companyGoals,
companyProjects,
companyWorkspaces,
pendingApprovals,
editingAgent,
selectedGoal,
selectedProject,
@@ -65,6 +67,7 @@ export function OrganizationSections({ section }: OrganizationSectionsProps) {
<>
{section === "agents" ? (
<>
<TeamHeaderSummary agents={companyAgents} pendingApprovals={pendingApprovals} />
<OrgChart agents={companyAgents} connectors={snapshot.connectors} onSelectAgent={openAgentEditor} />
<Panel title={t("org.agentRoster")} action={<ActionButton label={t("org.newHireRequest")} icon={UserPlus} tone="accent" onClick={() => setEditingAgentId(NEW_ENTITY_ID)} />}>
<AgentRoster
@@ -110,10 +113,6 @@ export function OrganizationSections({ section }: OrganizationSectionsProps) {
</>
) : null}
{section === "orgchart" ? (
<OrgChart agents={companyAgents} connectors={snapshot.connectors} onSelectAgent={(id) => { setSelectedAgentId(id); setSection("agents"); }} />
) : null}
{section === "hiring" && currentCompany ? (
<HiringPipeline
agents={companyAgents}
-1
View File
@@ -8,7 +8,6 @@ export type SectionId =
| "runs"
| "connectors"
| "agents"
| "orgchart"
| "hiring"
| "goals"
| "projects"
+2 -2
View File
@@ -184,7 +184,7 @@ describe("notification-service", () => {
title: "Agent Hired",
body: "Alice joined",
urgency: "informational",
navigation: { section: "orgchart", entityId: "agent-1" },
navigation: { section: "agents", entityId: "agent-1" },
batchKey: "agent_hired",
});
@@ -192,7 +192,7 @@ describe("notification-service", () => {
title: "Agent Hired",
body: "Bob joined",
urgency: "informational",
navigation: { section: "orgchart", entityId: "agent-2" },
navigation: { section: "agents", entityId: "agent-2" },
batchKey: "agent_hired",
});