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

609 lines
22 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 {
Plus,
Edit,
Trash2,
Search,
X,
Calendar,
User,
Building2,
Loader2,
ExternalLink
} from 'lucide-react'
import { contactsAPI } from '@/lib/api/contacts'
import { format } from 'date-fns'
import Link from 'next/link'
interface Relationship {
id: string
fromContactId: string
toContactId: string
type: string
startDate: string
endDate?: string
notes?: string
isActive: boolean
fromContact: {
id: string
uniqueContactId: string
type: string
name: string
email?: string
phone?: string
status: string
}
toContact: {
id: string
uniqueContactId: string
type: string
name: string
email?: string
phone?: string
status: string
}
}
interface RelationshipManagerProps {
contactId: string
}
const RELATIONSHIP_TYPES = [
{ value: 'REPRESENTATIVE', label: 'Representative - ممثل' },
{ value: 'PARTNER', label: 'Partner - شريك' },
{ value: 'SUPPLIER', label: 'Supplier - مورد' },
{ value: 'EMPLOYEE', label: 'Employee - موظف' },
{ value: 'SUBSIDIARY', label: 'Subsidiary - فرع' },
{ value: 'BRANCH', label: 'Branch - فرع' },
{ value: 'PARENT_COMPANY', label: 'Parent Company - شركة أم' },
{ value: 'CUSTOMER', label: 'Customer - عميل' },
{ value: 'VENDOR', label: 'Vendor - بائع' },
{ value: 'OTHER', label: 'Other - أخرى' },
]
export default function RelationshipManager({ contactId }: RelationshipManagerProps) {
const [relationships, setRelationships] = useState<Relationship[]>([])
const [loading, setLoading] = useState(true)
const [showAddModal, setShowAddModal] = useState(false)
const [showEditModal, setShowEditModal] = useState(false)
const [selectedRelationship, setSelectedRelationship] = useState<Relationship | null>(null)
const [submitting, setSubmitting] = useState(false)
// Form state
const [searchTerm, setSearchTerm] = useState('')
const [searchResults, setSearchResults] = useState<any[]>([])
const [searching, setSearching] = useState(false)
const [formData, setFormData] = useState({
toContactId: '',
type: 'REPRESENTATIVE',
startDate: new Date().toISOString().split('T')[0],
endDate: '',
notes: ''
})
useEffect(() => {
fetchRelationships()
}, [contactId])
const fetchRelationships = async () => {
setLoading(true)
try {
const data = await contactsAPI.getRelationships(contactId)
setRelationships(data)
} catch (error: any) {
toast.error('Failed to load relationships')
} finally {
setLoading(false)
}
}
// Search contacts with debouncing
useEffect(() => {
if (!searchTerm || searchTerm.length < 2) {
setSearchResults([])
return
}
const debounce = setTimeout(async () => {
setSearching(true)
try {
const data = await contactsAPI.getAll({ search: searchTerm, pageSize: 10 })
setSearchResults(data.contacts.filter(c => c.id !== contactId))
} catch (error) {
console.error('Search error:', error)
} finally {
setSearching(false)
}
}, 500)
return () => clearTimeout(debounce)
}, [searchTerm, contactId])
const resetForm = () => {
setFormData({
toContactId: '',
type: 'REPRESENTATIVE',
startDate: new Date().toISOString().split('T')[0],
endDate: '',
notes: ''
})
setSearchTerm('')
setSearchResults([])
setSelectedRelationship(null)
}
const handleAdd = async () => {
if (!formData.toContactId) {
toast.error('Please select a contact')
return
}
setSubmitting(true)
try {
await contactsAPI.addRelationship(contactId, {
toContactId: formData.toContactId,
type: formData.type,
startDate: formData.startDate,
endDate: formData.endDate || undefined,
notes: formData.notes || undefined
})
toast.success('Relationship added successfully')
setShowAddModal(false)
resetForm()
fetchRelationships()
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to add relationship')
} finally {
setSubmitting(false)
}
}
const handleEdit = async () => {
if (!selectedRelationship) return
setSubmitting(true)
try {
await contactsAPI.updateRelationship(
contactId,
selectedRelationship.id,
{
type: formData.type,
startDate: formData.startDate,
endDate: formData.endDate || undefined,
notes: formData.notes || undefined
}
)
toast.success('Relationship updated successfully')
setShowEditModal(false)
resetForm()
fetchRelationships()
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to update relationship')
} finally {
setSubmitting(false)
}
}
const handleDelete = async (relationship: Relationship) => {
if (!confirm('Are you sure you want to delete this relationship?')) return
try {
await contactsAPI.deleteRelationship(contactId, relationship.id)
toast.success('Relationship deleted successfully')
fetchRelationships()
} catch (error: any) {
toast.error('Failed to delete relationship')
}
}
const openEditModal = (relationship: Relationship) => {
setSelectedRelationship(relationship)
setFormData({
toContactId: relationship.toContactId,
type: relationship.type,
startDate: relationship.startDate.split('T')[0],
endDate: relationship.endDate ? relationship.endDate.split('T')[0] : '',
notes: relationship.notes || ''
})
setShowEditModal(true)
}
const getRelatedContact = (relationship: Relationship) => {
return relationship.fromContactId === contactId
? relationship.toContact
: relationship.fromContact
}
const getRelationshipDirection = (relationship: Relationship) => {
return relationship.fromContactId === contactId ? '→' : '←'
}
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="animate-spin text-blue-600" size={32} />
</div>
)
}
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">
العلاقات - Relationships ({relationships.length})
</h3>
<button
onClick={() => {
resetForm()
setShowAddModal(true)
}}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
<Plus size={18} />
Add Relationship
</button>
</div>
{/* Relationships List */}
{relationships.length === 0 ? (
<div className="text-center py-12 bg-gray-50 rounded-lg">
<User className="mx-auto h-12 w-12 text-gray-400 mb-4" />
<p className="text-gray-600">No relationships found</p>
<button
onClick={() => setShowAddModal(true)}
className="mt-4 text-blue-600 hover:text-blue-700 font-medium"
>
Add your first relationship
</button>
</div>
) : (
<div className="bg-white border rounded-lg overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Contact
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Type
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Start Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
End Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Status
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{relationships.map(relationship => {
const relatedContact = getRelatedContact(relationship)
return (
<tr key={relationship.id} className="hover:bg-gray-50">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<span className="text-gray-500">
{getRelationshipDirection(relationship)}
</span>
{relatedContact.type === 'INDIVIDUAL' ? (
<User size={18} className="text-gray-400" />
) : (
<Building2 size={18} className="text-gray-400" />
)}
<div>
<Link
href={`/contacts/${relatedContact.id}`}
className="font-medium text-gray-900 hover:text-blue-600 flex items-center gap-1"
>
{relatedContact.name}
<ExternalLink size={14} />
</Link>
<p className="text-sm text-gray-600">{relatedContact.email || relatedContact.phone}</p>
</div>
</div>
</td>
<td className="px-6 py-4">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{relationship.type.replace(/_/g, ' ')}
</span>
</td>
<td className="px-6 py-4 text-sm text-gray-600">
{format(new Date(relationship.startDate), 'MMM d, yyyy')}
</td>
<td className="px-6 py-4 text-sm text-gray-600">
{relationship.endDate ? format(new Date(relationship.endDate), 'MMM d, yyyy') : '-'}
</td>
<td className="px-6 py-4">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
relationship.isActive
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}>
{relationship.isActive ? 'Active' : 'Inactive'}
</span>
</td>
<td className="px-6 py-4 text-right">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => openEditModal(relationship)}
className="text-blue-600 hover:text-blue-900"
>
<Edit size={18} />
</button>
<button
onClick={() => handleDelete(relationship)}
className="text-red-600 hover:text-red-900"
>
<Trash2 size={18} />
</button>
</div>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
{/* Add Modal */}
{showAddModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
<div className="flex items-center justify-between p-6 border-b">
<h2 className="text-xl font-bold text-gray-900">
إضافة علاقة - Add Relationship
</h2>
<button onClick={() => { setShowAddModal(false); resetForm(); }} className="text-gray-400 hover:text-gray-600">
<X size={24} />
</button>
</div>
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{/* Contact Search */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Select Contact <span className="text-red-500">*</span>
</label>
<div className="relative">
<Search className="absolute left-3 top-3 text-gray-400" size={18} />
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search contacts..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900"
/>
</div>
{searching && (
<p className="text-sm text-gray-600 mt-2">Searching...</p>
)}
{searchResults.length > 0 && (
<div className="mt-2 border rounded-lg max-h-48 overflow-y-auto">
{searchResults.map(contact => (
<button
key={contact.id}
onClick={() => {
setFormData({ ...formData, toContactId: contact.id })
setSearchTerm(contact.name)
setSearchResults([])
}}
className="w-full text-left p-3 hover:bg-gray-50 border-b last:border-b-0"
>
<div className="flex items-center gap-2">
{contact.type === 'INDIVIDUAL' ? <User size={16} /> : <Building2 size={16} />}
<div>
<p className="font-medium text-gray-900">{contact.name}</p>
<p className="text-sm text-gray-600">{contact.email || contact.phone}</p>
</div>
</div>
</button>
))}
</div>
)}
</div>
{/* Relationship Type */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Relationship Type <span className="text-red-500">*</span>
</label>
<select
value={formData.type}
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white text-gray-900"
>
{RELATIONSHIP_TYPES.map(type => (
<option key={type.value} value={type.value}>{type.label}</option>
))}
</select>
</div>
{/* Start Date */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Start Date <span className="text-red-500">*</span>
</label>
<input
type="date"
value={formData.startDate}
onChange={(e) => setFormData({ ...formData, startDate: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900"
/>
</div>
{/* End Date */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
End Date (Optional)
</label>
<input
type="date"
value={formData.endDate}
onChange={(e) => setFormData({ ...formData, endDate: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900"
/>
</div>
{/* Notes */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Notes (Optional)
</label>
<textarea
value={formData.notes}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900"
placeholder="Add any notes about this relationship..."
/>
</div>
</div>
<div className="border-t p-6 flex justify-end gap-3">
<button
onClick={() => { setShowAddModal(false); resetForm(); }}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
disabled={submitting}
>
Cancel
</button>
<button
onClick={handleAdd}
disabled={submitting || !formData.toContactId}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{submitting ? (
<>
<Loader2 className="animate-spin" size={18} />
Adding...
</>
) : (
'Add Relationship'
)}
</button>
</div>
</div>
</div>
)}
{/* Edit Modal */}
{showEditModal && selectedRelationship && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl">
<div className="flex items-center justify-between p-6 border-b">
<h2 className="text-xl font-bold text-gray-900">
تعديل العلاقة - Edit Relationship
</h2>
<button onClick={() => { setShowEditModal(false); resetForm(); }} className="text-gray-400 hover:text-gray-600">
<X size={24} />
</button>
</div>
<div className="p-6 space-y-4">
{/* Contact (Read-only) */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Contact
</label>
<div className="px-3 py-2 bg-gray-50 border border-gray-300 rounded-lg text-gray-600">
{getRelatedContact(selectedRelationship).name}
</div>
</div>
{/* Relationship Type */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Relationship Type
</label>
<select
value={formData.type}
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white text-gray-900"
>
{RELATIONSHIP_TYPES.map(type => (
<option key={type.value} value={type.value}>{type.label}</option>
))}
</select>
</div>
{/* Start Date */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Start Date
</label>
<input
type="date"
value={formData.startDate}
onChange={(e) => setFormData({ ...formData, startDate: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900"
/>
</div>
{/* End Date */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
End Date
</label>
<input
type="date"
value={formData.endDate}
onChange={(e) => setFormData({ ...formData, endDate: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900"
/>
</div>
{/* Notes */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Notes
</label>
<textarea
value={formData.notes}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900"
/>
</div>
</div>
<div className="border-t p-6 flex justify-end gap-3">
<button
onClick={() => { setShowEditModal(false); resetForm(); }}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50"
disabled={submitting}
>
Cancel
</button>
<button
onClick={handleEdit}
disabled={submitting}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{submitting ? (
<>
<Loader2 className="animate-spin" size={18} />
Updating...
</>
) : (
'Update Relationship'
)}
</button>
</div>
</div>
</div>
)}
</div>
)
}