Deploy rule, CRM enhancements, Company Employee category, bilingual, contacts module completion

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Talal Sharabi
2026-02-19 14:59:34 +04:00
parent 0b126cb676
commit 680ba3871e
51 changed files with 11456 additions and 477 deletions

View File

@@ -0,0 +1,245 @@
'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
}