Employee Org Chart Tree
blockhrInteractive 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.
Installation
$pnpm dlx uipkge-ng@latest add employee-org-chart-tree$npx uipkge-ng@latest add employee-org-chart-tree$yarn dlx uipkge-ng@latest add employee-org-chart-tree$bunx uipkge-ng@latest add employee-org-chart-treeManifest:
https://uipkge.dev/r/angular/employee-org-chart-tree.jsonInstalls to:src/app/components/blocks/employee-org-chart-tree/Variants
Loading interactive previews…
Loading interactive previews…
Schema
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[]
}npm dependencies
Files installed (4)
src/app/components/blocks/employee-org-chart-tree/employee-org-chart-tree.component.ts35.4 kB
// 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. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, computed, signal } from '@angular/core' import { ArrowRight, Briefcase, Building2, Calendar, Check, ChevronDown, ChevronRight, Copy, Mail, MapPin, Minus, Plus, RotateCcw, Search, User, Users, X, ZoomIn, ZoomOut, LucideAngularModule, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiAvatarComponent, UiAvatarFallbackComponent, UiAvatarImageComponent } from '@/ui/avatar/avatar.component' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiInputComponent } from '@/ui/input/input.component' import { DEFAULT_ORG_DATA } from './org-chart-data' import { getDeptBadgeClasses, getDeptTopStripClasses, type EmployeeNode } from './org-chart-types' export type { EmployeeNode } 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 interface DepartmentCount { label: string value: string count: number } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-org-chart-node-card', standalone: true, imports: [ LucideAngularModule, UiAvatarComponent, UiAvatarFallbackComponent, UiAvatarImageComponent, UiBadgeComponent, UiButtonComponent, ], template: ` <div [class]="cardClass" tabindex="0" role="button" [attr.aria-label]="'Select ' + node.name" (click)="select.emit(node)" (keydown.enter)="select.emit(node)" (keydown.space)="select.emit(node); $event.preventDefault()" > <!-- Top department accent strip --> <div [class]="cn('h-1 w-full rounded-t-xl', stripClass())"></div> <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"> <ui-avatar class="border-border ring-background size-10 border ring-1"> <ui-avatar-image [src]="node.avatar" [alt]="node.name" /> <ui-avatar-fallback>{{ node.initials }}</ui-avatar-fallback> </ui-avatar> <span class="ring-background bg-success absolute right-0 bottom-0 size-2.5 rounded-full ring-2"></span> </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> <span ui-badge variant="outline" [class]="cn('shrink-0 text-xs font-normal', badgeClass())"> {{ node.department }} </span> </div> <!-- Contact & Location info --> <div class="text-muted-foreground space-y-1 text-xs"> <div class="flex items-center gap-1.5"> <lucide-icon [img]="MapPin" class="size-3.5 shrink-0" /> <span class="truncate">{{ node.location }}</span> </div> <div class="flex items-center gap-1.5"> <lucide-icon [img]="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"> <lucide-icon [img]="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) --> @if (node.children && node.children.length > 0 && toggleExpand.observed) { <button ui-button variant="ghost" size="sm" class="text-muted-foreground hover:text-foreground h-7 w-full justify-between text-xs font-medium" (click)="onToggle($event)" > <span class="flex items-center gap-1.5"> @if (isExpanded) { <lucide-icon [img]="ChevronDown" class="size-3.5" /> } @else { <lucide-icon [img]="ChevronRight" 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> `, }) export class UiOrgChartNodeCardComponent { protected readonly ChevronDown = ChevronDown protected readonly ChevronRight = ChevronRight protected readonly Mail = Mail protected readonly MapPin = MapPin protected readonly Users = Users protected readonly cn = cn @Input() node!: EmployeeNode @Input() isSelected = false @Input() isHighlighted = false @Input() isDimmed = false @Input() isExpanded = false @Input() reportsLabel?: string @Input() headcountLabel?: string @Input() expandLabel?: string @Input() collapseLabel?: string @Output() select = new EventEmitter<EmployeeNode>() @Output() toggleExpand = new EventEmitter<string>() badgeClass(): string { return getDeptBadgeClasses(this.node.department) } stripClass(): string { return getDeptTopStripClasses(this.node.department) } get cardClass(): string { return cn( 'bg-card border-border hover:border-primary/50 focus-visible:ring-ring relative w-72 cursor-pointer rounded-xl border text-left shadow-xs transition-colors duration-200 outline-none hover:shadow-md focus-visible:ring-2', this.isSelected && 'ring-primary border-primary bg-primary/[0.02] ring-2', this.isHighlighted && 'border-success bg-success/10/20 ring-success dark:bg-success/10 ring-2', this.isDimmed && 'opacity-35 grayscale-[25%]', ) } onToggle(event: Event): void { event.stopPropagation() this.toggleExpand.emit(this.node.id) } } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-org-chart-toolbar', standalone: true, imports: [LucideAngularModule, UiButtonComponent, UiInputComponent], 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"> <lucide-icon [img]="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"> <div class="relative max-w-xs min-w-[220px]"> <lucide-icon [img]="Search" class="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" /> <ui-input [value]="searchQuery" (valueChange)="searchChange.emit($event)" placeholder="Search employee, title, skill..." class="h-8 pr-7 pl-8 text-xs" /> @if (searchQuery) { <button type="button" class="text-muted-foreground hover:text-foreground focus-visible:ring-ring absolute top-1/2 right-2 -translate-y-1/2 rounded-sm outline-none focus-visible:ring-2" aria-label="Clear search" (click)="searchChange.emit('')" > <lucide-icon [img]="X" class="size-3.5" /> </button> } </div> <div class="bg-border hidden h-6 w-px md:block"></div> <button ui-button variant="outline" size="sm" class="h-8 gap-1.5 text-xs" (click)="expandAll.emit()"> <lucide-icon [img]="Plus" class="size-3.5" /> <span>Expand All</span> </button> <button ui-button variant="outline" size="sm" class="h-8 gap-1.5 text-xs" (click)="collapseAll.emit()"> <lucide-icon [img]="Minus" class="size-3.5" /> <span>Collapse All</span> </button> <button ui-button variant="outline" size="sm" [class]="cn('h-8 gap-1.5 text-xs', isDrawerOpen && 'bg-accent text-accent-foreground')" (click)="toggleDrawer.emit()" > <lucide-icon [img]="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> @for (dept of departmentCounts; track dept.value) { <button type="button" [class]="pillClass(dept.value)" (click)="departmentSelect.emit(dept.value)"> <span>{{ dept.label }}</span> <span [class]="pillCountClass(dept.value)">{{ dept.count }}</span> </button> } </div> <!-- Zoom and Search Banner --> <div class="flex items-center gap-2"> @if (searchQuery.trim()) { <div 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 ui-button variant="ghost" size="icon" class="size-6 text-xs" [disabled]="zoomLevel <= 70" aria-label="Zoom out" (click)="adjustZoom.emit(-10)" > <lucide-icon [img]="ZoomOut" class="size-3" /> </button> <span class="text-muted-foreground w-10 text-center text-xs font-medium">{{ zoomLevel }}%</span> <button ui-button variant="ghost" size="icon" class="size-6 text-xs" [disabled]="zoomLevel >= 130" aria-label="Zoom in" (click)="adjustZoom.emit(10)" > <lucide-icon [img]="ZoomIn" class="size-3" /> </button> <div class="bg-border mx-0.5 h-3.5 w-px"></div> <button ui-button variant="ghost" size="icon" class="size-6 text-xs" aria-label="Reset zoom" (click)="resetZoom.emit()" > <lucide-icon [img]="RotateCcw" class="size-3" /> </button> </div> </div> </div> </div> `, }) export class UiOrgChartToolbarComponent { protected readonly Building2 = Building2 protected readonly Minus = Minus protected readonly Plus = Plus protected readonly RotateCcw = RotateCcw protected readonly Search = Search protected readonly User = User protected readonly X = X protected readonly ZoomIn = ZoomIn protected readonly ZoomOut = ZoomOut protected readonly cn = cn @Input() title = '' @Input() subtitle = '' @Input() searchQuery = '' @Input() isDrawerOpen = false @Input() departmentCounts: DepartmentCount[] = [] @Input() selectedDepartment = 'All' @Input() matchingCount = 0 @Input() zoomLevel = 100 @Output() searchChange = new EventEmitter<string>() @Output() toggleDrawer = new EventEmitter<void>() @Output() departmentSelect = new EventEmitter<string>() @Output() expandAll = new EventEmitter<void>() @Output() collapseAll = new EventEmitter<void>() @Output() adjustZoom = new EventEmitter<number>() @Output() resetZoom = new EventEmitter<void>() pillClass(value: string): string { return 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', this.selectedDepartment === value ? 'bg-primary text-primary-foreground shadow-xs' : 'bg-background hover:bg-muted text-muted-foreground border-border border', ) } pillCountClass(value: string): string { return cn( 'py-0.2 rounded-full px-1.5 text-xs', this.selectedDepartment === value ? 'bg-primary-foreground/20 text-primary-foreground' : 'bg-muted text-muted-foreground', ) } } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-employee-profile-drawer', standalone: true, imports: [ LucideAngularModule, UiAvatarComponent, UiAvatarFallbackComponent, UiAvatarImageComponent, UiBadgeComponent, UiButtonComponent, ], 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"> <lucide-icon [img]="User" class="text-primary size-4" /> <h3 class="text-foreground text-sm font-semibold">Employee Profile</h3> </div> <button ui-button variant="ghost" size="icon" class="size-7" aria-label="Close profile drawer" (click)="close.emit()" > <lucide-icon [img]="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"> <ui-avatar class="border-border ring-background size-14 border ring-2"> <ui-avatar-image [src]="employee.avatar" [alt]="employee.name" /> <ui-avatar-fallback>{{ employee.initials }}</ui-avatar-fallback> </ui-avatar> <span class="ring-background bg-success absolute right-0 bottom-0 size-3 rounded-full ring-2"></span> </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"> <span ui-badge variant="outline" [class]="cn('text-xs font-medium', deptClass())">{{ employee.department }}</span> <span ui-badge variant="secondary" class="text-xs font-normal">Active · Full-time</span> </div> </div> </div> <!-- Quick Action Buttons --> <div class="grid grid-cols-1 gap-2 sm:grid-cols-2"> <a ui-button variant="outline" size="sm" class="h-8 gap-1.5 text-xs" [href]="'mailto:' + employee.email"> <lucide-icon [img]="Mail" class="size-3.5" /> <span>Send Email</span> </a> <button ui-button variant="outline" size="sm" class="h-8 gap-1.5 text-xs" (click)="copyEmail.emit(employee.email)" > @if (emailCopied) { <lucide-icon [img]="Check" class="text-success size-3.5" /> } @else { <lucide-icon [img]="Copy" class="size-3.5" /> } <span>{{ emailCopied ? 'Copied!' : 'Copy Email' }}</span> </button> </div> <div class="bg-border h-px"></div> <!-- Bio --> <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"> <lucide-icon [img]="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"> <lucide-icon [img]="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"> <lucide-icon [img]="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"> <lucide-icon [img]="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 (employee.managerName) { <div 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 ui-button variant="ghost" size="sm" class="h-7 gap-1 text-xs" (click)="viewManager.emit(employee.managerId)" > <span>View</span> <lucide-icon [img]="ArrowRight" class="size-3" /> </button> </div> </div> } <!-- Direct Reports Roster --> @if (employee.children && employee.children.length > 0) { <div 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"> @for (sub of employee.children; track sub.id) { <div 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)="selectEmployee.emit(sub)" > <div class="flex min-w-0 items-center gap-2"> <ui-avatar class="border-border size-7 border"> <ui-avatar-image [src]="sub.avatar" [alt]="sub.name" /> <ui-avatar-fallback>{{ sub.initials }}</ui-avatar-fallback> </ui-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> <span ui-badge variant="outline" [class]="cn('shrink-0 px-1.5 py-0 text-xs', deptClassFor(sub.department))" > {{ sub.teamHeadcount }} staff </span> </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"> @for (skill of employee.skills; track skill) { <span ui-badge variant="secondary" class="text-xs font-normal">{{ skill }}</span> } </div> </div> </div> </div> `, }) export class UiEmployeeProfileDrawerComponent { protected readonly ArrowRight = ArrowRight protected readonly Briefcase = Briefcase protected readonly Calendar = Calendar protected readonly Check = Check protected readonly Copy = Copy protected readonly Mail = Mail protected readonly MapPin = MapPin protected readonly User = User protected readonly Users = Users protected readonly X = X protected readonly cn = cn @Input() employee!: EmployeeNode @Input() emailCopied = false @Output() copyEmail = new EventEmitter<string>() @Output() close = new EventEmitter<void>() @Output() selectEmployee = new EventEmitter<EmployeeNode>() @Output() viewManager = new EventEmitter<string | undefined>() deptClass(): string { return getDeptBadgeClasses(this.employee.department) } deptClassFor(dept: string): string { return getDeptBadgeClasses(dept) } } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-employee-org-chart-tree, [ui-employee-org-chart-tree]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [UiOrgChartNodeCardComponent, UiOrgChartToolbarComponent, UiEmployeeProfileDrawerComponent], template: ` <div data-slot="employee-org-chart-tree" [class]="rootClass"> <!-- Header & Toolbar Controls --> <ui-org-chart-toolbar [title]="titleState()" [subtitle]="subtitleState()" [searchQuery]="searchQuery()" [isDrawerOpen]="isDrawerOpen()" [departmentCounts]="departmentCounts()" [selectedDepartment]="selectedDepartment()" [matchingCount]="matchingIds().length" [zoomLevel]="zoomLevel()" (searchChange)="onSearchChange($event)" (toggleDrawer)="isDrawerOpen.set(!isDrawerOpen())" (departmentSelect)="selectedDepartment.set($event)" (expandAll)="expandAll()" (collapseAll)="expandedIds.set([])" (adjustZoom)="adjustZoom($event)" (resetZoom)="zoomLevel.set(100)" /> <!-- Main Content: Canvas & Quick Info Drawer --> <div class="relative flex flex-1 flex-col lg:flex-row"> <!-- Tree Hierarchy Node Canvas --> <div 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 + ')'" style="transform-origin: top center" > <!-- LEVEL 0: CEO Node --> <div class="flex flex-col items-center"> <ui-org-chart-node-card [node]="org()" [isSelected]="selectedEmployeeId() === org().id" [isHighlighted]="isHighlighted(org())" [isDimmed]="isDimmed(org())" [isExpanded]="isExpanded(org().id)" reportsLabel="Direct reports: 3 VPs" [headcountLabel]="org().teamHeadcount + ' total org'" expandLabel="Expand Executive Team" collapseLabel="Collapse Executive Team" (select)="selectEmployee($event)" (toggleExpand)="toggleExpand($event)" /> @if (isExpanded(org().id) && org().children?.length) { <div class="bg-border h-8 w-px"></div> <!-- LEVEL 1: VPs and Directors --> <div class="relative flex items-start gap-10 pt-0"> @if ((org().children?.length ?? 0) > 1) { <div class="bg-border absolute top-0 right-[16.666%] left-[16.666%] h-px"></div> } @for (vp of org().children; track vp.id) { <div class="flex flex-col items-center"> <div class="bg-border h-8 w-px"></div> <ui-org-chart-node-card [node]="vp" [isSelected]="selectedEmployeeId() === vp.id" [isHighlighted]="isHighlighted(vp)" [isDimmed]="isDimmed(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" (select)="selectEmployee($event)" (toggleExpand)="toggleExpand($event)" /> @if (isExpanded(vp.id) && vp.children?.length) { <div class="bg-border h-8 w-px"></div> <!-- LEVEL 2: Staff Leads & Managers --> <div class="relative flex items-start gap-6 pt-0"> @if (vp.children?.length === 3) { <div class="bg-border absolute top-0 right-[16.666%] left-[16.666%] h-px"></div> } @if (vp.children?.length === 2) { <div class="bg-border absolute top-0 right-[25%] left-[25%] h-px"></div> } @for (lead of vp.children; track lead.id) { <div class="flex flex-col items-center"> <div class="bg-border h-8 w-px"></div> <ui-org-chart-node-card [node]="lead" [isSelected]="selectedEmployeeId() === lead.id" [isHighlighted]="isHighlighted(lead)" [isDimmed]="isDimmed(lead)" [isExpanded]="isExpanded(lead.id)" [reportsLabel]="lead.reportsCount + ' reports'" [headcountLabel]="lead.teamHeadcount + ' members'" [expandLabel]="'Team (' + (lead.children?.length || 0) + ')'" collapseLabel="Collapse" (select)="selectEmployee($event)" (toggleExpand)="toggleExpand($event)" /> @if (isExpanded(lead.id) && lead.children?.length) { <div class="bg-border h-8 w-px"></div> <!-- LEVEL 3: Team ICs --> <div class="relative flex items-start gap-4 pt-0"> @if (lead.children?.length === 2) { <div class="bg-border absolute top-0 right-[25%] left-[25%] h-px"></div> } @for (ic of lead.children; track ic.id) { <div class="flex flex-col items-center"> <div class="bg-border h-8 w-px"></div> <ui-org-chart-node-card [node]="ic" [isSelected]="selectedEmployeeId() === ic.id" [isHighlighted]="isHighlighted(ic)" [isDimmed]="isDimmed(ic)" [reportsLabel]="ic.reportsCount + ' direct'" [headcountLabel]="ic.teamHeadcount + ' member'" (select)="selectEmployee($event)" /> </div> } </div> } </div> } </div> } </div> } </div> } </div> </div> </div> <!-- Quick Info Side Profile Drawer --> @if (isDrawerOpen()) { <ui-employee-profile-drawer [employee]="selectedEmployee()" [emailCopied]="emailCopied()" (copyEmail)="copyEmail($event)" (close)="isDrawerOpen.set(false)" (selectEmployee)="selectEmployee($event)" (viewManager)="viewManager($event)" /> } </div> </div> `, }) export class UiEmployeeOrgChartTreeComponent { @Input() set title(v: string | undefined) { if (v !== undefined) this.titleState.set(v) } @Input() set subtitle(v: string | undefined) { if (v !== undefined) this.subtitleState.set(v) } @Input() set orgData(v: EmployeeNode | undefined) { if (v) this.orgState.set(v) } @Input() set initialSelectedId(v: string | undefined) { if (v !== undefined) this.selectedEmployeeId.set(v) } @Input() set initialDepartment(v: string | undefined) { if (v !== undefined) this.selectedDepartment.set(v) } @Input() set initialSearch(v: string | undefined) { if (v !== undefined) this.searchQuery.set(v) } @Input() set initialExpandedIds(v: string[] | undefined) { if (v) this.expandedIds.set([...v]) } @Input() set initialDrawerOpen(v: boolean | undefined) { if (v !== undefined) this.isDrawerOpen.set(v) } @Input('class') className?: string readonly titleState = signal('Company Organizational Chart') readonly subtitleState = signal('148 Employees across 5 Departments') readonly orgState = signal<EmployeeNode>(DEFAULT_ORG_DATA) readonly selectedEmployeeId = signal('emp-ceo') readonly selectedDepartment = signal('All') readonly searchQuery = signal('') readonly isDrawerOpen = signal(true) readonly zoomLevel = signal(100) readonly emailCopied = signal(false) readonly expandedIds = signal<string[]>(['emp-ceo', 'emp-eng-vp', 'emp-prod-vp', 'emp-ops-vp']) private copyTimer: ReturnType<typeof setTimeout> | undefined org(): EmployeeNode { return this.orgState() } readonly allEmployees = computed(() => flatten(this.orgState())) readonly departmentCounts = computed<DepartmentCount[]>(() => { const all = this.allEmployees() const count = (dept: string) => all.filter((e) => e.department === dept).length return [ { label: 'All', value: 'All', count: all.length }, { label: 'Executive', value: 'Executive', count: count('Executive') }, { label: 'Engineering', value: 'Engineering', count: count('Engineering') }, { label: 'Design', value: 'Design', count: count('Design') }, { label: 'Product', value: 'Product', count: count('Product') }, { label: 'Operations', value: 'Operations', count: count('Operations') }, ] }) readonly selectedEmployee = computed<EmployeeNode>(() => { return this.allEmployees().find((e) => e.id === this.selectedEmployeeId()) ?? this.orgState() }) readonly matchingIds = computed<string[]>(() => { const q = this.searchQuery().trim().toLowerCase() if (!q) return [] return this.allEmployees() .filter( (emp) => 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)), ) .map((e) => e.id) }) get rootClass(): string { return cn('bg-background border-border flex flex-col rounded-xl border shadow-xs', this.className) } isExpanded(id: string): boolean { return this.expandedIds().includes(id) } toggleExpand(id: string): void { this.expandedIds.update((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id])) } expandAll(): void { this.expandedIds.set( this.allEmployees() .filter((e) => e.children?.length) .map((e) => e.id), ) } onSearchChange(value: string): void { this.searchQuery.set(value) if (value.trim()) this.expandAll() } selectEmployee(emp: EmployeeNode): void { this.selectedEmployeeId.set(emp.id) this.isDrawerOpen.set(true) } viewManager(managerId: string | undefined): void { if (!managerId) return const manager = this.allEmployees().find((e) => e.id === managerId) if (manager) this.selectEmployee(manager) } copyEmail(email: string): void { try { void navigator.clipboard?.writeText(email) } catch { // Clipboard fallback } this.emailCopied.set(true) clearTimeout(this.copyTimer) this.copyTimer = setTimeout(() => this.emailCopied.set(false), 2000) } adjustZoom(delta: number): void { this.zoomLevel.update((prev) => Math.min(130, Math.max(70, prev + delta))) } isDimmed(node: EmployeeNode): boolean { const dept = this.selectedDepartment() if (dept !== 'All') { if (dept === 'Executive' && node.department !== 'Executive') return true if (dept === 'Engineering' && node.department !== 'Engineering' && node.department !== 'Executive') return true if (dept === 'Design' && node.department !== 'Design' && node.department !== 'Executive') return true if (dept === 'Product' && node.department !== 'Product' && node.department !== 'Executive') return true if (dept === 'Operations' && node.department !== 'Operations' && node.department !== 'Executive') return true } if (this.searchQuery().trim().length > 0) { const matches = new Set(this.matchingIds()) const isDirectMatch = matches.has(node.id) const hasMatchingDescendant = flatten(node).some((n) => matches.has(n.id)) return !isDirectMatch && !hasMatchingDescendant } return false } isHighlighted(node: EmployeeNode): boolean { if (this.searchQuery().trim().length > 0) return this.matchingIds().includes(node.id) return false } }src/app/components/blocks/employee-org-chart-tree/org-chart-types.ts1.4 kB
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' } }src/app/components/blocks/employee-org-chart-tree/org-chart-data.ts23.2 kB
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: 'sarah.jenkins@acme.corp', 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: 'marcus.vance@acme.corp', 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: 'alex.rivera@acme.corp', 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: 'kai.zhang@acme.corp', 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: 'hannah.schmidt@acme.corp', 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: 'sofia.rossi@acme.corp', 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: 'leo.garcia@acme.corp', 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: 'nina.patel@acme.corp', 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: 'tariq.mansour@acme.corp', 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: 'kiran.rao@acme.corp', 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: 'elena.dubois@acme.corp', 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: 'elena.rostova@acme.corp', 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: 'maya.patel@acme.corp', 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: 'aria.thorne@acme.corp', 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: 'lucas.silva@acme.corp', 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: 'liam.tanaka@acme.corp', 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: 'devon.vance@acme.corp', 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: 'sarah.lin@acme.corp', 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: 'david.chen@acme.corp', 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: 'chloe.dupont@acme.corp', 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: 'arthur.leclerc@acme.corp', 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: 'zoe.martinez@acme.corp', 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: 'james.wilson@acme.corp', 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: 'rachel.green@acme.corp', 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: 'vikram.seth@acme.corp', 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', }, ], }, ], }, ], }src/app/components/blocks/employee-org-chart-tree/index.ts0.4 kB
export { UiEmployeeOrgChartTreeComponent, UiEmployeeProfileDrawerComponent, UiOrgChartNodeCardComponent, UiOrgChartToolbarComponent, type DepartmentCount, type EmployeeNode, } from './employee-org-chart-tree.component' export { getDeptBadgeClasses, getDeptTopStripClasses } from './org-chart-types' export { DEFAULT_ORG_DATA } from './org-chart-data'
Raw manifest:https://uipkge.dev/r/angular/employee-org-chart-tree.json