Files
zerp/frontend/src/app/contacts/page.tsx
2026-05-03 15:25:50 +03:00

980 lines
40 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, useCallback } from 'react'
import ProtectedRoute from '@/components/ProtectedRoute'
import Modal from '@/components/Modal'
import LoadingSpinner from '@/components/LoadingSpinner'
import Link from 'next/link'
import { toast } from 'react-hot-toast'
import {
Users,
Plus,
Search,
Filter,
Mail,
Phone,
Building2,
Star,
Edit,
Trash2,
Eye,
Download,
Upload,
ArrowLeft,
UserPlus,
Briefcase,
Tag,
X,
Loader2
} from 'lucide-react'
import { contactsAPI, Contact, CreateContactData, UpdateContactData, ContactFilters } from '@/lib/api/contacts'
import { categoriesAPI, Category } from '@/lib/api/categories'
import ContactForm from '@/components/contacts/ContactForm'
import ContactImport from '@/components/contacts/ContactImport'
function flattenCategories(cats: Category[], result: Category[] = []): Category[] {
for (const c of cats) {
result.push(c)
if (c.children?.length) flattenCategories(c.children, result)
}
return result
}
function ContactsContent() {
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)
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
const [total, setTotal] = useState(0)
const pageSize = 10
const [searchTerm, setSearchTerm] = useState('')
const [selectedType, setSelectedType] = useState('all')
const [selectedSpecialization, setSelectedSpecialization] = useState('all')
const [createDefaultType, setCreateDefaultType] = useState('INDIVIDUAL')
const [selectedStatus, setSelectedStatus] = useState('all')
const [selectedSource, setSelectedSource] = useState('all')
const [selectedRating, setSelectedRating] = useState('all')
const [selectedCategory, setSelectedCategory] = useState('all')
const [categories, setCategories] = useState<Category[]>([])
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false)
const [showCreateModal, setShowCreateModal] = useState(false)
const [showEditModal, setShowEditModal] = useState(false)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [showExportModal, setShowExportModal] = useState(false)
const [showImportModal, setShowImportModal] = useState(false)
const [selectedContact, setSelectedContact] = useState<Contact | null>(null)
const [submitting, setSubmitting] = useState(false)
const [exporting, setExporting] = useState(false)
const [exportExcludeCompanyEmployees, setExportExcludeCompanyEmployees] = useState(false)
const fetchContacts = useCallback(async () => {
setLoading(true)
setError(null)
try {
const filters: ContactFilters = {
page: currentPage,
pageSize,
}
if (searchTerm) filters.search = searchTerm
if (selectedType !== 'all') filters.type = selectedType
if (selectedSpecialization !== 'all') filters.specialization = selectedSpecialization
if (selectedStatus !== 'all') filters.status = selectedStatus
if (selectedSource !== 'all') filters.source = selectedSource
if (selectedRating !== 'all') filters.rating = parseInt(selectedRating)
if (selectedCategory !== 'all') filters.category = selectedCategory
const data = await contactsAPI.getAll(filters)
setContacts(data.contacts)
setTotal(data.total)
setTotalPages(data.totalPages)
} catch (err: any) {
setError(err.response?.data?.message || 'Failed to load contacts')
toast.error('Failed to load contacts')
} finally {
setLoading(false)
}
}, [currentPage, searchTerm, selectedType, selectedStatus, selectedSource, selectedRating, selectedCategory, selectedSpecialization])
useEffect(() => {
const debounce = setTimeout(() => {
setCurrentPage(1)
fetchContacts()
}, 500)
return () => clearTimeout(debounce)
}, [searchTerm])
useEffect(() => {
fetchContacts()
}, [currentPage, selectedType, selectedStatus, selectedSource, selectedRating, selectedCategory, selectedSpecialization])
const handleCreate = async (data: CreateContactData) => {
setSubmitting(true)
try {
await contactsAPI.create(data)
toast.success('Contact created successfully!')
setShowCreateModal(false)
resetForm()
fetchContacts()
} catch (err: any) {
const message = err.response?.data?.message || 'Failed to create contact'
toast.error(message)
throw err
} finally {
setSubmitting(false)
}
}
const handleEdit = async (data: UpdateContactData) => {
if (!selectedContact) return
setSubmitting(true)
try {
await contactsAPI.update(selectedContact.id, data)
toast.success('Contact updated successfully!')
setShowEditModal(false)
resetForm()
fetchContacts()
} catch (err: any) {
const message = err.response?.data?.message || 'Failed to update contact'
toast.error(message)
throw err
} finally {
setSubmitting(false)
}
}
const handleDelete = async () => {
if (!selectedContact) return
setSubmitting(true)
try {
await contactsAPI.archive(selectedContact.id, 'Deleted by user')
toast.success('Contact deleted successfully!')
setShowDeleteDialog(false)
setSelectedContact(null)
fetchContacts()
} catch (err: any) {
const message = err.response?.data?.message || 'Failed to delete contact'
toast.error(message)
} finally {
setSubmitting(false)
}
}
const resetForm = () => {
setSelectedContact(null)
}
const openEditModal = (contact: Contact) => {
setSelectedContact(contact)
setShowEditModal(true)
}
const openDeleteDialog = (contact: Contact) => {
setSelectedContact(contact)
setShowDeleteDialog(true)
}
const getTypeColor = (type: string) => {
const colors: Record<string, string> = {
INDIVIDUAL: 'bg-blue-100 text-blue-700',
COMPANY: 'bg-green-100 text-green-700',
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',
UN: 'bg-sky-100 text-sky-700',
NGO: 'bg-pink-100 text-pink-700',
INSTITUTION: 'bg-gray-100 text-gray-700'
}
return colors[type] || 'bg-gray-100 text-gray-700'
}
useEffect(() => {
categoriesAPI.getTree().then(setCategories).catch(() => {})
}, [])
const getTypeLabel = (type: string) => {
const labels: Record<string, string> = {
INDIVIDUAL: 'فرد',
COMPANY: 'شركة',
HOLDING: 'مجموعة',
GOVERNMENT: 'حكومي',
ORGANIZATION: 'منظمات',
EMBASSIES: 'سفارات',
BANK: 'بنوك',
UNIVERSITY: 'جامعات',
SCHOOL: 'مدارس',
UN: 'UN',
NGO: 'NGO',
INSTITUTION: 'مؤسسة'
}
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 || ''
}
const supplierSpecializations = [
'كاميرات',
'شبكات',
'أجهزة كومبيوتر',
'projectors',
'مقاسم هاتفية',
' Mobile - Tablet',
'firewall',
'طاقة بديلة',
'حديد',
' باركود - POS',
'أجهزة منزلية',
'تكييف وتبريد',
]
return (
<div className="min-h-screen bg-gray-50">
<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">
<Link
href="/dashboard"
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<ArrowLeft className="h-5 w-5 text-gray-600" />
</Link>
<div className="flex items-center gap-3">
<div className="bg-blue-100 p-2 rounded-lg">
<Users className="h-6 w-6 text-blue-600" />
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900">إدارة جهات الاتصال</h1>
<p className="text-sm text-gray-600">Contact Management</p>
</div>
</div>
</div>
<div className="flex items-center gap-3">
{selectedContacts.size > 0 && (
<div className="flex items-center gap-2 px-4 py-2 bg-blue-50 border border-blue-200 rounded-lg">
<span className="text-sm font-medium text-blue-700">
{selectedContacts.size} selected
</span>
<button
onClick={() => setShowBulkActions(!showBulkActions)}
className="text-blue-600 hover:text-blue-700"
>
Actions
</button>
<button
onClick={() => setSelectedContacts(new Set())}
className="text-blue-600 hover:text-blue-700"
>
<X className="h-4 w-4" />
</button>
</div>
)}
<button
onClick={() => setShowImportModal(true)}
className="flex items-center gap-2 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
<Upload className="h-4 w-4" />
Import
</button>
<button
onClick={() => setShowExportModal(true)}
className="flex items-center gap-2 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
<Download className="h-4 w-4" />
Export
</button>
<button
onClick={() => {
resetForm()
setCreateDefaultType('SUPPLIER')
setShowCreateModal(true)
}}
className="flex items-center gap-2 px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 transition-colors"
>
<Plus className="h-4 w-4" />
إضافة موردين
</button>
<button
onClick={() => {
resetForm()
setCreateDefaultType('INDIVIDUAL')
setShowCreateModal(true)
}}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Plus className="h-4 w-4" />
Add Contact
</button>
</div>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<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">
<div>
<p className="text-sm text-gray-600">Total Contacts</p>
<p className="text-3xl font-bold text-gray-900 mt-1">{total}</p>
</div>
<div className="bg-blue-100 p-3 rounded-lg">
<Users className="h-8 w-8 text-blue-600" />
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Active Individuals</p>
<p className="text-3xl font-bold text-gray-900 mt-1">
{contacts.filter(c => c.type === 'INDIVIDUAL' && c.status === 'ACTIVE').length}
</p>
</div>
<div className="bg-green-100 p-3 rounded-lg">
<UserPlus className="h-8 w-8 text-green-600" />
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Companies</p>
<p className="text-3xl font-bold text-gray-900 mt-1">
{contacts.filter(c => c.type === 'COMPANY').length}
</p>
</div>
<div className="bg-orange-100 p-3 rounded-lg">
<Star className="h-8 w-8 text-orange-600" />
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">This Page</p>
<p className="text-3xl font-bold text-gray-900 mt-1">{contacts.length}</p>
</div>
<div className="bg-purple-100 p-3 rounded-lg">
<Briefcase className="h-8 w-8 text-purple-600" />
</div>
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-200 mb-6">
<div className="space-y-4">
<div className="flex flex-col md:flex-row gap-4">
<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
type="text"
placeholder="Search contacts (name, email, company...)"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pr-10 pl-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white text-gray-900"
/>
</div>
<select
value={selectedType}
onChange={(e) => setSelectedType(e.target.value)}
className="px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white text-gray-900"
>
<option value="all">All Types</option>
<option value="INDIVIDUAL">Individuals</option>
<option value="COMPANY">Companies</option>
<option value="HOLDING">Holdings</option>
<option value="GOVERNMENT">Government</option>
<option value="ORGANIZATION">Organizations</option>
<option value="EMBASSIES">Embassies</option>
<option value="BANK">Banks</option>
<option value="UNIVERSITY">Universities</option>
<option value="SCHOOL">Schools</option>
<option value="UN">UN</option>
<option value="NGO">NGO</option>
<option value="INSTITUTION">Institution</option>
<option value="SUPPLIER">Suppliers - موردين</option>
</select>
<select
value={selectedStatus}
onChange={(e) => setSelectedStatus(e.target.value)}
className="px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white text-gray-900"
>
<option value="all">All Status</option>
<option value="ACTIVE">Active</option>
<option value="INACTIVE">Inactive</option>
</select>
<button
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
className={`flex items-center gap-2 px-4 py-3 border rounded-lg transition-colors ${
showAdvancedFilters
? 'bg-blue-50 border-blue-300 text-blue-700'
: 'border-gray-300 text-gray-700 hover:bg-gray-50'
}`}
>
<Filter className="h-4 w-4" />
Advanced
</button>
</div>
{showAdvancedFilters && (
<div className="pt-4 border-t border-gray-200">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Source</label>
<select
value={selectedSource}
onChange={(e) => setSelectedSource(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"
>
<option value="all">All Sources</option>
<option value="WEBSITE">Website</option>
<option value="REFERRAL">Referral</option>
<option value="COLD_CALL">Cold Call</option>
<option value="SOCIAL_MEDIA">Social Media</option>
<option value="EXHIBITION">Exhibition</option>
<option value="EVENT">Event</option>
<option value="VISIT">Visit</option>
<option value="OTHER">Other</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
اختصاص المورد
</label>
<select
value={selectedSpecialization}
onChange={(e) => setSelectedSpecialization(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"
>
<option value="all">كل الاختصاصات</option>
{supplierSpecializations.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Rating</label>
<select
value={selectedRating}
onChange={(e) => setSelectedRating(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"
>
<option value="all">All Ratings</option>
<option value="5">5 Stars</option>
<option value="4">4 Stars</option>
<option value="3">3 Stars</option>
<option value="2">2 Stars</option>
<option value="1">1 Star</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Category</label>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(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"
>
<option value="all">All Categories</option>
{flattenCategories(categories).map((cat) => (
<option key={cat.id} value={cat.id}>{cat.name}{cat.nameAr ? ` (${cat.nameAr})` : ''}</option>
))}
</select>
</div>
<div className="flex items-end">
<button
onClick={() => {
setSearchTerm('')
setSelectedType('all')
setSelectedStatus('all')
setSelectedSource('all')
setSelectedRating('all')
setSelectedCategory('all')
setCurrentPage(1)
setSelectedSpecialization('all')
}}
className="w-full px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
Clear All Filters
</button>
</div>
</div>
</div>
)}
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
{loading ? (
<div className="p-12">
<LoadingSpinner size="lg" message="Loading contacts..." />
</div>
) : error ? (
<div className="p-12 text-center">
<p className="text-red-600 mb-4">{error}</p>
<button
onClick={fetchContacts}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Retry
</button>
</div>
) : contacts.length === 0 ? (
<div className="p-12 text-center">
<Users className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-600 mb-4">No contacts found</p>
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
{createDefaultType === 'SUPPLIER' ? 'إضافة مورد' : 'Create New Contact'}
</button>
</div>
) : (
<>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-4 text-center w-12">
<input
type="checkbox"
checked={contacts.length > 0 && contacts.every(c => selectedContacts.has(c.id))}
onChange={(e) => {
if (e.target.checked) {
setSelectedContacts(new Set(contacts.map(c => c.id)))
} else {
setSelectedContacts(new Set())
}
}}
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">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>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{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>
<td className="px-6 py-4">
{getListCompanyName(contact) !== '-' ? (
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-slate-600 to-slate-700 flex items-center justify-center text-white font-bold">
{getListCompanyName(contact).charAt(0).toUpperCase()}
</div>
<div>
<p className="font-semibold text-gray-900">
{getListCompanyName(contact)}
</p>
</div>
</div>
) : (
<span className="text-sm text-gray-400">-</span>
)}
</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>
<p className="text-sm text-gray-900">
{getListContactName(contact)}
</p>
{getListContactNameAr(contact) && (
<p className="text-sm text-gray-600">
{getListContactNameAr(contact)}
</p>
)}
</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>
)
})}
</tbody>
</table>
</div>
<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{' '}
<span className="font-semibold">{Math.min(currentPage * pageSize, total)}</span> of{' '}
<span className="font-semibold">{total}</span> contacts
</p>
<div className="flex items-center gap-2">
<button
onClick={() => setCurrentPage(currentPage - 1)}
disabled={currentPage === 1}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
const page = i + 1
return (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={`px-4 py-2 rounded-lg transition-colors ${
currentPage === page
? 'bg-blue-600 text-white'
: 'border border-gray-300 text-gray-700 hover:bg-gray-50'
}`}
>
{page}
</button>
)
})}
{totalPages > 5 && <span className="px-2">...</span>}
<button
onClick={() => setCurrentPage(currentPage + 1)}
disabled={currentPage === totalPages}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
</>
)}
</div>
</main>
<Modal
isOpen={showCreateModal}
onClose={() => {
setShowCreateModal(false)
resetForm()
}}
title="Create New Contact"
size="xl"
>
<ContactForm
key={`create-${createDefaultType}`}
defaultType={createDefaultType}
onSubmit={async (data) => {
await handleCreate(data as CreateContactData)
}}
onCancel={() => {
setShowCreateModal(false)
resetForm()
}}
submitting={submitting}
/>
</Modal>
<Modal
isOpen={showEditModal}
onClose={() => {
setShowEditModal(false)
resetForm()
}}
title="Edit Contact"
size="xl"
>
<ContactForm
key={selectedContact?.id || 'edit-contact'}
contact={selectedContact || undefined}
onSubmit={async (data) => {
await handleEdit(data as UpdateContactData)
}}
onCancel={() => {
setShowEditModal(false)
resetForm()
}}
submitting={submitting}
/>
</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)} />
<div className="flex min-h-screen items-center justify-center p-4">
<div className="relative bg-white rounded-xl shadow-2xl p-6 max-w-md w-full">
<div className="flex items-center gap-4 mb-4">
<div className="bg-blue-100 p-3 rounded-full">
<Download className="h-6 w-6 text-blue-600" />
</div>
<div>
<h3 className="text-lg font-bold text-gray-900">Export Contacts</h3>
<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">
Export <span className="font-semibold">{total}</span> contacts matching current filters
</p>
<p className="text-xs text-gray-500">
Format: Excel (.xlsx)
</p>
</div>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={exportExcludeCompanyEmployees}
onChange={(e) => setExportExcludeCompanyEmployees(e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">Exclude company employees</span>
</label>
</div>
<div className="flex items-center justify-end gap-3">
<button
onClick={() => setShowExportModal(false)}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
disabled={exporting}
>
Cancel
</button>
<button
onClick={async () => {
setExporting(true)
try {
const filters: ContactFilters & { excludeCompanyEmployees?: boolean } = {}
if (searchTerm) filters.search = searchTerm
if (selectedType !== 'all') filters.type = selectedType
if (selectedStatus !== 'all') filters.status = selectedStatus
if (selectedCategory !== 'all') filters.category = selectedCategory
if (exportExcludeCompanyEmployees) filters.excludeCompanyEmployees = true
const blob = await contactsAPI.export(filters)
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `contacts_${new Date().toISOString().split('T')[0]}.xlsx`
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
toast.success('Contacts exported successfully!')
setShowExportModal(false)
} catch (err: any) {
toast.error(err.response?.data?.message || 'Failed to export contacts')
} finally {
setExporting(false)
}
}}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
disabled={exporting}
>
{exporting ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Exporting...
</>
) : (
<>
<Download className="h-4 w-4" />
Export
</>
)}
</button>
</div>
</div>
</div>
</div>
)}
{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)} />
<div className="flex min-h-screen items-center justify-center p-4">
<div className="relative bg-white rounded-xl shadow-2xl p-6 max-w-md w-full">
<div className="flex items-center gap-4 mb-4">
<div className="bg-red-100 p-3 rounded-full">
<Trash2 className="h-6 w-6 text-red-600" />
</div>
<div>
<h3 className="text-lg font-bold text-gray-900">Delete Contact</h3>
<p className="text-sm text-gray-600">This action cannot be undone</p>
</div>
</div>
<p className="text-gray-700 mb-6">
Are you sure you want to delete <span className="font-semibold">{selectedContact.name}</span>?
</p>
<div className="flex items-center justify-end gap-3">
<button
onClick={() => {
setShowDeleteDialog(false)
setSelectedContact(null)
}}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
disabled={submitting}
>
Cancel
</button>
<button
onClick={handleDelete}
className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50"
disabled={submitting}
>
{submitting ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Deleting...
</>
) : (
'Delete Contact'
)}
</button>
</div>
</div>
</div>
</div>
)}
{showImportModal && (
<ContactImport
onClose={() => setShowImportModal(false)}
onSuccess={() => {
setShowImportModal(false)
fetchContacts()
}}
/>
)}
</div>
)
}
export default function ContactsPage() {
return (
<ProtectedRoute>
<ContactsContent />
</ProtectedRoute>
)
}