
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Interactive organizational hierarchy tree with manager reporting lines, team counts, multi-level expandable branches, department filter pills, employee search, and an employee profile quick info drawer.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/employee-org-chart-tree.json$npx shadcn@latest add https://uipkge.dev/r/react/employee-org-chart-tree.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/employee-org-chart-tree.json$bunx shadcn@latest add https://uipkge.dev/r/react/employee-org-chart-tree.jsonnpx shadcn@latest add @uipkge-react/employee-org-chart-treeInstalls to:components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
title | string | — | optional |
subtitle | string | — | optional |
orgData | EmployeeNode | — | optional |
initialSelectedId | string | — | optional |
initialDepartment | string | — | optional |
initialSearch | string | — | optional |
initialExpandedIds | string[] | — | optional |
initialDrawerOpen | boolean | — | optional |
className | string | — | optional |
Type aliases exported from this item's source. Use these to shape the data you pass in.
DepartmentCountinterface DepartmentCount {
label: string
value: string
count: number
}EmployeeNodeinterface EmployeeNode {
id: string
name: string
role: string
department: 'Executive' | 'Engineering' | 'Design' | 'Product' | 'Operations'
email: string
phone: string
location: string
avatar: string
initials: string
reportsCount: number
teamHeadcount: number
managerId?: string
managerName?: string
managerRole?: string
startDate: string
tenure: string
bio: string
skills: string[]
status: 'active' | 'on-leave'
children?: EmployeeNode[]
}'use client'
import * as React from 'react'
import { cn } from '@/lib/utils'
import { EmployeeProfileDrawer } from './EmployeeProfileDrawer'
import { OrgChartNodeCard, getDeptBadgeClasses } from './OrgChartNodeCard'
import { OrgChartToolbar } from './OrgChartToolbar'
import { DEFAULT_ORG_DATA } from './org-chart-data'
import type { EmployeeNode } from './org-chart-types'
export type { EmployeeNode }
export interface EmployeeOrgChartTreeProps {
title?: string
subtitle?: string
orgData?: EmployeeNode
initialSelectedId?: string
initialDepartment?: string
initialSearch?: string
initialExpandedIds?: string[]
initialDrawerOpen?: boolean
className?: string
}
function flatten(node: EmployeeNode): EmployeeNode[] {
const result: EmployeeNode[] = [node]
if (node.children) {
for (const child of node.children) {
result.push(...flatten(child))
}
}
return result
}
export function EmployeeOrgChartTree({
title = 'Company Organizational Chart',
subtitle = '148 Employees across 5 Departments',
orgData = DEFAULT_ORG_DATA,
initialSelectedId = 'emp-ceo',
initialDepartment = 'All',
initialSearch = '',
initialExpandedIds = ['emp-ceo', 'emp-eng-vp', 'emp-prod-vp', 'emp-ops-vp'],
initialDrawerOpen = true,
className,
}: EmployeeOrgChartTreeProps) {
const canvasRef = React.useRef<HTMLDivElement | null>(null)
React.useEffect(() => {
const el = canvasRef.current
if (el) el.scrollLeft = Math.max(0, (el.scrollWidth - el.clientWidth) / 2)
}, [])
const allEmployees = React.useMemo(() => flatten(orgData), [orgData])
const [selectedEmployeeId, setSelectedEmployeeId] = React.useState<string>(initialSelectedId)
const [selectedDepartment, setSelectedDepartment] = React.useState<string>(initialDepartment)
const [searchQuery, setSearchQuery] = React.useState<string>(initialSearch)
const [isDrawerOpen, setIsDrawerOpen] = React.useState<boolean>(initialDrawerOpen)
const [zoomLevel, setZoomLevel] = React.useState<number>(100)
const [emailCopied, setEmailCopied] = React.useState<boolean>(false)
const [expandedIds, setExpandedIds] = React.useState<Set<string>>(new Set(initialExpandedIds))
const departmentCounts = React.useMemo(
() => [
{ label: 'All', value: 'All', count: allEmployees.length },
{
label: 'Executive',
value: 'Executive',
count: allEmployees.filter((e) => e.department === 'Executive').length,
},
{
label: 'Engineering',
value: 'Engineering',
count: allEmployees.filter((e) => e.department === 'Engineering').length,
},
{ label: 'Design', value: 'Design', count: allEmployees.filter((e) => e.department === 'Design').length },
{ label: 'Product', value: 'Product', count: allEmployees.filter((e) => e.department === 'Product').length },
{
label: 'Operations',
value: 'Operations',
count: allEmployees.filter((e) => e.department === 'Operations').length,
},
],
[allEmployees],
)
const selectedEmployee = React.useMemo(() => {
return allEmployees.find((e) => e.id === selectedEmployeeId) || orgData
}, [allEmployees, selectedEmployeeId, orgData])
const matchingEmployeeIds = React.useMemo(() => {
const q = searchQuery.trim().toLowerCase()
if (!q) return new Set<string>()
const matches = new Set<string>()
for (const emp of allEmployees) {
if (
emp.name.toLowerCase().includes(q) ||
emp.role.toLowerCase().includes(q) ||
emp.department.toLowerCase().includes(q) ||
emp.location.toLowerCase().includes(q) ||
emp.skills.some((s) => s.toLowerCase().includes(q))
) {
matches.add(emp.id)
}
}
return matches
}, [allEmployees, searchQuery])
React.useEffect(() => {
if (searchQuery.trim()) {
const allParentIds = allEmployees.filter((e) => e.children && e.children.length > 0).map((e) => e.id)
setExpandedIds(new Set(allParentIds))
}
}, [searchQuery, allEmployees])
const isExpanded = (id: string): boolean => expandedIds.has(id)
const toggleExpand = (id: string) => {
setExpandedIds((prev) => {
const next = new Set(prev)
if (next.has(id)) {
next.delete(id)
} else {
next.add(id)
}
return next
})
}
const expandAll = () => {
const allParentIds = allEmployees.filter((e) => e.children && e.children.length > 0).map((e) => e.id)
setExpandedIds(new Set(allParentIds))
}
const collapseAll = () => {
setExpandedIds(new Set())
}
const selectEmployee = (emp: EmployeeNode) => {
setSelectedEmployeeId(emp.id)
setIsDrawerOpen(true)
}
const selectSelectedManager = () => {
const manager = allEmployees.find((e) => e.id === selectedEmployee?.managerId)
if (manager) selectEmployee(manager)
}
const copyEmail = (email: string) => {
navigator.clipboard.writeText(email)
setEmailCopied(true)
setTimeout(() => {
setEmailCopied(false)
}, 2000)
}
const adjustZoom = (delta: number) => {
setZoomLevel((prev) => Math.min(130, Math.max(70, prev + delta)))
}
const resetZoom = () => {
setZoomLevel(100)
}
const isNodeDimmed = (node: EmployeeNode): boolean => {
if (selectedDepartment !== 'All') {
if (selectedDepartment === 'Executive' && node.department !== 'Executive') return true
if (selectedDepartment === 'Engineering' && node.department !== 'Engineering' && node.department !== 'Executive')
return true
if (selectedDepartment === 'Design' && node.department !== 'Design' && node.department !== 'Executive')
return true
if (selectedDepartment === 'Product' && node.department !== 'Product' && node.department !== 'Executive')
return true
if (selectedDepartment === 'Operations' && node.department !== 'Operations' && node.department !== 'Executive')
return true
}
if (searchQuery.trim().length > 0) {
const isDirectMatch = matchingEmployeeIds.has(node.id)
const hasMatchingDescendant = flatten(node).some((n) => matchingEmployeeIds.has(n.id))
return !isDirectMatch && !hasMatchingDescendant
}
return false
}
const isNodeHighlighted = (node: EmployeeNode): boolean => {
if (searchQuery.trim().length > 0) {
return matchingEmployeeIds.has(node.id)
}
return false
}
return (
<div
data-slot="employee-org-chart-tree"
className={cn('bg-background border-border flex flex-col rounded-xl border shadow-xs', className)}
>
{/* Header & Toolbar Controls */}
<OrgChartToolbar
title={title}
subtitle={subtitle}
searchQuery={searchQuery}
isDrawerOpen={isDrawerOpen}
departmentCounts={departmentCounts}
selectedDepartment={selectedDepartment}
matchingCount={matchingEmployeeIds.size}
zoomLevel={zoomLevel}
onSearchChange={setSearchQuery}
onToggleDrawer={() => setIsDrawerOpen((prev) => !prev)}
onDepartmentSelect={setSelectedDepartment}
onExpandAll={expandAll}
onCollapseAll={collapseAll}
onAdjustZoom={adjustZoom}
onResetZoom={resetZoom}
/>
{/* Main Content: Canvas & Quick Info Drawer */}
<div className="relative flex flex-1 flex-col lg:flex-row">
{/* Tree Hierarchy Node Canvas */}
<div ref={canvasRef} className="bg-muted/10 relative flex-1 overflow-x-auto overflow-y-visible p-6 md:p-10">
<div
className="flex min-w-max flex-col items-center transition-transform duration-200"
style={{ transform: `scale(${zoomLevel / 100})`, transformOrigin: 'top center' }}
>
{/* LEVEL 0: CEO Node */}
<div className="flex flex-col items-center">
<OrgChartNodeCard
node={orgData}
isSelected={selectedEmployeeId === orgData.id}
isHighlighted={isNodeHighlighted(orgData)}
isDimmed={isNodeDimmed(orgData)}
isExpanded={isExpanded(orgData.id)}
reportsLabel="Direct reports: 3 VPs"
headcountLabel={`${orgData.teamHeadcount} total org`}
expandLabel="Expand Executive Team"
collapseLabel="Collapse Executive Team"
onSelect={selectEmployee}
onToggleExpand={toggleExpand}
/>
{/* Connector stem down from CEO */}
{isExpanded(orgData.id) && Boolean(orgData.children?.length) && <div className="bg-border h-8 w-px" />}
{/* LEVEL 1: VPs and Directors */}
{isExpanded(orgData.id) && Boolean(orgData.children?.length) && (
<div className="relative flex items-start gap-10 pt-0">
{/* Horizontal branching rail connecting Level 1 nodes */}
{(orgData.children?.length ?? 0) > 1 && (
<div className="bg-border absolute top-0 right-[16.666%] left-[16.666%] h-px" />
)}
{/* Iterate Level 1 VPs */}
{orgData.children?.map((vp) => (
<div key={vp.id} className="flex flex-col items-center">
{/* Top stem connecting rail to VP card */}
<div className="bg-border h-8 w-px" />
{/* VP Node Card */}
<OrgChartNodeCard
node={vp}
isSelected={selectedEmployeeId === vp.id}
isHighlighted={isNodeHighlighted(vp)}
isDimmed={isNodeDimmed(vp)}
isExpanded={isExpanded(vp.id)}
reportsLabel={`Direct reports: ${vp.reportsCount} teams`}
headcountLabel={`${vp.teamHeadcount} staff`}
expandLabel={`Expand (${vp.children?.length || 0} Leads)`}
collapseLabel="Collapse Branch"
onSelect={selectEmployee}
onToggleExpand={toggleExpand}
/>
{/* Connector down from VP */}
{isExpanded(vp.id) && Boolean(vp.children?.length) && <div className="bg-border h-8 w-px" />}
{/* LEVEL 2: Staff Leads & Managers */}
{isExpanded(vp.id) && Boolean(vp.children?.length) && (
<div className="relative flex items-start gap-6 pt-0">
{/* Horizontal rail for Level 2 nodes */}
{vp.children?.length === 3 && (
<div className="bg-border absolute top-0 right-[16.666%] left-[16.666%] h-px" />
)}
{vp.children?.length === 2 && (
<div className="bg-border absolute top-0 right-[25%] left-[25%] h-px" />
)}
{/* Iterate Level 2 Leads */}
{vp.children?.map((lead) => (
<div key={lead.id} className="flex flex-col items-center">
<div className="bg-border h-8 w-px" />
{/* Lead Node Card */}
<OrgChartNodeCard
node={lead}
isSelected={selectedEmployeeId === lead.id}
isHighlighted={isNodeHighlighted(lead)}
isDimmed={isNodeDimmed(lead)}
isExpanded={isExpanded(lead.id)}
reportsLabel={`${lead.reportsCount} reports`}
headcountLabel={`${lead.teamHeadcount} members`}
expandLabel={`Team (${lead.children?.length || 0})`}
collapseLabel="Collapse"
onSelect={selectEmployee}
onToggleExpand={toggleExpand}
/>
{/* Connector down from Lead */}
{isExpanded(lead.id) && Boolean(lead.children?.length) && (
<div className="bg-border h-8 w-px" />
)}
{/* LEVEL 3: Team ICs / Senior Contributors */}
{isExpanded(lead.id) && Boolean(lead.children?.length) && (
<div className="relative flex items-start gap-4 pt-0">
{lead.children?.length === 2 && (
<div className="bg-border absolute top-0 right-[25%] left-[25%] h-px" />
)}
{/* Iterate Level 3 ICs */}
{lead.children?.map((ic) => (
<div key={ic.id} className="flex flex-col items-center">
<div className="bg-border h-8 w-px" />
{/* IC Card */}
<OrgChartNodeCard
node={ic}
isSelected={selectedEmployeeId === ic.id}
isHighlighted={isNodeHighlighted(ic)}
isDimmed={isNodeDimmed(ic)}
reportsLabel={`${ic.reportsCount} direct`}
headcountLabel={`${ic.teamHeadcount} member`}
onSelect={selectEmployee}
/>
</div>
))}
</div>
)}
</div>
))}
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
</div>
{/* Quick Info Side Profile Drawer */}
{isDrawerOpen && (
<EmployeeProfileDrawer
employee={selectedEmployee}
emailCopied={emailCopied}
onCopyEmail={copyEmail}
getDeptBadgeClasses={getDeptBadgeClasses}
onClose={() => setIsDrawerOpen(false)}
onSelectEmployee={selectEmployee}
onViewManager={selectSelectedManager}
/>
)}
</div>
</div>
)
}
'use client'
import { ArrowRight, Briefcase, Calendar, Check, Copy, Mail, MapPin, User, Users, X } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import type { EmployeeNode } from './org-chart-types'
export interface EmployeeProfileDrawerProps {
employee: EmployeeNode
emailCopied: boolean
onCopyEmail: (email: string) => void
onClose: () => void
getDeptBadgeClasses: (dept: string) => string
onSelectEmployee: (employee: EmployeeNode) => void
onViewManager: (managerId: string | undefined) => void
}
export function EmployeeProfileDrawer({
employee,
emailCopied,
onCopyEmail,
onClose,
getDeptBadgeClasses,
onSelectEmployee,
onViewManager,
}: EmployeeProfileDrawerProps) {
return (
<div className="border-border bg-card flex w-full shrink-0 flex-col border-t lg:w-96 lg:border-t-0 lg:border-l">
{/* Drawer Header */}
<div className="border-border flex items-center justify-between border-b p-4">
<div className="flex items-center gap-2">
<User className="text-primary size-4" />
<h3 className="text-foreground text-sm font-semibold">Employee Profile</h3>
</div>
<Button
variant="ghost"
size="icon"
className="size-7"
aria-label="Close profile drawer"
onClick={() => onClose()}
>
<X className="size-4" />
</Button>
</div>
{/* Drawer Body */}
<div className="max-h-[700px] flex-1 space-y-5 overflow-y-auto p-5">
{/* Profile Identity Header */}
<div className="flex items-start gap-3.5">
<div className="relative">
<Avatar className="border-border ring-background size-14 border ring-2">
<AvatarImage src={employee.avatar} alt={employee.name} />
<AvatarFallback>{employee.initials}</AvatarFallback>
</Avatar>
<span className="ring-background bg-success absolute right-0 bottom-0 size-3 rounded-full ring-2" />
</div>
<div className="min-w-0 flex-1">
<h4 className="text-foreground text-base font-semibold tracking-tight">{employee.name}</h4>
<p className="text-muted-foreground text-xs">{employee.role}</p>
<div className="mt-2 flex flex-wrap items-center gap-1.5">
<Badge variant="outline" className={cn('text-xs font-medium', getDeptBadgeClasses(employee.department))}>
{employee.department}
</Badge>
<Badge variant="secondary" className="text-xs font-normal">
Active · Full-time
</Badge>
</div>
</div>
</div>
{/* Quick Action Buttons */}
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
<Button asChild variant="outline" size="sm" className="h-8 gap-1.5 text-xs">
<a href={`mailto:${employee.email}`}>
<Mail className="size-3.5" />
<span>Send Email</span>
</a>
</Button>
<Button
variant="outline"
size="sm"
className="h-8 gap-1.5 text-xs"
onClick={() => onCopyEmail(employee.email)}
>
{emailCopied ? <Check className="text-success size-3.5" /> : <Copy className="size-3.5" />}
<span>{emailCopied ? 'Copied!' : 'Copy Email'}</span>
</Button>
</div>
<Separator />
{/* Bio / Leadership Scope */}
<div className="space-y-1.5">
<h5 className="text-foreground text-xs font-semibold tracking-wide uppercase">About & Focus</h5>
<p className="text-muted-foreground text-xs leading-relaxed">{employee.bio}</p>
</div>
{/* Key Details Grid */}
<div className="space-y-1.5">
<h5 className="text-foreground text-xs font-semibold tracking-wide uppercase">Overview Details</h5>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
<div className="bg-muted/40 border-border/60 rounded-lg border p-2.5">
<div className="text-muted-foreground flex items-center gap-1 text-xs">
<Users className="size-3" />
<span>Team Scope</span>
</div>
<p className="text-foreground mt-1 text-xs font-semibold">{employee.teamHeadcount} Members</p>
</div>
<div className="bg-muted/40 border-border/60 rounded-lg border p-2.5">
<div className="text-muted-foreground flex items-center gap-1 text-xs">
<MapPin className="size-3" />
<span>Location</span>
</div>
<p className="text-foreground mt-1 truncate text-xs font-semibold">{employee.location}</p>
</div>
<div className="bg-muted/40 border-border/60 rounded-lg border p-2.5">
<div className="text-muted-foreground flex items-center gap-1 text-xs">
<Calendar className="size-3" />
<span>Tenure</span>
</div>
<p className="text-foreground mt-1 text-xs font-semibold">{employee.tenure}</p>
</div>
<div className="bg-muted/40 border-border/60 rounded-lg border p-2.5">
<div className="text-muted-foreground flex items-center gap-1 text-xs">
<Briefcase className="size-3" />
<span>Phone</span>
</div>
<p className="text-foreground mt-1 truncate text-xs font-semibold">{employee.phone}</p>
</div>
</div>
</div>
{/* Reporting Manager Context (if applicable) */}
{employee.managerName && (
<div className="space-y-1.5">
<h5 className="text-foreground text-xs font-semibold tracking-wide uppercase">Reports To</h5>
<div className="border-border/80 bg-muted/20 flex items-center justify-between rounded-lg border p-2.5">
<div className="min-w-0">
<p className="text-foreground text-xs font-medium">{employee.managerName}</p>
<p className="text-muted-foreground text-xs">{employee.managerRole}</p>
</div>
<Button
variant="ghost"
size="sm"
className="h-7 gap-1 text-xs"
onClick={() => onViewManager(employee.managerId)}
>
<span>View</span>
<ArrowRight className="size-3" />
</Button>
</div>
</div>
)}
{/* Direct Reports Roster (if has children) */}
{employee.children && employee.children.length > 0 && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h5 className="text-foreground text-xs font-semibold tracking-wide uppercase">
Direct Reports ({employee.children.length})
</h5>
<span className="text-muted-foreground text-xs font-medium">{employee.reportsCount} reporting teams</span>
</div>
<div className="space-y-1.5">
{employee.children.map((sub) => (
<div
key={sub.id}
className="border-border/60 bg-muted/20 hover:bg-muted/40 flex cursor-pointer items-center justify-between rounded-lg border p-2 transition-colors"
onClick={() => onSelectEmployee(sub)}
>
<div className="flex min-w-0 items-center gap-2">
<Avatar className="border-border size-7 border">
<AvatarImage src={sub.avatar} alt={sub.name} />
<AvatarFallback>{sub.initials}</AvatarFallback>
</Avatar>
<div className="min-w-0">
<p className="text-foreground truncate text-xs font-medium">{sub.name}</p>
<p className="text-muted-foreground truncate text-xs">{sub.role}</p>
</div>
</div>
<Badge
variant="outline"
className={cn('shrink-0 px-1.5 py-0 text-xs', getDeptBadgeClasses(sub.department))}
>
{sub.teamHeadcount} staff
</Badge>
</div>
))}
</div>
</div>
)}
{/* Skills & Competencies */}
<div className="space-y-1.5">
<h5 className="text-foreground text-xs font-semibold tracking-wide uppercase">Core Competencies</h5>
<div className="flex flex-wrap gap-1.5">
{employee.skills.map((skill) => (
<Badge key={skill} variant="secondary" className="text-xs font-normal">
{skill}
</Badge>
))}
</div>
</div>
</div>
</div>
)
}
'use client'
import * as React from 'react'
import { Building2, Minus, Plus, RotateCcw, Search, User, X, ZoomIn, ZoomOut } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Separator } from '@/components/ui/separator'
export interface DepartmentCount {
label: string
value: string
count: number
}
export interface OrgChartToolbarProps {
title: string
subtitle: string
searchQuery: string
isDrawerOpen: boolean
departmentCounts: DepartmentCount[]
selectedDepartment: string
matchingCount: number
zoomLevel: number
onSearchChange: (value: string) => void
onToggleDrawer: () => void
onDepartmentSelect: (dept: string) => void
onExpandAll: () => void
onCollapseAll: () => void
onAdjustZoom: (delta: number) => void
onResetZoom: () => void
}
export function OrgChartToolbar({
title,
subtitle,
searchQuery,
isDrawerOpen,
departmentCounts,
selectedDepartment,
matchingCount,
zoomLevel,
onSearchChange,
onToggleDrawer,
onDepartmentSelect,
onExpandAll,
onCollapseAll,
onAdjustZoom,
onResetZoom,
}: OrgChartToolbarProps) {
return (
<div>
{/* Header Section */}
<div className="border-border flex flex-col gap-4 border-b p-5 md:flex-row md:items-center md:justify-between">
<div>
<div className="flex items-center gap-2">
<div className="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg">
<Building2 className="size-4" />
</div>
<h2 className="text-foreground text-lg font-semibold tracking-tight">{title}</h2>
</div>
<p className="text-muted-foreground mt-1 text-xs">{subtitle}</p>
</div>
{/* Header Action Controls */}
<div className="flex flex-wrap items-center gap-2">
{/* Search input */}
<div className="relative max-w-xs min-w-[220px]">
<Search className="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" />
<Input
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
placeholder="Search employee, title, skill..."
className="h-8 pr-7 pl-8 text-xs"
/>
{searchQuery && (
<button
type="button"
className="text-muted-foreground hover:text-foreground absolute top-1/2 right-2 -translate-y-1/2"
aria-label="Clear search"
onClick={() => onSearchChange('')}
>
<X className="size-3.5" />
</button>
)}
</div>
<Separator orientation="vertical" className="hidden h-6 md:block" />
{/* Expand / Collapse All */}
<Button variant="outline" size="sm" className="h-8 gap-1.5 text-xs" onClick={onExpandAll}>
<Plus className="size-3.5" />
<span>Expand All</span>
</Button>
<Button variant="outline" size="sm" className="h-8 gap-1.5 text-xs" onClick={onCollapseAll}>
<Minus className="size-3.5" />
<span>Collapse All</span>
</Button>
{/* Profile Drawer Toggle Button */}
<Button
variant="outline"
size="sm"
className={cn('h-8 gap-1.5 text-xs', isDrawerOpen && 'bg-accent text-accent-foreground')}
onClick={onToggleDrawer}
>
<User className="size-3.5" />
<span>{isDrawerOpen ? 'Hide Profile' : 'View Profile'}</span>
</Button>
</div>
</div>
{/* Filter & Toolbar Bar */}
<div className="border-border bg-muted/20 flex flex-wrap items-center justify-between gap-3 border-b px-5 py-3">
{/* Department Filter Pills */}
<div className="flex flex-wrap items-center gap-1.5">
<span className="text-muted-foreground mr-1 text-xs font-medium">Department:</span>
{departmentCounts.map((dept) => (
<button
key={dept.value}
type="button"
className={cn(
'focus-visible:ring-ring inline-flex h-7 items-center gap-1.5 rounded-full px-2.5 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',
selectedDepartment === dept.value
? 'bg-primary text-primary-foreground shadow-xs'
: 'bg-background hover:bg-muted text-muted-foreground border-border border',
)}
onClick={() => onDepartmentSelect(dept.value)}
>
<span>{dept.label}</span>
<span
className={cn(
'py-0.2 rounded-full px-1.5 text-xs',
selectedDepartment === dept.value
? 'bg-primary-foreground/20 text-primary-foreground'
: 'bg-muted text-muted-foreground',
)}
>
{{ dept: dept.count }.dept}
</span>
</button>
))}
</div>
{/* Zoom and Search Banner */}
<div className="flex items-center gap-2">
{searchQuery.trim() && (
<div className="text-success text-xs">
Found {matchingCount} matching member{matchingCount === 1 ? '' : 's'}
</div>
)}
<div className="bg-background border-border flex items-center rounded-lg border p-0.5 shadow-2xs">
<Button
variant="ghost"
size="icon"
className="size-6 text-xs"
disabled={zoomLevel <= 70}
aria-label="Zoom out"
onClick={() => onAdjustZoom(-10)}
>
<ZoomOut className="size-3" />
</Button>
<span className="text-muted-foreground w-10 text-center text-xs font-medium">{zoomLevel}%</span>
<Button
variant="ghost"
size="icon"
className="size-6 text-xs"
disabled={zoomLevel >= 130}
aria-label="Zoom in"
onClick={() => onAdjustZoom(10)}
>
<ZoomIn className="size-3" />
</Button>
<Separator orientation="vertical" className="mx-0.5 h-3.5" />
<Button
variant="ghost"
size="icon"
className="size-6 text-xs"
aria-label="Reset zoom"
onClick={onResetZoom}
>
<RotateCcw className="size-3" />
</Button>
</div>
</div>
</div>
</div>
)
}
'use client'
import * as React from 'react'
import { ChevronDown, ChevronRight, Mail, MapPin, Users } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import type { EmployeeNode } from './org-chart-types'
export function getDeptBadgeClasses(dept: string): string {
switch (dept) {
case 'Executive':
return 'border-chart-1/30 bg-chart-1/10 text-chart-1'
case 'Engineering':
return 'border-chart-2/30 bg-chart-2/10 text-chart-2'
case 'Design':
return 'border-chart-3/30 bg-chart-3/10 text-chart-3'
case 'Product':
return 'border-chart-4/30 bg-chart-4/10 text-chart-4'
case 'Operations':
return 'border-chart-5/30 bg-chart-5/10 text-chart-5'
default:
return 'border-border bg-muted text-muted-foreground'
}
}
export function getDeptTopStripClasses(dept: string): string {
switch (dept) {
case 'Executive':
return 'bg-chart-1'
case 'Engineering':
return 'bg-chart-2'
case 'Design':
return 'bg-chart-3'
case 'Product':
return 'bg-chart-4'
case 'Operations':
return 'bg-chart-5'
default:
return 'bg-muted-foreground'
}
}
export interface OrgChartNodeCardProps {
node: EmployeeNode
isSelected: boolean
isHighlighted: boolean
isDimmed: boolean
isExpanded?: boolean
reportsLabel?: string
headcountLabel?: string
expandLabel?: string
collapseLabel?: string
onSelect: (node: EmployeeNode) => void
onToggleExpand?: (id: string) => void
}
export function OrgChartNodeCard({
node,
isSelected,
isHighlighted,
isDimmed,
isExpanded = false,
reportsLabel,
headcountLabel,
expandLabel,
collapseLabel,
onSelect,
onToggleExpand,
}: OrgChartNodeCardProps) {
return (
<div
className={cn(
'bg-card border-border hover:border-primary/50 relative w-72 cursor-pointer rounded-xl border text-left shadow-xs transition-colors duration-200 hover:shadow-md',
isSelected && 'ring-primary border-primary bg-primary/[0.02] ring-2',
isHighlighted && 'border-success bg-success/10/20 ring-success dark:bg-success/10 ring-2',
isDimmed && 'opacity-35 grayscale-[25%]',
)}
tabIndex={0}
role="button"
aria-label={`Select ${node.name}`}
onClick={() => onSelect(node)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onSelect(node)
}
}}
>
{/* Top department accent strip */}
<div className={cn('h-1 w-full rounded-t-xl', getDeptTopStripClasses(node.department))} />
<div className="space-y-3 p-4">
{/* Identity row */}
<div className="flex items-start justify-between gap-2.5">
<div className="flex min-w-0 items-center gap-2.5">
<div className="relative">
<Avatar className="border-border ring-background size-10 border ring-1">
<AvatarImage src={node.avatar} alt={node.name} />
<AvatarFallback>{node.initials}</AvatarFallback>
</Avatar>
<span className="ring-background bg-success absolute right-0 bottom-0 size-2.5 rounded-full ring-2" />
</div>
<div className="min-w-0">
<h4 className="text-foreground truncate text-sm font-semibold">{node.name}</h4>
<p className="text-muted-foreground truncate text-xs">{node.role}</p>
</div>
</div>
<Badge variant="outline" className={cn('shrink-0 text-xs font-normal', getDeptBadgeClasses(node.department))}>
{node.department}
</Badge>
</div>
{/* Contact & Location info */}
<div className="text-muted-foreground space-y-1 text-xs">
<div className="flex items-center gap-1.5">
<MapPin className="size-3.5 shrink-0" />
<span className="truncate">{node.location}</span>
</div>
<div className="flex items-center gap-1.5">
<Mail className="size-3.5 shrink-0" />
<span className="truncate">{node.email}</span>
</div>
</div>
{/* Team Reports Count Pill */}
<div className="bg-muted/60 flex items-center justify-between rounded-lg px-2.5 py-1.5 text-xs">
<div className="text-foreground flex items-center gap-1.5 font-medium">
<Users className="text-muted-foreground size-3.5" />
<span>{reportsLabel || `Direct reports: ${node.reportsCount}`}</span>
</div>
<span className="text-muted-foreground text-xs">{headcountLabel || `${node.teamHeadcount} staff`}</span>
</div>
{/* Expand/Collapse Button (if has children) */}
{node.children && node.children.length > 0 && onToggleExpand && (
<Button
variant="ghost"
size="sm"
className="text-muted-foreground hover:text-foreground h-7 w-full justify-between text-xs font-medium"
onClick={(e) => {
e.stopPropagation()
onToggleExpand(node.id)
}}
>
<span className="flex items-center gap-1.5">
{isExpanded ? <ChevronDown className="size-3.5" /> : <ChevronRight className="size-3.5" />}
<span>
{isExpanded ? collapseLabel || 'Collapse Branch' : expandLabel || `Expand (${node.children.length})`}
</span>
</span>
<span className="bg-muted rounded px-1.5 py-0.5 text-xs">{node.children.length}</span>
</Button>
)}
</div>
</div>
)
}
export interface EmployeeNode {
id: string
name: string
role: string
department: 'Executive' | 'Engineering' | 'Design' | 'Product' | 'Operations'
email: string
phone: string
location: string
avatar: string
initials: string
reportsCount: number
teamHeadcount: number
managerId?: string
managerName?: string
managerRole?: string
startDate: string
tenure: string
bio: string
skills: string[]
status: 'active' | 'on-leave'
children?: EmployeeNode[]
}
export function getDeptBadgeClasses(dept: string): string {
switch (dept) {
case 'Executive':
return 'border-chart-1/30 bg-chart-1/10 text-chart-1'
case 'Engineering':
return 'border-chart-2/30 bg-chart-2/10 text-chart-2'
case 'Design':
return 'border-chart-3/30 bg-chart-3/10 text-chart-3'
case 'Product':
return 'border-chart-4/30 bg-chart-4/10 text-chart-4'
case 'Operations':
return 'border-chart-5/30 bg-chart-5/10 text-chart-5'
default:
return 'border-border bg-muted text-muted-foreground'
}
}
export function getDeptTopStripClasses(dept: string): string {
switch (dept) {
case 'Executive':
return 'bg-chart-1'
case 'Engineering':
return 'bg-chart-2'
case 'Design':
return 'bg-chart-3'
case 'Product':
return 'bg-chart-4'
case 'Operations':
return 'bg-chart-5'
default:
return 'bg-muted-foreground'
}
}
import type { EmployeeNode } from './org-chart-types'
export const DEFAULT_ORG_DATA: EmployeeNode = {
id: 'emp-ceo',
name: 'Sarah Jenkins',
role: 'Chief Executive Officer',
department: 'Executive',
email: '[email protected]',
phone: '+1 (415) 890-1200',
location: 'San Francisco, CA (HQ)',
avatar: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=160&auto=format&fit=crop&q=80',
initials: 'SJ',
reportsCount: 3,
teamHeadcount: 148,
startDate: 'January 2019',
tenure: '5 yrs 8 mos',
bio: 'Directs company-wide vision, long-range enterprise roadmap, capital allocation, and executive leadership across 5 global divisions.',
skills: ['Executive Leadership', 'Corporate Strategy', 'Enterprise SaaS', 'M&A', 'Culture'],
status: 'active',
children: [
{
id: 'emp-eng-vp',
name: 'Marcus Vance',
role: 'VP of Engineering',
department: 'Engineering',
email: '[email protected]',
phone: '+1 (212) 745-9921',
location: 'New York, NY',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=160&auto=format&fit=crop&q=80',
initials: 'MV',
reportsCount: 3,
teamHeadcount: 64,
managerId: 'emp-ceo',
managerName: 'Sarah Jenkins',
managerRole: 'Chief Executive Officer',
startDate: 'March 2020',
tenure: '4 yrs 6 mos',
bio: 'Leads 64 engineers across platform architecture, frontend infrastructure, and Site Reliability Engineering with 99.99% uptime SLA.',
skills: ['Distributed Systems', 'Cloud Architecture', 'Tech Strategy', 'Kubernetes', 'Scalability'],
status: 'active',
children: [
{
id: 'emp-eng-lead-1',
name: 'Alex Rivera',
role: 'Principal Architect',
department: 'Engineering',
email: '[email protected]',
phone: '+1 (512) 634-1109',
location: 'Austin, TX',
avatar: 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=160&auto=format&fit=crop&q=80',
initials: 'AR',
reportsCount: 2,
teamHeadcount: 18,
managerId: 'emp-eng-vp',
managerName: 'Marcus Vance',
managerRole: 'VP of Engineering',
startDate: 'September 2020',
tenure: '4 yrs',
bio: 'Designs core distributed event pipelines, data mesh architecture, and multi-tenant database infrastructure.',
skills: ['Go', 'Kafka', 'PostgreSQL', 'System Architecture', 'Event Streaming'],
status: 'active',
children: [
{
id: 'emp-eng-ic-1',
name: 'Kai Zhang',
role: 'Senior Backend Engineer',
department: 'Engineering',
email: '[email protected]',
phone: '+1 (512) 634-1188',
location: 'Austin, TX',
avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=160&auto=format&fit=crop&q=80',
initials: 'KZ',
reportsCount: 0,
teamHeadcount: 1,
managerId: 'emp-eng-lead-1',
managerName: 'Alex Rivera',
managerRole: 'Principal Architect',
startDate: 'January 2022',
tenure: '2 yrs 8 mos',
bio: 'Builds low-latency gRPC services, consensus layers, and distributed caching protocols.',
skills: ['Rust', 'gRPC', 'Raft', 'Redis', 'High Throughput'],
status: 'active',
},
{
id: 'emp-eng-ic-2',
name: 'Hannah Schmidt',
role: 'Staff Database Architect',
department: 'Engineering',
email: '[email protected]',
phone: '+1 (512) 634-1192',
location: 'Remote, US',
avatar: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=160&auto=format&fit=crop&q=80',
initials: 'HS',
reportsCount: 0,
teamHeadcount: 1,
managerId: 'emp-eng-lead-1',
managerName: 'Alex Rivera',
managerRole: 'Principal Architect',
startDate: 'May 2021',
tenure: '3 yrs 4 mos',
bio: 'Specializes in multi-master replication, query optimization, and automated sharding migrations.',
skills: ['PostgreSQL', 'CockroachDB', 'Data Warehousing', 'Query Tuning'],
status: 'active',
},
],
},
{
id: 'emp-eng-lead-2',
name: 'Sofia Rossi',
role: 'Staff Frontend Lead',
department: 'Engineering',
email: '[email protected]',
phone: '+49 30 2219 4481',
location: 'Berlin, Germany',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=160&auto=format&fit=crop&q=80',
initials: 'SR',
reportsCount: 2,
teamHeadcount: 24,
managerId: 'emp-eng-vp',
managerName: 'Marcus Vance',
managerRole: 'VP of Engineering',
startDate: 'November 2020',
tenure: '3 yrs 10 mos',
bio: 'Leads the unified component design system, micro-frontend architecture, and web accessibility standards.',
skills: ['Vue 3', 'React', 'Design Systems', 'Web Performance', 'WCAG AA'],
status: 'active',
children: [
{
id: 'emp-eng-ic-3',
name: 'Leo Garcia',
role: 'Senior UI Engineer',
department: 'Engineering',
email: '[email protected]',
phone: '+49 30 2219 4490',
location: 'Berlin, Germany',
avatar: 'https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?w=160&auto=format&fit=crop&q=80',
initials: 'LG',
reportsCount: 0,
teamHeadcount: 1,
managerId: 'emp-eng-lead-2',
managerName: 'Sofia Rossi',
managerRole: 'Staff Frontend Lead',
startDate: 'February 2022',
tenure: '2 yrs 7 mos',
bio: 'Focuses on complex interactive data visualization canvases, charts, and keyboard shortcuts engine.',
skills: ['TypeScript', 'Tailwind CSS', 'D3.js', 'Canvas API'],
status: 'active',
},
{
id: 'emp-eng-ic-4',
name: 'Nina Patel',
role: 'Frontend Platform Engineer',
department: 'Engineering',
email: '[email protected]',
phone: '+44 20 8123 9940',
location: 'London, UK',
avatar: 'https://images.unsplash.com/photo-1573497019940-1c28c88b4f3e?w=160&auto=format&fit=crop&q=80',
initials: 'NP',
reportsCount: 0,
teamHeadcount: 1,
managerId: 'emp-eng-lead-2',
managerName: 'Sofia Rossi',
managerRole: 'Staff Frontend Lead',
startDate: 'October 2022',
tenure: '1 yr 11 mos',
bio: 'Manages automated bundle size budgets, CI build plugins, and island rendering hydration pipelines.',
skills: ['Vite', 'Turborepo', 'Playwright', 'ESBuild'],
status: 'active',
},
],
},
{
id: 'emp-eng-lead-3',
name: 'Tariq Mansour',
role: 'DevOps & SRE Lead',
department: 'Engineering',
email: '[email protected]',
phone: '+1 (415) 555-8910',
location: 'San Francisco, CA',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=160&auto=format&fit=crop&q=80',
initials: 'TM',
reportsCount: 2,
teamHeadcount: 22,
managerId: 'emp-eng-vp',
managerName: 'Marcus Vance',
managerRole: 'VP of Engineering',
startDate: 'January 2021',
tenure: '3 yrs 8 mos',
bio: 'Directs cloud infrastructure resilience, multi-region Kubernetes clusters, and automated zero-downtime rollouts.',
skills: ['AWS', 'Terraform', 'Kubernetes', 'CI/CD', 'Observability'],
status: 'active',
children: [
{
id: 'emp-eng-ic-5',
name: 'Kiran Rao',
role: 'Senior SRE Engineer',
department: 'Engineering',
email: '[email protected]',
phone: '+1 (415) 555-8933',
location: 'San Francisco, CA',
avatar: 'https://images.unsplash.com/photo-1522075469751-3a6694fb2f61?w=160&auto=format&fit=crop&q=80',
initials: 'KR',
reportsCount: 0,
teamHeadcount: 1,
managerId: 'emp-eng-lead-3',
managerName: 'Tariq Mansour',
managerRole: 'DevOps & SRE Lead',
startDate: 'August 2022',
tenure: '2 yrs 1 mo',
bio: 'Maintains Prometheus/Grafana telemetry stacks, SLO/SLI tracking dashboards, and disaster drills.',
skills: ['Prometheus', 'Grafana', 'OpenTelemetry', 'Chaos Engineering'],
status: 'active',
},
{
id: 'emp-eng-ic-6',
name: 'Elena Dubois',
role: 'Cloud Security Specialist',
department: 'Engineering',
email: '[email protected]',
phone: '+33 1 42 68 55 00',
location: 'Paris, France',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=160&auto=format&fit=crop&q=80',
initials: 'ED',
reportsCount: 0,
teamHeadcount: 1,
managerId: 'emp-eng-lead-3',
managerName: 'Tariq Mansour',
managerRole: 'DevOps & SRE Lead',
startDate: 'March 2023',
tenure: '1 yr 6 mos',
bio: 'Owns zero-trust cloud network topology, HashiCorp Vault key rotation, and automated CVE vulnerability audits.',
skills: ['Vault', 'OIDC', 'IAM Hardening', 'SOC 2 Type II'],
status: 'active',
},
],
},
],
},
{
id: 'emp-prod-vp',
name: 'Elena Rostova',
role: 'VP of Product & Design',
department: 'Product',
email: '[email protected]',
phone: '+44 20 7946 0912',
location: 'London, UK',
avatar: 'https://images.unsplash.com/photo-1580489944761-15a19d654956?w=160&auto=format&fit=crop&q=80',
initials: 'ER',
reportsCount: 2,
teamHeadcount: 40,
managerId: 'emp-ceo',
managerName: 'Sarah Jenkins',
managerRole: 'Chief Executive Officer',
startDate: 'June 2020',
tenure: '4 yrs 3 mos',
bio: 'Guides end-to-end product vision, quarterly roadmap discovery cycles, and customer experience excellence across enterprise tools.',
skills: ['Product Strategy', 'Design Leadership', 'User Research', 'PLG', 'Enterprise UX'],
status: 'active',
children: [
{
id: 'emp-prod-lead-1',
name: 'Maya Patel',
role: 'Head of Product Design',
department: 'Design',
email: '[email protected]',
phone: '+44 20 7946 0988',
location: 'London, UK',
avatar: 'https://images.unsplash.com/photo-1567532939604-b6b5b0db2604?w=160&auto=format&fit=crop&q=80',
initials: 'MP',
reportsCount: 2,
teamHeadcount: 18,
managerId: 'emp-prod-vp',
managerName: 'Elena Rostova',
managerRole: 'VP of Product & Design',
startDate: 'February 2021',
tenure: '3 yrs 7 mos',
bio: 'Leads our 18-member UI/UX and design research studio, creating intuitive interfaces for data-intensive enterprise users.',
skills: ['Figma', 'UX Research', 'Design Systems', 'Design Operations'],
status: 'active',
children: [
{
id: 'emp-des-ic-1',
name: 'Aria Thorne',
role: 'Lead UX Researcher',
department: 'Design',
email: '[email protected]',
phone: '+1 (415) 321-7789',
location: 'San Francisco, CA',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=160&auto=format&fit=crop&q=80',
initials: 'AT',
reportsCount: 0,
teamHeadcount: 1,
managerId: 'emp-prod-lead-1',
managerName: 'Maya Patel',
managerRole: 'Head of Product Design',
startDate: 'July 2022',
tenure: '2 yrs 2 mos',
bio: 'Conducts qualitative enterprise customer interview loops, card sorting, and workflow pain-point mapping.',
skills: ['Qualitative Analysis', 'Usability Testing', 'Personas', 'Journey Mapping'],
status: 'active',
},
{
id: 'emp-des-ic-2',
name: 'Lucas Silva',
role: 'Senior Interaction Designer',
department: 'Design',
email: '[email protected]',
phone: '+351 21 098 7654',
location: 'Lisbon, Portugal',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=160&auto=format&fit=crop&q=80',
initials: 'LS',
reportsCount: 0,
teamHeadcount: 1,
managerId: 'emp-prod-lead-1',
managerName: 'Maya Patel',
managerRole: 'Head of Product Design',
startDate: 'November 2022',
tenure: '1 yr 10 mos',
bio: 'Architects micro-interactions, spring animation specs, and tactile feedback patterns across web and desktop.',
skills: ['Framer', 'Prototyping', 'Micro-interactions', 'Token Systems'],
status: 'active',
},
],
},
{
id: 'emp-prod-lead-2',
name: 'Liam Tanaka',
role: 'Principal Product Manager',
department: 'Product',
email: '[email protected]',
phone: '+81 3 5555 0142',
location: 'Tokyo, Japan',
avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=160&auto=format&fit=crop&q=80',
initials: 'LT',
reportsCount: 2,
teamHeadcount: 22,
managerId: 'emp-prod-vp',
managerName: 'Elena Rostova',
managerRole: 'VP of Product & Design',
startDate: 'August 2021',
tenure: '3 yrs 1 mo',
bio: 'Spearheads core enterprise workflow automation, AI copilots, and monetization experiments across global markets.',
skills: ['Product Roadmapping', 'AI Integration', 'Data Analytics', 'Enterprise Growth'],
status: 'active',
children: [
{
id: 'emp-prod-ic-1',
name: 'Devon Vance',
role: 'Senior Technical PM',
department: 'Product',
email: '[email protected]',
phone: '+1 (212) 745-3390',
location: 'New York, NY',
avatar: 'https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?w=160&auto=format&fit=crop&q=80',
initials: 'DV',
reportsCount: 0,
teamHeadcount: 1,
managerId: 'emp-prod-lead-2',
managerName: 'Liam Tanaka',
managerRole: 'Principal Product Manager',
startDate: 'April 2023',
tenure: '1 yr 5 mos',
bio: 'Defines developer API contracts, webhook architectures, and 3rd party integration partner specifications.',
skills: ['API Specs', 'OpenAPI', 'Webhooks', 'Partner Integrations'],
status: 'active',
},
{
id: 'emp-prod-ic-2',
name: 'Sarah Lin',
role: 'Product Analytics Lead',
department: 'Product',
email: '[email protected]',
phone: '+81 3 5555 0199',
location: 'Tokyo, Japan',
avatar: 'https://images.unsplash.com/photo-1573497019940-1c28c88b4f3e?w=160&auto=format&fit=crop&q=80',
initials: 'SL',
reportsCount: 0,
teamHeadcount: 1,
managerId: 'emp-prod-lead-2',
managerName: 'Liam Tanaka',
managerRole: 'Principal Product Manager',
startDate: 'January 2023',
tenure: '1 yr 8 mos',
bio: 'Models customer retention cohorts, feature usage drop-off rates, and self-service conversion funnels.',
skills: ['SQL', 'Mixpanel', 'Cohort Retention', 'A/B Testing'],
status: 'active',
},
],
},
],
},
{
id: 'emp-ops-vp',
name: 'David Chen',
role: 'VP of Operations & Finance',
department: 'Operations',
email: '[email protected]',
phone: '+65 6789 0123',
location: 'Singapore',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=160&auto=format&fit=crop&q=80',
initials: 'DC',
reportsCount: 2,
teamHeadcount: 44,
managerId: 'emp-ceo',
managerName: 'Sarah Jenkins',
managerRole: 'Chief Executive Officer',
startDate: 'August 2021',
tenure: '3 yrs 1 mo',
bio: 'Manages worldwide financial planning, global entity setup, real estate facilities, and international compliance operations.',
skills: ['Financial Modeling', 'Corporate Finance', 'Global Compliance', 'Treasury', 'Operations'],
status: 'active',
children: [
{
id: 'emp-ops-lead-1',
name: 'Chloe Dupont',
role: 'Director of Finance',
department: 'Operations',
email: '[email protected]',
phone: '+33 1 42 68 90 12',
location: 'Paris, France',
avatar: 'https://images.unsplash.com/photo-1580489944761-15a19d654956?w=160&auto=format&fit=crop&q=80',
initials: 'CD',
reportsCount: 2,
teamHeadcount: 20,
managerId: 'emp-ops-vp',
managerName: 'David Chen',
managerRole: 'VP of Operations & Finance',
startDate: 'March 2022',
tenure: '2 yrs 6 mos',
bio: 'Leads FP&A, annual budgeting models, GAAP audits, and international cross-border tax compliance.',
skills: ['FP&A', 'GAAP Audits', 'Treasury Management', 'Tax Modeling'],
status: 'active',
children: [
{
id: 'emp-ops-ic-1',
name: 'Arthur Leclerc',
role: 'Senior Financial Analyst',
department: 'Operations',
email: '[email protected]',
phone: '+33 1 42 68 90 33',
location: 'Paris, France',
avatar: 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=160&auto=format&fit=crop&q=80',
initials: 'AL',
reportsCount: 0,
teamHeadcount: 1,
managerId: 'emp-ops-lead-1',
managerName: 'Chloe Dupont',
managerRole: 'Director of Finance',
startDate: 'November 2022',
tenure: '1 yr 10 mos',
bio: 'Analyzes recurring SaaS revenue trends, customer lifetime values, and department OPEX burn rates.',
skills: ['Financial Forecasting', 'Excel Modeling', 'ARR Accounting'],
status: 'active',
},
{
id: 'emp-ops-ic-2',
name: 'Zoe Martinez',
role: 'Global Payroll & Benefits Lead',
department: 'Operations',
email: '[email protected]',
phone: '+34 91 555 0177',
location: 'Madrid, Spain',
avatar: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=160&auto=format&fit=crop&q=80',
initials: 'ZM',
reportsCount: 0,
teamHeadcount: 1,
managerId: 'emp-ops-lead-1',
managerName: 'Chloe Dupont',
managerRole: 'Director of Finance',
startDate: 'June 2023',
tenure: '1 yr 3 mos',
bio: 'Oversees payroll runs across 12 countries, international equity plans, and local pension schemes.',
skills: ['Global Payroll', 'Equity Schemes', 'Statutory Benefits'],
status: 'active',
},
],
},
{
id: 'emp-ops-lead-2',
name: 'James Wilson',
role: 'Director of Global Operations',
department: 'Operations',
email: '[email protected]',
phone: '+1 (212) 745-8812',
location: 'New York, NY',
avatar: 'https://images.unsplash.com/photo-1522075469751-3a6694fb2f61?w=160&auto=format&fit=crop&q=80',
initials: 'JW',
reportsCount: 2,
teamHeadcount: 24,
managerId: 'emp-ops-vp',
managerName: 'David Chen',
managerRole: 'VP of Operations & Finance',
startDate: 'May 2022',
tenure: '2 yrs 4 mos',
bio: 'Oversees global facilities, vendor procurement relationships, workplace experience, and corporate IT hardware fleets.',
skills: ['Vendor Negotiations', 'Facilities Management', 'Asset Tracking', 'Workplace Ops'],
status: 'active',
children: [
{
id: 'emp-ops-ic-3',
name: 'Rachel Green',
role: 'Workplace Experience Manager',
department: 'Operations',
email: '[email protected]',
phone: '+1 (212) 745-8833',
location: 'New York, NY',
avatar: 'https://images.unsplash.com/photo-1567532939604-b6b5b0db2604?w=160&auto=format&fit=crop&q=80',
initials: 'RG',
reportsCount: 0,
teamHeadcount: 1,
managerId: 'emp-ops-lead-2',
managerName: 'James Wilson',
managerRole: 'Director of Global Operations',
startDate: 'January 2023',
tenure: '1 yr 8 mos',
bio: 'Coordinates global office events, collaborative space design, catering vendors, and team onsites.',
skills: ['Event Planning', 'Space Management', 'Culture Initiatives'],
status: 'active',
},
{
id: 'emp-ops-ic-4',
name: 'Vikram Seth',
role: 'IT Systems & Security Lead',
department: 'Operations',
email: '[email protected]',
phone: '+65 6789 0188',
location: 'Singapore',
avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=160&auto=format&fit=crop&q=80',
initials: 'VS',
reportsCount: 0,
teamHeadcount: 1,
managerId: 'emp-ops-lead-2',
managerName: 'James Wilson',
managerRole: 'Director of Global Operations',
startDate: 'October 2022',
tenure: '1 yr 11 mos',
bio: 'Manages enterprise MDM configurations, Okta SSO provisioning, and hardware lifecycle deployment.',
skills: ['Okta SSO', 'Jamf MDM', 'Endpoint Security', 'Hardware Logistics'],
status: 'active',
},
],
},
],
},
],
}
Raw manifest:https://uipkge.dev/r/react/employee-org-chart-tree.json