246 lines
7.3 KiB
TypeScript
246 lines
7.3 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useEffect } from 'react'
|
||
import { toast } from 'react-hot-toast'
|
||
import { ChevronDown, ChevronRight, Building2, User, Plus, ExternalLink, Loader2 } from 'lucide-react'
|
||
import { contactsAPI, Contact } from '@/lib/api/contacts'
|
||
import Link from 'next/link'
|
||
|
||
interface HierarchyNode extends Contact {
|
||
children?: HierarchyNode[]
|
||
expanded?: boolean
|
||
}
|
||
|
||
interface HierarchyTreeProps {
|
||
rootContactId: string
|
||
}
|
||
|
||
export default function HierarchyTree({ rootContactId }: HierarchyTreeProps) {
|
||
const [root, setRoot] = useState<HierarchyNode | null>(null)
|
||
const [loading, setLoading] = useState(true)
|
||
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set([rootContactId]))
|
||
|
||
useEffect(() => {
|
||
fetchHierarchy()
|
||
}, [rootContactId])
|
||
|
||
const fetchHierarchy = async () => {
|
||
setLoading(true)
|
||
try {
|
||
// Fetch root contact
|
||
const rootContact = await contactsAPI.getById(rootContactId)
|
||
|
||
// Fetch all contacts to build hierarchy
|
||
const allContacts = await contactsAPI.getAll({ pageSize: 1000 })
|
||
|
||
// Build tree structure
|
||
const tree = buildTree(rootContact, allContacts.contacts)
|
||
setRoot(tree)
|
||
} catch (error: any) {
|
||
toast.error('Failed to load hierarchy')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const buildTree = (rootContact: Contact, allContacts: Contact[]): HierarchyNode => {
|
||
const findChildren = (parentId: string): HierarchyNode[] => {
|
||
return allContacts
|
||
.filter(c => c.parentId === parentId)
|
||
.map(child => ({
|
||
...child,
|
||
children: findChildren(child.id),
|
||
expanded: expandedNodes.has(child.id)
|
||
}))
|
||
}
|
||
|
||
return {
|
||
...rootContact,
|
||
children: findChildren(rootContact.id),
|
||
expanded: expandedNodes.has(rootContact.id)
|
||
}
|
||
}
|
||
|
||
const toggleNode = (nodeId: string) => {
|
||
setExpandedNodes(prev => {
|
||
const newSet = new Set(prev)
|
||
if (newSet.has(nodeId)) {
|
||
newSet.delete(nodeId)
|
||
} else {
|
||
newSet.add(nodeId)
|
||
}
|
||
return newSet
|
||
})
|
||
}
|
||
|
||
const renderNode = (node: HierarchyNode, level: number = 0) => {
|
||
const hasChildren = node.children && node.children.length > 0
|
||
const isExpanded = expandedNodes.has(node.id)
|
||
const Icon = node.type === 'INDIVIDUAL' ? User : Building2
|
||
|
||
return (
|
||
<div key={node.id} className="select-none">
|
||
<div
|
||
className={`flex items-center gap-2 py-2 px-3 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors ${
|
||
level > 0 ? 'ml-8' : ''
|
||
}`}
|
||
style={{ paddingLeft: `${level * 32 + 12}px` }}
|
||
>
|
||
{/* Expand/Collapse Button */}
|
||
{hasChildren ? (
|
||
<button
|
||
onClick={() => toggleNode(node.id)}
|
||
className="flex-shrink-0 w-6 h-6 flex items-center justify-center text-gray-500 hover:text-gray-700"
|
||
>
|
||
{isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||
</button>
|
||
) : (
|
||
<div className="w-6" />
|
||
)}
|
||
|
||
{/* Contact Icon */}
|
||
<Icon className="flex-shrink-0 text-blue-600" size={20} />
|
||
|
||
{/* Contact Info */}
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2">
|
||
<Link
|
||
href={`/contacts/${node.id}`}
|
||
className="font-medium text-gray-900 hover:text-blue-600 truncate"
|
||
>
|
||
{node.name}
|
||
</Link>
|
||
<span className="flex-shrink-0 text-xs bg-gray-100 text-gray-600 px-2 py-0.5 rounded">
|
||
{node.type}
|
||
</span>
|
||
{node.id === rootContactId && (
|
||
<span className="flex-shrink-0 text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded font-medium">
|
||
Root
|
||
</span>
|
||
)}
|
||
</div>
|
||
{(node.email || node.phone) && (
|
||
<p className="text-sm text-gray-600 truncate">
|
||
{node.email || node.phone}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Actions */}
|
||
<div className="flex items-center gap-2 flex-shrink-0">
|
||
<Link
|
||
href={`/contacts/${node.id}`}
|
||
target="_blank"
|
||
className="p-1 text-gray-400 hover:text-blue-600"
|
||
title="Open in new tab"
|
||
>
|
||
<ExternalLink size={16} />
|
||
</Link>
|
||
<Link
|
||
href={`/contacts?create=true&parentId=${node.id}`}
|
||
className="p-1 text-gray-400 hover:text-green-600"
|
||
title="Add child"
|
||
>
|
||
<Plus size={16} />
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Render Children */}
|
||
{hasChildren && isExpanded && (
|
||
<div className="border-l-2 border-gray-200 ml-4">
|
||
{node.children!.map(child => renderNode(child, level + 1))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="flex items-center justify-center py-12">
|
||
<Loader2 className="animate-spin text-blue-600" size={32} />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (!root) {
|
||
return (
|
||
<div className="text-center py-12 text-gray-500">
|
||
<Building2 className="h-12 w-12 mx-auto mb-2 text-gray-300" />
|
||
<p>Failed to load hierarchy</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const totalNodes = root.children ? countNodes(root) : 1
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h3 className="text-lg font-semibold text-gray-900">
|
||
الهيكل التنظيمي - Company Hierarchy
|
||
</h3>
|
||
<p className="text-sm text-gray-600">
|
||
{totalNodes} contact{totalNodes !== 1 ? 's' : ''} in hierarchy
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => setExpandedNodes(new Set(getAllNodeIds(root)))}
|
||
className="text-sm text-blue-600 hover:text-blue-700 font-medium"
|
||
>
|
||
Expand All
|
||
</button>
|
||
<span className="text-gray-300">|</span>
|
||
<button
|
||
onClick={() => setExpandedNodes(new Set([rootContactId]))}
|
||
className="text-sm text-gray-600 hover:text-gray-700 font-medium"
|
||
>
|
||
Collapse All
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white border rounded-lg p-4">
|
||
{renderNode(root, 0)}
|
||
</div>
|
||
|
||
{root.children && root.children.length === 0 && (
|
||
<div className="text-center py-8 text-gray-500">
|
||
<p>No child contacts in this hierarchy</p>
|
||
<Link
|
||
href={`/contacts?create=true&parentId=${rootContactId}`}
|
||
className="mt-4 inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 font-medium"
|
||
>
|
||
<Plus size={16} />
|
||
Add First Child Contact
|
||
</Link>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Helper functions
|
||
function countNodes(node: HierarchyNode): number {
|
||
let count = 1
|
||
if (node.children) {
|
||
node.children.forEach(child => {
|
||
count += countNodes(child)
|
||
})
|
||
}
|
||
return count
|
||
}
|
||
|
||
function getAllNodeIds(node: HierarchyNode): string[] {
|
||
let ids = [node.id]
|
||
if (node.children) {
|
||
node.children.forEach(child => {
|
||
ids = ids.concat(getAllNodeIds(child))
|
||
})
|
||
}
|
||
return ids
|
||
}
|