
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 React ->$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/employee-org-chart-tree.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/employee-org-chart-tree.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/employee-org-chart-tree.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/employee-org-chart-tree.jsonnpx shadcn-vue@latest add @uipkge/employee-org-chart-treeInstalls to:app/components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
title | string | 'Company Organizational Chart' | optional |
subtitle | string | '148 Employees across 5 Departments' | optional |
orgData | EmployeeNode | () => DEFAULT_ORG_DATA, initialSelectedId: 'emp-ceo', ini… | optional |
initialSelectedId | string | — | optional |
initialDepartment | string | — | optional |
initialSearch | string | — | optional |
initialExpandedIds | string[] | — | optional |
initialDrawerOpen | boolean | — | optional |
class | HTMLAttributes['class'] | — | 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[]
}<script setup lang="ts">
import { computed, onMounted, ref, watch, type HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import EmployeeProfileDrawer from './EmployeeProfileDrawer.vue'
import OrgChartNodeCard from './OrgChartNodeCard.vue'
import OrgChartToolbar from './OrgChartToolbar.vue'
import { DEFAULT_ORG_DATA } from './org-chart-data'
import type { EmployeeNode } from './org-chart-types'
import { getDeptBadgeClasses } from './org-chart-types'
export type { EmployeeNode }
interface Props {
title?: string
subtitle?: string
orgData?: EmployeeNode
initialSelectedId?: string
initialDepartment?: string
initialSearch?: string
initialExpandedIds?: string[]
initialDrawerOpen?: boolean
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<Props>(), {
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,
})
const canvasRef = ref<HTMLDivElement | null>(null)
onMounted(() => {
const el = canvasRef.value
if (el) el.scrollLeft = Math.max(0, (el.scrollWidth - el.clientWidth) / 2)
})
function flatten(node: EmployeeNode): EmployeeNode[] {
const result: EmployeeNode[] = [node]
if (node.children) {
for (const child of node.children) {
result.push(...flatten(child))
}
}
return result
}
const allEmployees = computed(() => flatten(props.orgData))
const selectedEmployeeId = ref<string>(props.initialSelectedId)
const selectedDepartment = ref<string>(props.initialDepartment)
const searchQuery = ref<string>(props.initialSearch)
const isDrawerOpen = ref<boolean>(props.initialDrawerOpen)
const zoomLevel = ref<number>(100)
const emailCopied = ref<boolean>(false)
const expandedIds = ref<Set<string>>(new Set(props.initialExpandedIds))
const departmentCounts = computed(() => [
{ label: 'All', value: 'All', count: allEmployees.value.length },
{
label: 'Executive',
value: 'Executive',
count: allEmployees.value.filter((e) => e.department === 'Executive').length,
},
{
label: 'Engineering',
value: 'Engineering',
count: allEmployees.value.filter((e) => e.department === 'Engineering').length,
},
{ label: 'Design', value: 'Design', count: allEmployees.value.filter((e) => e.department === 'Design').length },
{ label: 'Product', value: 'Product', count: allEmployees.value.filter((e) => e.department === 'Product').length },
{
label: 'Operations',
value: 'Operations',
count: allEmployees.value.filter((e) => e.department === 'Operations').length,
},
])
const selectedEmployee = computed(() => {
return allEmployees.value.find((e) => e.id === selectedEmployeeId.value) || props.orgData
})
const matchingEmployeeIds = computed(() => {
const q = searchQuery.value.trim().toLowerCase()
if (!q) return new Set<string>()
const matches = new Set<string>()
for (const emp of allEmployees.value) {
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
})
watch(
() => searchQuery.value,
(q) => {
if (q.trim()) {
const allParentIds = allEmployees.value.filter((e) => e.children && e.children.length > 0).map((e) => e.id)
expandedIds.value = new Set(allParentIds)
}
},
)
function isExpanded(id: string): boolean {
return expandedIds.value.has(id)
}
function toggleExpand(id: string) {
const updated = new Set(expandedIds.value)
if (updated.has(id)) {
updated.delete(id)
} else {
updated.add(id)
}
expandedIds.value = updated
}
function expandAll() {
const allParentIds = allEmployees.value.filter((e) => e.children && e.children.length > 0).map((e) => e.id)
expandedIds.value = new Set(allParentIds)
}
function collapseAll() {
expandedIds.value = new Set()
}
function selectEmployee(emp: EmployeeNode) {
selectedEmployeeId.value = emp.id
isDrawerOpen.value = true
}
function selectSelectedManager() {
const manager = allEmployees.value.find((e) => e.id === selectedEmployee.value?.managerId)
if (manager) selectEmployee(manager)
}
function copyEmail(email: string) {
navigator.clipboard.writeText(email)
emailCopied.value = true
setTimeout(() => {
emailCopied.value = false
}, 2000)
}
function adjustZoom(delta: number) {
zoomLevel.value = Math.min(130, Math.max(70, zoomLevel.value + delta))
}
function resetZoom() {
zoomLevel.value = 100
}
function isNodeDimmed(node: EmployeeNode): boolean {
if (selectedDepartment.value !== 'All') {
if (selectedDepartment.value === 'Executive' && node.department !== 'Executive') return true
if (
selectedDepartment.value === 'Engineering' &&
node.department !== 'Engineering' &&
node.department !== 'Executive'
)
return true
if (selectedDepartment.value === 'Design' && node.department !== 'Design' && node.department !== 'Executive')
return true
if (selectedDepartment.value === 'Product' && node.department !== 'Product' && node.department !== 'Executive')
return true
if (
selectedDepartment.value === 'Operations' &&
node.department !== 'Operations' &&
node.department !== 'Executive'
)
return true
}
if (searchQuery.value.trim().length > 0) {
const isDirectMatch = matchingEmployeeIds.value.has(node.id)
const hasMatchingDescendant = flatten(node).some((n) => matchingEmployeeIds.value.has(n.id))
return !isDirectMatch && !hasMatchingDescendant
}
return false
}
function isNodeHighlighted(node: EmployeeNode): boolean {
if (searchQuery.value.trim().length > 0) {
return matchingEmployeeIds.value.has(node.id)
}
return false
}
</script>
<template>
<div
data-slot="employee-org-chart-tree"
:class="cn('bg-background border-border flex flex-col rounded-xl border shadow-xs', props.class)"
>
<!-- Header & Toolbar Controls -->
<OrgChartToolbar
:title="props.title"
:subtitle="props.subtitle"
:search-query="searchQuery"
:is-drawer-open="isDrawerOpen"
:department-counts="departmentCounts"
:selected-department="selectedDepartment"
:matching-count="matchingEmployeeIds.size"
:zoom-level="zoomLevel"
@update:search-query="searchQuery = $event"
@update:is-drawer-open="isDrawerOpen = $event"
@update:selected-department="selectedDepartment = $event"
@expand-all="expandAll"
@collapse-all="collapseAll"
@adjust-zoom="adjustZoom"
@reset-zoom="resetZoom"
/>
<!-- Main Content: Canvas & Quick Info Drawer -->
<div class="relative flex flex-1 flex-col lg:flex-row">
<!-- Tree Hierarchy Node Canvas -->
<div ref="canvasRef" class="bg-muted/10 relative flex-1 overflow-x-auto overflow-y-visible p-6 md:p-10">
<div
class="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 class="flex flex-col items-center">
<OrgChartNodeCard
:node="orgData"
:is-selected="selectedEmployeeId === orgData.id"
:is-highlighted="isNodeHighlighted(orgData)"
:is-dimmed="isNodeDimmed(orgData)"
:is-expanded="isExpanded(orgData.id)"
reports-label="Direct reports: 3 VPs"
:headcount-label="`${orgData.teamHeadcount} total org`"
expand-label="Expand Executive Team"
collapse-label="Collapse Executive Team"
@select="selectEmployee"
@toggle-expand="toggleExpand"
/>
<!-- Connector stem down from CEO -->
<div v-if="isExpanded(orgData.id) && orgData.children?.length" class="bg-border h-8 w-px" />
<!-- LEVEL 1: VPs and Directors -->
<div
v-if="isExpanded(orgData.id) && orgData.children?.length"
class="relative flex items-start gap-10 pt-0"
>
<!-- Horizontal branching rail connecting Level 1 nodes -->
<div
v-if="orgData.children.length > 1"
class="bg-border absolute top-0 right-[16.666%] left-[16.666%] h-px"
/>
<!-- Iterate Level 1 VPs -->
<div v-for="vp in orgData.children" :key="vp.id" class="flex flex-col items-center">
<!-- Top stem connecting rail to VP card -->
<div class="bg-border h-8 w-px" />
<!-- VP Node Card -->
<OrgChartNodeCard
:node="vp"
:is-selected="selectedEmployeeId === vp.id"
:is-highlighted="isNodeHighlighted(vp)"
:is-dimmed="isNodeDimmed(vp)"
:is-expanded="isExpanded(vp.id)"
:reports-label="`Direct reports: ${vp.reportsCount} teams`"
:headcount-label="`${vp.teamHeadcount} staff`"
:expand-label="`Expand (${vp.children?.length || 0} Leads)`"
collapse-label="Collapse Branch"
@select="selectEmployee"
@toggle-expand="toggleExpand"
/>
<!-- Connector down from VP -->
<div v-if="isExpanded(vp.id) && vp.children?.length" class="bg-border h-8 w-px" />
<!-- LEVEL 2: Staff Leads & Managers -->
<div v-if="isExpanded(vp.id) && vp.children?.length" class="relative flex items-start gap-6 pt-0">
<!-- Horizontal rail for Level 2 nodes -->
<div
v-if="vp.children.length === 3"
class="bg-border absolute top-0 right-[16.666%] left-[16.666%] h-px"
/>
<div
v-else-if="vp.children.length === 2"
class="bg-border absolute top-0 right-[25%] left-[25%] h-px"
/>
<!-- Iterate Level 2 Leads -->
<div v-for="lead in vp.children" :key="lead.id" class="flex flex-col items-center">
<div class="bg-border h-8 w-px" />
<!-- Lead Node Card -->
<OrgChartNodeCard
:node="lead"
:is-selected="selectedEmployeeId === lead.id"
:is-highlighted="isNodeHighlighted(lead)"
:is-dimmed="isNodeDimmed(lead)"
:is-expanded="isExpanded(lead.id)"
:reports-label="`${lead.reportsCount} reports`"
:headcount-label="`${lead.teamHeadcount} members`"
:expand-label="`Team (${lead.children?.length || 0})`"
collapse-label="Collapse"
@select="selectEmployee"
@toggle-expand="toggleExpand"
/>
<!-- Connector down from Lead -->
<div v-if="isExpanded(lead.id) && lead.children?.length" class="bg-border h-8 w-px" />
<!-- LEVEL 3: Team ICs / Senior Contributors -->
<div
v-if="isExpanded(lead.id) && lead.children?.length"
class="relative flex items-start gap-4 pt-0"
>
<div
v-if="lead.children.length === 2"
class="bg-border absolute top-0 right-[25%] left-[25%] h-px"
/>
<!-- Iterate Level 3 ICs -->
<div v-for="ic in lead.children" :key="ic.id" class="flex flex-col items-center">
<div class="bg-border h-8 w-px" />
<!-- IC Card -->
<OrgChartNodeCard
:node="ic"
:is-selected="selectedEmployeeId === ic.id"
:is-highlighted="isNodeHighlighted(ic)"
:is-dimmed="isNodeDimmed(ic)"
:reports-label="`${ic.reportsCount} direct`"
:headcount-label="`${ic.teamHeadcount} member`"
@select="selectEmployee"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Quick Info Side Profile Drawer -->
<EmployeeProfileDrawer
v-if="isDrawerOpen"
:employee="selectedEmployee"
:email-copied="emailCopied"
:copy-email="copyEmail"
:get-dept-badge-classes="getDeptBadgeClasses"
@close="isDrawerOpen = false"
@select-employee="selectEmployee"
@view-manager="selectSelectedManager"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ArrowRight, Briefcase, Calendar, Check, Copy, Mail, MapPin, User, Users, X } from 'lucide-vue-next'
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'
interface Props {
employee: EmployeeNode
emailCopied: boolean
copyEmail: (email: string) => void
getDeptBadgeClasses: (dept: string) => string
}
defineProps<Props>()
defineEmits<{
close: []
'select-employee': [employee: EmployeeNode]
'view-manager': []
}>()
</script>
<template>
<div class="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 class="border-border flex items-center justify-between border-b p-4">
<div class="flex items-center gap-2">
<User class="text-primary size-4" />
<h3 class="text-foreground text-sm font-semibold">Employee Profile</h3>
</div>
<Button variant="ghost" size="icon" class="size-7" aria-label="Close profile drawer" @click="$emit('close')">
<X class="size-4" />
</Button>
</div>
<!-- Drawer Body -->
<div class="max-h-[700px] flex-1 space-y-5 overflow-y-auto p-5">
<!-- Profile Identity Header -->
<div class="flex items-start gap-3.5">
<div class="relative">
<Avatar class="border-border ring-background size-14 border ring-2">
<AvatarImage :src="employee.avatar" :alt="employee.name" />
<AvatarFallback>{{ employee.initials }}</AvatarFallback>
</Avatar>
<span class="ring-background bg-success absolute right-0 bottom-0 size-3 rounded-full ring-2" />
</div>
<div class="min-w-0 flex-1">
<h4 class="text-foreground text-base font-semibold tracking-tight">{{ employee.name }}</h4>
<p class="text-muted-foreground text-xs">{{ employee.role }}</p>
<div class="mt-2 flex flex-wrap items-center gap-1.5">
<Badge variant="outline" :class="cn('text-xs font-medium', getDeptBadgeClasses(employee.department))">
{{ employee.department }}
</Badge>
<Badge variant="secondary" class="text-xs font-normal"> Active · Full-time </Badge>
</div>
</div>
</div>
<!-- Quick Action Buttons -->
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
<Button as-child variant="outline" size="sm" class="h-8 gap-1.5 text-xs">
<a :href="`mailto:${employee.email}`">
<Mail class="size-3.5" />
<span>Send Email</span>
</a>
</Button>
<Button variant="outline" size="sm" class="h-8 gap-1.5 text-xs" @click="copyEmail(employee.email)">
<Check v-if="emailCopied" class="text-success size-3.5" />
<Copy v-else class="size-3.5" />
<span>{{ emailCopied ? 'Copied!' : 'Copy Email' }}</span>
</Button>
</div>
<Separator />
<!-- Bio / Leadership Scope -->
<div class="space-y-1.5">
<h5 class="text-foreground text-xs font-semibold tracking-wide uppercase">About & Focus</h5>
<p class="text-muted-foreground text-xs leading-relaxed">
{{ employee.bio }}
</p>
</div>
<!-- Key Details Grid -->
<div class="space-y-1.5">
<h5 class="text-foreground text-xs font-semibold tracking-wide uppercase">Overview Details</h5>
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
<div class="bg-muted/40 border-border/60 rounded-lg border p-2.5">
<div class="text-muted-foreground flex items-center gap-1 text-xs">
<Users class="size-3" />
<span>Team Scope</span>
</div>
<p class="text-foreground mt-1 text-xs font-semibold">{{ employee.teamHeadcount }} Members</p>
</div>
<div class="bg-muted/40 border-border/60 rounded-lg border p-2.5">
<div class="text-muted-foreground flex items-center gap-1 text-xs">
<MapPin class="size-3" />
<span>Location</span>
</div>
<p class="text-foreground mt-1 truncate text-xs font-semibold">
{{ employee.location }}
</p>
</div>
<div class="bg-muted/40 border-border/60 rounded-lg border p-2.5">
<div class="text-muted-foreground flex items-center gap-1 text-xs">
<Calendar class="size-3" />
<span>Tenure</span>
</div>
<p class="text-foreground mt-1 text-xs font-semibold">
{{ employee.tenure }}
</p>
</div>
<div class="bg-muted/40 border-border/60 rounded-lg border p-2.5">
<div class="text-muted-foreground flex items-center gap-1 text-xs">
<Briefcase class="size-3" />
<span>Phone</span>
</div>
<p class="text-foreground mt-1 truncate text-xs font-semibold">
{{ employee.phone }}
</p>
</div>
</div>
</div>
<!-- Reporting Manager Context (if applicable) -->
<div v-if="employee.managerName" class="space-y-1.5">
<h5 class="text-foreground text-xs font-semibold tracking-wide uppercase">Reports To</h5>
<div class="border-border/80 bg-muted/20 flex items-center justify-between rounded-lg border p-2.5">
<div class="min-w-0">
<p class="text-foreground text-xs font-medium">{{ employee.managerName }}</p>
<p class="text-muted-foreground text-xs">{{ employee.managerRole }}</p>
</div>
<Button variant="ghost" size="sm" class="h-7 gap-1 text-xs" @click="$emit('view-manager')">
<span>View</span>
<ArrowRight class="size-3" />
</Button>
</div>
</div>
<!-- Direct Reports Roster (if has children) -->
<div v-if="employee.children?.length" class="space-y-2">
<div class="flex items-center justify-between">
<h5 class="text-foreground text-xs font-semibold tracking-wide uppercase">
Direct Reports ({{ employee.children.length }})
</h5>
<span class="text-muted-foreground text-xs font-medium">{{ employee.reportsCount }} reporting teams</span>
</div>
<div class="space-y-1.5">
<div
v-for="sub in employee.children"
:key="sub.id"
class="border-border/60 bg-muted/20 hover:bg-muted/40 flex cursor-pointer items-center justify-between rounded-lg border p-2 transition-colors"
@click="$emit('select-employee', sub)"
>
<div class="flex min-w-0 items-center gap-2">
<Avatar class="border-border size-7 border">
<AvatarImage :src="sub.avatar" :alt="sub.name" />
<AvatarFallback>{{ sub.initials }}</AvatarFallback>
</Avatar>
<div class="min-w-0">
<p class="text-foreground truncate text-xs font-medium">{{ sub.name }}</p>
<p class="text-muted-foreground truncate text-xs">{{ sub.role }}</p>
</div>
</div>
<Badge variant="outline" :class="cn('shrink-0 px-1.5 py-0 text-xs', getDeptBadgeClasses(sub.department))">
{{ sub.teamHeadcount }} staff
</Badge>
</div>
</div>
</div>
<!-- Skills & Competencies -->
<div class="space-y-1.5">
<h5 class="text-foreground text-xs font-semibold tracking-wide uppercase">Core Competencies</h5>
<div class="flex flex-wrap gap-1.5">
<Badge v-for="skill in employee.skills" :key="skill" variant="secondary" class="text-xs font-normal">
{{ skill }}
</Badge>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Building2, Minus, Plus, RotateCcw, Search, User, X, ZoomIn, ZoomOut } from 'lucide-vue-next'
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
}
interface Props {
title: string
subtitle: string
searchQuery: string
isDrawerOpen: boolean
departmentCounts: DepartmentCount[]
selectedDepartment: string
matchingCount: number
zoomLevel: number
}
defineProps<Props>()
const emit = defineEmits<{
'update:searchQuery': [value: string]
'update:isDrawerOpen': [value: boolean]
'update:selectedDepartment': [value: string]
expandAll: []
collapseAll: []
adjustZoom: [delta: number]
resetZoom: []
}>()
</script>
<template>
<div>
<!-- Header Section -->
<div class="border-border flex flex-col gap-4 border-b p-5 md:flex-row md:items-center md:justify-between">
<div>
<div class="flex items-center gap-2">
<div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg">
<Building2 class="size-4" />
</div>
<h2 class="text-foreground text-lg font-semibold tracking-tight">{{ title }}</h2>
</div>
<p class="text-muted-foreground mt-1 text-xs">
{{ subtitle }}
</p>
</div>
<!-- Header Action Controls -->
<div class="flex flex-wrap items-center gap-2">
<!-- Search input -->
<div class="relative max-w-xs min-w-[220px]">
<Search class="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" />
<Input
:model-value="searchQuery"
placeholder="Search employee, title, skill..."
class="h-8 pr-7 pl-8 text-xs"
@update:model-value="emit('update:searchQuery', String($event))"
/>
<button
v-if="searchQuery"
type="button"
class="text-muted-foreground hover:text-foreground absolute top-1/2 right-2 -translate-y-1/2"
aria-label="Clear search"
@click="emit('update:searchQuery', '')"
>
<X class="size-3.5" />
</button>
</div>
<Separator orientation="vertical" class="hidden h-6 md:block" />
<!-- Expand / Collapse All -->
<Button variant="outline" size="sm" class="h-8 gap-1.5 text-xs" @click="emit('expandAll')">
<Plus class="size-3.5" />
<span>Expand All</span>
</Button>
<Button variant="outline" size="sm" class="h-8 gap-1.5 text-xs" @click="emit('collapseAll')">
<Minus class="size-3.5" />
<span>Collapse All</span>
</Button>
<!-- Profile Drawer Toggle Button -->
<Button
variant="outline"
size="sm"
:class="cn('h-8 gap-1.5 text-xs', isDrawerOpen && 'bg-accent text-accent-foreground')"
@click="emit('update:isDrawerOpen', !isDrawerOpen)"
>
<User class="size-3.5" />
<span>{{ isDrawerOpen ? 'Hide Profile' : 'View Profile' }}</span>
</Button>
</div>
</div>
<!-- Filter & Toolbar Bar -->
<div class="border-border bg-muted/20 flex flex-wrap items-center justify-between gap-3 border-b px-5 py-3">
<!-- Department Filter Pills -->
<div class="flex flex-wrap items-center gap-1.5">
<span class="text-muted-foreground mr-1 text-xs font-medium">Department:</span>
<button
v-for="dept in departmentCounts"
:key="dept.value"
type="button"
:class="
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',
)
"
@click="emit('update:selectedDepartment', dept.value)"
>
<span>{{ dept.label }}</span>
<span
:class="
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.count }}
</span>
</button>
</div>
<!-- Zoom and Search Banner -->
<div class="flex items-center gap-2">
<div v-if="searchQuery.trim()" class="text-success text-xs">
Found {{ matchingCount }} matching member{{ matchingCount === 1 ? '' : 's' }}
</div>
<div class="bg-background border-border flex items-center rounded-lg border p-0.5 shadow-2xs">
<Button
variant="ghost"
size="icon"
class="size-6 text-xs"
:disabled="zoomLevel <= 70"
aria-label="Zoom out"
@click="emit('adjustZoom', -10)"
>
<ZoomOut class="size-3" />
</Button>
<span class="text-muted-foreground w-10 text-center text-xs font-medium">{{ zoomLevel }}%</span>
<Button
variant="ghost"
size="icon"
class="size-6 text-xs"
:disabled="zoomLevel >= 130"
aria-label="Zoom in"
@click="emit('adjustZoom', 10)"
>
<ZoomIn class="size-3" />
</Button>
<Separator orientation="vertical" class="mx-0.5 h-3.5" />
<Button variant="ghost" size="icon" class="size-6 text-xs" aria-label="Reset zoom" @click="emit('resetZoom')">
<RotateCcw class="size-3" />
</Button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ChevronDown, ChevronRight, Mail, MapPin, Users } from 'lucide-vue-next'
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'
import { getDeptBadgeClasses, getDeptTopStripClasses } from './org-chart-types'
interface Props {
node: EmployeeNode
isSelected: boolean
isHighlighted: boolean
isDimmed: boolean
isExpanded?: boolean
reportsLabel?: string
headcountLabel?: string
expandLabel?: string
collapseLabel?: string
}
withDefaults(defineProps<Props>(), {
isExpanded: false,
reportsLabel: '',
headcountLabel: '',
expandLabel: '',
collapseLabel: '',
})
const emit = defineEmits<{
select: [node: EmployeeNode]
toggleExpand: [id: string]
}>()
</script>
<template>
<div
:class="
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}`"
@click="emit('select', node)"
@keydown.enter="emit('select', node)"
@keydown.space.prevent="emit('select', node)"
>
<!-- Top department accent strip -->
<div :class="cn('h-1 w-full rounded-t-xl', getDeptTopStripClasses(node.department))" />
<div class="space-y-3 p-4">
<!-- Identity row -->
<div class="flex items-start justify-between gap-2.5">
<div class="flex min-w-0 items-center gap-2.5">
<div class="relative">
<Avatar class="border-border ring-background size-10 border ring-1">
<AvatarImage :src="node.avatar" :alt="node.name" />
<AvatarFallback>{{ node.initials }}</AvatarFallback>
</Avatar>
<span class="ring-background bg-success absolute right-0 bottom-0 size-2.5 rounded-full ring-2" />
</div>
<div class="min-w-0">
<h4 class="text-foreground truncate text-sm font-semibold">{{ node.name }}</h4>
<p class="text-muted-foreground truncate text-xs">{{ node.role }}</p>
</div>
</div>
<Badge variant="outline" :class="cn('shrink-0 text-xs font-normal', getDeptBadgeClasses(node.department))">
{{ node.department }}
</Badge>
</div>
<!-- Contact & Location info -->
<div class="text-muted-foreground space-y-1 text-xs">
<div class="flex items-center gap-1.5">
<MapPin class="size-3.5 shrink-0" />
<span class="truncate">{{ node.location }}</span>
</div>
<div class="flex items-center gap-1.5">
<Mail class="size-3.5 shrink-0" />
<span class="truncate">{{ node.email }}</span>
</div>
</div>
<!-- Team Reports Count Pill -->
<div class="bg-muted/60 flex items-center justify-between rounded-lg px-2.5 py-1.5 text-xs">
<div class="text-foreground flex items-center gap-1.5 font-medium">
<Users class="text-muted-foreground size-3.5" />
<span>{{ reportsLabel || `Direct reports: ${node.reportsCount}` }}</span>
</div>
<span class="text-muted-foreground text-xs">{{ headcountLabel || `${node.teamHeadcount} staff` }}</span>
</div>
<!-- Expand/Collapse Button (if has children) -->
<Button
v-if="node.children && node.children.length > 0"
variant="ghost"
size="sm"
class="text-muted-foreground hover:text-foreground h-7 w-full justify-between text-xs font-medium"
@click.stop="emit('toggleExpand', node.id)"
>
<span class="flex items-center gap-1.5">
<ChevronDown v-if="isExpanded" class="size-3.5" />
<ChevronRight v-else class="size-3.5" />
<span>{{
isExpanded ? collapseLabel || 'Collapse Branch' : expandLabel || `Expand (${node.children.length})`
}}</span>
</span>
<span class="bg-muted rounded px-1.5 py-0.5 text-xs">{{ node.children.length }}</span>
</Button>
</div>
</div>
</template>
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/vue/employee-org-chart-tree.json