edit contact form
This commit is contained in:
@@ -41,20 +41,17 @@ function flattenCategories(cats: Category[], result: Category[] = []): Category[
|
||||
}
|
||||
|
||||
function ContactsContent() {
|
||||
// State Management
|
||||
const [contacts, setContacts] = useState<Contact[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [selectedContacts, setSelectedContacts] = useState<Set<string>>(new Set())
|
||||
const [showBulkActions, setShowBulkActions] = useState(false)
|
||||
|
||||
// Pagination
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
const [total, setTotal] = useState(0)
|
||||
const pageSize = 10
|
||||
|
||||
// Filters
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [selectedType, setSelectedType] = useState('all')
|
||||
const [selectedStatus, setSelectedStatus] = useState('all')
|
||||
@@ -64,7 +61,6 @@ function ContactsContent() {
|
||||
const [categories, setCategories] = useState<Category[]>([])
|
||||
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false)
|
||||
|
||||
// Modals
|
||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||
const [showEditModal, setShowEditModal] = useState(false)
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
@@ -75,7 +71,6 @@ function ContactsContent() {
|
||||
const [exporting, setExporting] = useState(false)
|
||||
const [exportExcludeCompanyEmployees, setExportExcludeCompanyEmployees] = useState(false)
|
||||
|
||||
// Fetch Contacts (with debouncing for search)
|
||||
const fetchContacts = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
@@ -84,7 +79,7 @@ function ContactsContent() {
|
||||
page: currentPage,
|
||||
pageSize,
|
||||
}
|
||||
|
||||
|
||||
if (searchTerm) filters.search = searchTerm
|
||||
if (selectedType !== 'all') filters.type = selectedType
|
||||
if (selectedStatus !== 'all') filters.status = selectedStatus
|
||||
@@ -104,21 +99,18 @@ function ContactsContent() {
|
||||
}
|
||||
}, [currentPage, searchTerm, selectedType, selectedStatus, selectedSource, selectedRating, selectedCategory])
|
||||
|
||||
// Debounced search
|
||||
useEffect(() => {
|
||||
const debounce = setTimeout(() => {
|
||||
setCurrentPage(1) // Reset to page 1 on new search
|
||||
setCurrentPage(1)
|
||||
fetchContacts()
|
||||
}, 500)
|
||||
return () => clearTimeout(debounce)
|
||||
}, [searchTerm])
|
||||
|
||||
// Fetch on filter/page change
|
||||
useEffect(() => {
|
||||
fetchContacts()
|
||||
}, [currentPage, selectedType, selectedStatus, selectedSource, selectedRating, selectedCategory])
|
||||
|
||||
// Create Contact
|
||||
const handleCreate = async (data: CreateContactData) => {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
@@ -136,7 +128,6 @@ function ContactsContent() {
|
||||
}
|
||||
}
|
||||
|
||||
// Edit Contact
|
||||
const handleEdit = async (data: UpdateContactData) => {
|
||||
if (!selectedContact) return
|
||||
|
||||
@@ -156,7 +147,6 @@ function ContactsContent() {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Contact
|
||||
const handleDelete = async () => {
|
||||
if (!selectedContact) return
|
||||
|
||||
@@ -175,7 +165,6 @@ function ContactsContent() {
|
||||
}
|
||||
}
|
||||
|
||||
// Utility Functions
|
||||
const resetForm = () => {
|
||||
setSelectedContact(null)
|
||||
}
|
||||
@@ -197,6 +186,7 @@ function ContactsContent() {
|
||||
HOLDING: 'bg-purple-100 text-purple-700',
|
||||
GOVERNMENT: 'bg-orange-100 text-orange-700',
|
||||
ORGANIZATION: 'bg-cyan-100 text-cyan-700',
|
||||
EMBASSIES: 'bg-red-100 text-red-700',
|
||||
BANK: 'bg-emerald-100 text-emerald-700',
|
||||
UNIVERSITY: 'bg-indigo-100 text-indigo-700',
|
||||
SCHOOL: 'bg-yellow-100 text-yellow-700',
|
||||
@@ -218,6 +208,7 @@ function ContactsContent() {
|
||||
HOLDING: 'مجموعة',
|
||||
GOVERNMENT: 'حكومي',
|
||||
ORGANIZATION: 'منظمات',
|
||||
EMBASSIES: 'سفارات',
|
||||
BANK: 'بنوك',
|
||||
UNIVERSITY: 'جامعات',
|
||||
SCHOOL: 'مدارس',
|
||||
@@ -228,14 +219,40 @@ function ContactsContent() {
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
const organizationTypes = new Set([
|
||||
'COMPANY',
|
||||
'HOLDING',
|
||||
'GOVERNMENT',
|
||||
'ORGANIZATION',
|
||||
'EMBASSIES',
|
||||
'BANK',
|
||||
'UNIVERSITY',
|
||||
'SCHOOL',
|
||||
'UN',
|
||||
'NGO',
|
||||
'INSTITUTION',
|
||||
])
|
||||
|
||||
const isOrganizationContact = (contact: Contact) => organizationTypes.has(contact.type)
|
||||
|
||||
const getListContactName = (contact: Contact) => {
|
||||
return contact.name || '-'
|
||||
}
|
||||
|
||||
const getListCompanyName = (contact: Contact) => {
|
||||
return contact.companyName || '-'
|
||||
}
|
||||
|
||||
const getListContactNameAr = (contact: Contact) => {
|
||||
return (contact as any).nameAr || ''
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<header className="bg-white shadow-sm border-b">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
@@ -303,7 +320,6 @@ function ContactsContent() {
|
||||
</header>
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -358,12 +374,9 @@ function ContactsContent() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters and Search */}
|
||||
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-200 mb-6">
|
||||
<div className="space-y-4">
|
||||
{/* Main Filters Row */}
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
{/* Search */}
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute right-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
||||
<input
|
||||
@@ -375,7 +388,6 @@ function ContactsContent() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Type Filter */}
|
||||
<select
|
||||
value={selectedType}
|
||||
onChange={(e) => setSelectedType(e.target.value)}
|
||||
@@ -396,7 +408,6 @@ function ContactsContent() {
|
||||
<option value="INSTITUTION">Institution</option>
|
||||
</select>
|
||||
|
||||
{/* Status Filter */}
|
||||
<select
|
||||
value={selectedStatus}
|
||||
onChange={(e) => setSelectedStatus(e.target.value)}
|
||||
@@ -407,7 +418,6 @@ function ContactsContent() {
|
||||
<option value="INACTIVE">Inactive</option>
|
||||
</select>
|
||||
|
||||
{/* Advanced Filters Toggle */}
|
||||
<button
|
||||
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
|
||||
className={`flex items-center gap-2 px-4 py-3 border rounded-lg transition-colors ${
|
||||
@@ -421,11 +431,9 @@ function ContactsContent() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Advanced Filters */}
|
||||
{showAdvancedFilters && (
|
||||
<div className="pt-4 border-t border-gray-200">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{/* Source Filter */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Source</label>
|
||||
<select
|
||||
@@ -445,7 +453,6 @@ function ContactsContent() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Rating Filter */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Rating</label>
|
||||
<select
|
||||
@@ -462,7 +469,6 @@ function ContactsContent() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Category Filter */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Category</label>
|
||||
<select
|
||||
@@ -477,7 +483,6 @@ function ContactsContent() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Clear Filters */}
|
||||
<div className="flex items-end">
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -500,7 +505,6 @@ function ContactsContent() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contacts Table */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="p-12">
|
||||
@@ -547,9 +551,9 @@ function ContactsContent() {
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
</th>
|
||||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Contact</th>
|
||||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Contact Info</th>
|
||||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Company</th>
|
||||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Contact Info</th>
|
||||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Contact</th>
|
||||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Type</th>
|
||||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Status</th>
|
||||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Actions</th>
|
||||
@@ -559,105 +563,119 @@ function ContactsContent() {
|
||||
{contacts.map((contact) => {
|
||||
const isSelected = selectedContacts.has(contact.id)
|
||||
return (
|
||||
<tr key={contact.id} className={`hover:bg-gray-50 transition-colors ${isSelected ? 'bg-blue-50' : ''}`}>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={(e) => {
|
||||
const newSelected = new Set(selectedContacts)
|
||||
if (e.target.checked) {
|
||||
newSelected.add(contact.id)
|
||||
} else {
|
||||
newSelected.delete(contact.id)
|
||||
}
|
||||
setSelectedContacts(newSelected)
|
||||
}}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
</td>
|
||||
<tr key={contact.id} className={`hover:bg-gray-50 transition-colors ${isSelected ? 'bg-blue-50' : ''}`}>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={(e) => {
|
||||
const newSelected = new Set(selectedContacts)
|
||||
if (e.target.checked) {
|
||||
newSelected.add(contact.id)
|
||||
} else {
|
||||
newSelected.delete(contact.id)
|
||||
}
|
||||
setSelectedContacts(newSelected)
|
||||
}}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-4">
|
||||
{getListCompanyName(contact) !== '-' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Building2 className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-sm text-gray-900">
|
||||
{getListCompanyName(contact)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-4">
|
||||
<div className="space-y-1">
|
||||
{contact.email && (
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<Mail className="h-4 w-4" />
|
||||
{contact.email}
|
||||
</div>
|
||||
)}
|
||||
{(contact.phone || contact.mobile) && (
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<Phone className="h-4 w-4" />
|
||||
{contact.phone || contact.mobile}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white font-bold">
|
||||
{contact.name.charAt(0)}
|
||||
{getListContactName(contact).charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">{contact.name}</p>
|
||||
{contact.nameAr && <p className="text-sm text-gray-600">{contact.nameAr}</p>}
|
||||
<p className="font-semibold text-gray-900">
|
||||
{getListContactName(contact)}
|
||||
</p>
|
||||
{getListContactNameAr(contact) && (
|
||||
<p className="text-sm text-gray-600">
|
||||
{getListContactNameAr(contact)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="space-y-1">
|
||||
{contact.email && (
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<Mail className="h-4 w-4" />
|
||||
{contact.email}
|
||||
</div>
|
||||
)}
|
||||
{contact.phone && (
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<Phone className="h-4 w-4" />
|
||||
{contact.phone}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{contact.companyName && (
|
||||
|
||||
<td className="px-6 py-4">
|
||||
<span className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium ${getTypeColor(contact.type)}`}>
|
||||
<Tag className="h-3 w-3" />
|
||||
{getTypeLabel(contact.type)}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-4">
|
||||
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${
|
||||
contact.status === 'ACTIVE'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
{contact.status === 'ACTIVE' ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Building2 className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-sm text-gray-900">{contact.companyName}</span>
|
||||
<Link
|
||||
href={`/contacts/${contact.id}`}
|
||||
className="p-2 hover:bg-blue-50 text-blue-600 rounded-lg transition-colors"
|
||||
title="View"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => openEditModal(contact)}
|
||||
className="p-2 hover:bg-green-50 text-green-600 rounded-lg transition-colors"
|
||||
title="Edit"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openDeleteDialog(contact)}
|
||||
className="p-2 hover:bg-red-50 text-red-600 rounded-lg transition-colors"
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium ${getTypeColor(contact.type)}`}>
|
||||
<Tag className="h-3 w-3" />
|
||||
{getTypeLabel(contact.type)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${
|
||||
contact.status === 'ACTIVE'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
{contact.status === 'ACTIVE' ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href={`/contacts/${contact.id}`}
|
||||
className="p-2 hover:bg-blue-50 text-blue-600 rounded-lg transition-colors"
|
||||
title="View"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => openEditModal(contact)}
|
||||
className="p-2 hover:bg-green-50 text-green-600 rounded-lg transition-colors"
|
||||
title="Edit"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openDeleteDialog(contact)}
|
||||
className="p-2 hover:bg-red-50 text-red-600 rounded-lg transition-colors"
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)})}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
|
||||
<p className="text-sm text-gray-600">
|
||||
Showing <span className="font-semibold">{((currentPage - 1) * pageSize) + 1}</span> to{' '}
|
||||
@@ -703,7 +721,6 @@ function ContactsContent() {
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Create Modal */}
|
||||
<Modal
|
||||
isOpen={showCreateModal}
|
||||
onClose={() => {
|
||||
@@ -714,6 +731,7 @@ function ContactsContent() {
|
||||
size="xl"
|
||||
>
|
||||
<ContactForm
|
||||
key="create-contact"
|
||||
onSubmit={async (data) => {
|
||||
await handleCreate(data as CreateContactData)
|
||||
}}
|
||||
@@ -725,7 +743,6 @@ function ContactsContent() {
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
{/* Edit Modal */}
|
||||
<Modal
|
||||
isOpen={showEditModal}
|
||||
onClose={() => {
|
||||
@@ -736,6 +753,7 @@ function ContactsContent() {
|
||||
size="xl"
|
||||
>
|
||||
<ContactForm
|
||||
key={selectedContact?.id || 'edit-contact'}
|
||||
contact={selectedContact || undefined}
|
||||
onSubmit={async (data) => {
|
||||
await handleEdit(data as UpdateContactData)
|
||||
@@ -748,7 +766,6 @@ function ContactsContent() {
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
{/* Export Modal */}
|
||||
{showExportModal && (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={() => setShowExportModal(false)} />
|
||||
@@ -763,7 +780,7 @@ function ContactsContent() {
|
||||
<p className="text-sm text-gray-600">Download contacts data</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<div>
|
||||
<p className="text-sm text-gray-700 mb-2">
|
||||
@@ -812,7 +829,7 @@ function ContactsContent() {
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
document.body.removeChild(a)
|
||||
|
||||
|
||||
toast.success('Contacts exported successfully!')
|
||||
setShowExportModal(false)
|
||||
} catch (err: any) {
|
||||
@@ -842,7 +859,6 @@ function ContactsContent() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
{showDeleteDialog && selectedContact && (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={() => setShowDeleteDialog(false)} />
|
||||
@@ -891,7 +907,6 @@ function ContactsContent() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Import Modal */}
|
||||
{showImportModal && (
|
||||
<ContactImport
|
||||
onClose={() => setShowImportModal(false)}
|
||||
@@ -911,4 +926,4 @@ export default function ContactsPage() {
|
||||
<ContactsContent />
|
||||
</ProtectedRoute>
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user