d912670f72
Expand NewIssueDialog with richer form fields. Add NewProjectDialog. Enhance CommandPalette with more actions and search. Improve CompanySwitcher, EmptyState, and IssueProperties. Flesh out Activity, Companies, Dashboard, and Inbox pages with real content and layouts. Refine sidebar, routing, and dialog context. CSS tweaks for dark theme. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
129 lines
3.6 KiB
TypeScript
129 lines
3.6 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { agentsApi, type OrgNode } from "../api/agents";
|
|
import { useCompany } from "../context/CompanyContext";
|
|
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
|
import { useApi } from "../hooks/useApi";
|
|
import { StatusBadge } from "../components/StatusBadge";
|
|
import { EmptyState } from "../components/EmptyState";
|
|
import { ChevronRight, GitBranch } from "lucide-react";
|
|
import { cn } from "../lib/utils";
|
|
|
|
function OrgTree({
|
|
nodes,
|
|
depth = 0,
|
|
onSelect,
|
|
}: {
|
|
nodes: OrgNode[];
|
|
depth?: number;
|
|
onSelect: (id: string) => void;
|
|
}) {
|
|
return (
|
|
<div>
|
|
{nodes.map((node) => (
|
|
<OrgTreeNode key={node.id} node={node} depth={depth} onSelect={onSelect} />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function OrgTreeNode({
|
|
node,
|
|
depth,
|
|
onSelect,
|
|
}: {
|
|
node: OrgNode;
|
|
depth: number;
|
|
onSelect: (id: string) => void;
|
|
}) {
|
|
const [expanded, setExpanded] = useState(true);
|
|
const hasChildren = node.reports.length > 0;
|
|
|
|
return (
|
|
<div>
|
|
<div
|
|
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors cursor-pointer hover:bg-accent/50"
|
|
style={{ paddingLeft: `${depth * 24 + 12}px` }}
|
|
onClick={() => onSelect(node.id)}
|
|
>
|
|
{hasChildren ? (
|
|
<button
|
|
className="p-0.5"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setExpanded(!expanded);
|
|
}}
|
|
>
|
|
<ChevronRight
|
|
className={cn("h-3 w-3 transition-transform", expanded && "rotate-90")}
|
|
/>
|
|
</button>
|
|
) : (
|
|
<span className="w-4" />
|
|
)}
|
|
<span
|
|
className={cn(
|
|
"h-2 w-2 rounded-full shrink-0",
|
|
node.status === "active"
|
|
? "bg-green-400"
|
|
: node.status === "paused"
|
|
? "bg-yellow-400"
|
|
: node.status === "error"
|
|
? "bg-red-400"
|
|
: "bg-neutral-400"
|
|
)}
|
|
/>
|
|
<span className="font-medium flex-1">{node.name}</span>
|
|
<span className="text-xs text-muted-foreground">{node.role}</span>
|
|
<StatusBadge status={node.status} />
|
|
</div>
|
|
{hasChildren && expanded && (
|
|
<OrgTree nodes={node.reports} depth={depth + 1} onSelect={onSelect} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function Org() {
|
|
const { selectedCompanyId } = useCompany();
|
|
const { setBreadcrumbs } = useBreadcrumbs();
|
|
const navigate = useNavigate();
|
|
|
|
useEffect(() => {
|
|
setBreadcrumbs([{ label: "Org Chart" }]);
|
|
}, [setBreadcrumbs]);
|
|
|
|
const fetcher = useCallback(() => {
|
|
if (!selectedCompanyId) return Promise.resolve([] as OrgNode[]);
|
|
return agentsApi.org(selectedCompanyId);
|
|
}, [selectedCompanyId]);
|
|
|
|
const { data, loading, error } = useApi(fetcher);
|
|
|
|
if (!selectedCompanyId) {
|
|
return <EmptyState icon={GitBranch} message="Select a company to view org chart." />;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<h2 className="text-lg font-semibold">Org Chart</h2>
|
|
|
|
{loading && <p className="text-sm text-muted-foreground">Loading...</p>}
|
|
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
|
|
|
{data && data.length === 0 && (
|
|
<EmptyState
|
|
icon={GitBranch}
|
|
message="No agents in the organization. Create agents to build your org chart."
|
|
/>
|
|
)}
|
|
|
|
{data && data.length > 0 && (
|
|
<div className="border border-border rounded-md py-1">
|
|
<OrgTree nodes={data} onSelect={(id) => navigate(`/agents/${id}`)} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|