Files
zerp/frontend/src/components/contacts/HierarchyTree.tsx

246 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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
}