edit contact form
This commit is contained in:
@@ -58,7 +58,29 @@ router.put(
|
|||||||
authorize('contacts', 'contacts', 'update'),
|
authorize('contacts', 'contacts', 'update'),
|
||||||
[
|
[
|
||||||
param('id').isUUID(),
|
param('id').isUUID(),
|
||||||
body('email').optional().isEmail(),
|
body('type')
|
||||||
|
.optional()
|
||||||
|
.isIn([
|
||||||
|
'INDIVIDUAL',
|
||||||
|
'COMPANY',
|
||||||
|
'HOLDING',
|
||||||
|
'GOVERNMENT',
|
||||||
|
'ORGANIZATION',
|
||||||
|
'EMBASSIES',
|
||||||
|
'BANK',
|
||||||
|
'UNIVERSITY',
|
||||||
|
'SCHOOL',
|
||||||
|
'UN',
|
||||||
|
'NGO',
|
||||||
|
'INSTITUTION',
|
||||||
|
]),
|
||||||
|
body('email')
|
||||||
|
.optional({ values: 'falsy' })
|
||||||
|
.custom((value) => {
|
||||||
|
if (value === null || value === undefined || value === '') return true
|
||||||
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
||||||
|
})
|
||||||
|
.withMessage('Invalid email format'),
|
||||||
validate,
|
validate,
|
||||||
],
|
],
|
||||||
contactsController.update
|
contactsController.update
|
||||||
|
|||||||
@@ -330,9 +330,10 @@ class ContactsService {
|
|||||||
const contact = await prisma.contact.update({
|
const contact = await prisma.contact.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
|
type: data.type,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
nameAr: data.nameAr,
|
nameAr: data.nameAr,
|
||||||
email: data.email,
|
email: data.email === '' || data.email === undefined ? null : data.email,
|
||||||
phone: data.phone,
|
phone: data.phone,
|
||||||
mobile: data.mobile,
|
mobile: data.mobile,
|
||||||
website: data.website,
|
website: data.website,
|
||||||
@@ -344,11 +345,14 @@ class ContactsService {
|
|||||||
city: data.city,
|
city: data.city,
|
||||||
country: data.country,
|
country: data.country,
|
||||||
postalCode: data.postalCode,
|
postalCode: data.postalCode,
|
||||||
categories: data.categories ? {
|
categories: data.categories
|
||||||
set: data.categories.map(id => ({ id }))
|
? {
|
||||||
} : undefined,
|
set: data.categories.map((id) => ({ id })),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
tags: data.tags,
|
tags: data.tags,
|
||||||
employeeId: data.employeeId !== undefined ? (data.employeeId || null) : undefined,
|
employeeId:
|
||||||
|
data.employeeId !== undefined ? (data.employeeId || null) : undefined,
|
||||||
source: data.source,
|
source: data.source,
|
||||||
status: data.status,
|
status: data.status,
|
||||||
rating: data.rating,
|
rating: data.rating,
|
||||||
|
|||||||
@@ -41,20 +41,17 @@ function flattenCategories(cats: Category[], result: Category[] = []): Category[
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ContactsContent() {
|
function ContactsContent() {
|
||||||
// State Management
|
|
||||||
const [contacts, setContacts] = useState<Contact[]>([])
|
const [contacts, setContacts] = useState<Contact[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [selectedContacts, setSelectedContacts] = useState<Set<string>>(new Set())
|
const [selectedContacts, setSelectedContacts] = useState<Set<string>>(new Set())
|
||||||
const [showBulkActions, setShowBulkActions] = useState(false)
|
const [showBulkActions, setShowBulkActions] = useState(false)
|
||||||
|
|
||||||
// Pagination
|
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
const [totalPages, setTotalPages] = useState(1)
|
const [totalPages, setTotalPages] = useState(1)
|
||||||
const [total, setTotal] = useState(0)
|
const [total, setTotal] = useState(0)
|
||||||
const pageSize = 10
|
const pageSize = 10
|
||||||
|
|
||||||
// Filters
|
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [selectedType, setSelectedType] = useState('all')
|
const [selectedType, setSelectedType] = useState('all')
|
||||||
const [selectedStatus, setSelectedStatus] = useState('all')
|
const [selectedStatus, setSelectedStatus] = useState('all')
|
||||||
@@ -64,7 +61,6 @@ function ContactsContent() {
|
|||||||
const [categories, setCategories] = useState<Category[]>([])
|
const [categories, setCategories] = useState<Category[]>([])
|
||||||
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false)
|
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false)
|
||||||
|
|
||||||
// Modals
|
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||||
const [showEditModal, setShowEditModal] = useState(false)
|
const [showEditModal, setShowEditModal] = useState(false)
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||||
@@ -75,7 +71,6 @@ function ContactsContent() {
|
|||||||
const [exporting, setExporting] = useState(false)
|
const [exporting, setExporting] = useState(false)
|
||||||
const [exportExcludeCompanyEmployees, setExportExcludeCompanyEmployees] = useState(false)
|
const [exportExcludeCompanyEmployees, setExportExcludeCompanyEmployees] = useState(false)
|
||||||
|
|
||||||
// Fetch Contacts (with debouncing for search)
|
|
||||||
const fetchContacts = useCallback(async () => {
|
const fetchContacts = useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
@@ -104,21 +99,18 @@ function ContactsContent() {
|
|||||||
}
|
}
|
||||||
}, [currentPage, searchTerm, selectedType, selectedStatus, selectedSource, selectedRating, selectedCategory])
|
}, [currentPage, searchTerm, selectedType, selectedStatus, selectedSource, selectedRating, selectedCategory])
|
||||||
|
|
||||||
// Debounced search
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const debounce = setTimeout(() => {
|
const debounce = setTimeout(() => {
|
||||||
setCurrentPage(1) // Reset to page 1 on new search
|
setCurrentPage(1)
|
||||||
fetchContacts()
|
fetchContacts()
|
||||||
}, 500)
|
}, 500)
|
||||||
return () => clearTimeout(debounce)
|
return () => clearTimeout(debounce)
|
||||||
}, [searchTerm])
|
}, [searchTerm])
|
||||||
|
|
||||||
// Fetch on filter/page change
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchContacts()
|
fetchContacts()
|
||||||
}, [currentPage, selectedType, selectedStatus, selectedSource, selectedRating, selectedCategory])
|
}, [currentPage, selectedType, selectedStatus, selectedSource, selectedRating, selectedCategory])
|
||||||
|
|
||||||
// Create Contact
|
|
||||||
const handleCreate = async (data: CreateContactData) => {
|
const handleCreate = async (data: CreateContactData) => {
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
try {
|
try {
|
||||||
@@ -136,7 +128,6 @@ function ContactsContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edit Contact
|
|
||||||
const handleEdit = async (data: UpdateContactData) => {
|
const handleEdit = async (data: UpdateContactData) => {
|
||||||
if (!selectedContact) return
|
if (!selectedContact) return
|
||||||
|
|
||||||
@@ -156,7 +147,6 @@ function ContactsContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete Contact
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!selectedContact) return
|
if (!selectedContact) return
|
||||||
|
|
||||||
@@ -175,7 +165,6 @@ function ContactsContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility Functions
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
setSelectedContact(null)
|
setSelectedContact(null)
|
||||||
}
|
}
|
||||||
@@ -197,6 +186,7 @@ function ContactsContent() {
|
|||||||
HOLDING: 'bg-purple-100 text-purple-700',
|
HOLDING: 'bg-purple-100 text-purple-700',
|
||||||
GOVERNMENT: 'bg-orange-100 text-orange-700',
|
GOVERNMENT: 'bg-orange-100 text-orange-700',
|
||||||
ORGANIZATION: 'bg-cyan-100 text-cyan-700',
|
ORGANIZATION: 'bg-cyan-100 text-cyan-700',
|
||||||
|
EMBASSIES: 'bg-red-100 text-red-700',
|
||||||
BANK: 'bg-emerald-100 text-emerald-700',
|
BANK: 'bg-emerald-100 text-emerald-700',
|
||||||
UNIVERSITY: 'bg-indigo-100 text-indigo-700',
|
UNIVERSITY: 'bg-indigo-100 text-indigo-700',
|
||||||
SCHOOL: 'bg-yellow-100 text-yellow-700',
|
SCHOOL: 'bg-yellow-100 text-yellow-700',
|
||||||
@@ -218,6 +208,7 @@ function ContactsContent() {
|
|||||||
HOLDING: 'مجموعة',
|
HOLDING: 'مجموعة',
|
||||||
GOVERNMENT: 'حكومي',
|
GOVERNMENT: 'حكومي',
|
||||||
ORGANIZATION: 'منظمات',
|
ORGANIZATION: 'منظمات',
|
||||||
|
EMBASSIES: 'سفارات',
|
||||||
BANK: 'بنوك',
|
BANK: 'بنوك',
|
||||||
UNIVERSITY: 'جامعات',
|
UNIVERSITY: 'جامعات',
|
||||||
SCHOOL: 'مدارس',
|
SCHOOL: 'مدارس',
|
||||||
@@ -228,10 +219,36 @@ function ContactsContent() {
|
|||||||
return labels[type] || type
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
{/* Header */}
|
|
||||||
<header className="bg-white shadow-sm border-b">
|
<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="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 justify-between">
|
||||||
@@ -303,7 +320,6 @@ function ContactsContent() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<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="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="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -358,12 +374,9 @@ function ContactsContent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filters and Search */}
|
|
||||||
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-200 mb-6">
|
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-200 mb-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Main Filters Row */}
|
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
{/* Search */}
|
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
<Search className="absolute right-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
<Search className="absolute right-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
@@ -375,7 +388,6 @@ function ContactsContent() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Type Filter */}
|
|
||||||
<select
|
<select
|
||||||
value={selectedType}
|
value={selectedType}
|
||||||
onChange={(e) => setSelectedType(e.target.value)}
|
onChange={(e) => setSelectedType(e.target.value)}
|
||||||
@@ -396,7 +408,6 @@ function ContactsContent() {
|
|||||||
<option value="INSTITUTION">Institution</option>
|
<option value="INSTITUTION">Institution</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
{/* Status Filter */}
|
|
||||||
<select
|
<select
|
||||||
value={selectedStatus}
|
value={selectedStatus}
|
||||||
onChange={(e) => setSelectedStatus(e.target.value)}
|
onChange={(e) => setSelectedStatus(e.target.value)}
|
||||||
@@ -407,7 +418,6 @@ function ContactsContent() {
|
|||||||
<option value="INACTIVE">Inactive</option>
|
<option value="INACTIVE">Inactive</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
{/* Advanced Filters Toggle */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
|
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
|
||||||
className={`flex items-center gap-2 px-4 py-3 border rounded-lg transition-colors ${
|
className={`flex items-center gap-2 px-4 py-3 border rounded-lg transition-colors ${
|
||||||
@@ -421,11 +431,9 @@ function ContactsContent() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Advanced Filters */}
|
|
||||||
{showAdvancedFilters && (
|
{showAdvancedFilters && (
|
||||||
<div className="pt-4 border-t border-gray-200">
|
<div className="pt-4 border-t border-gray-200">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
{/* Source Filter */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">Source</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">Source</label>
|
||||||
<select
|
<select
|
||||||
@@ -445,7 +453,6 @@ function ContactsContent() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Rating Filter */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">Rating</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">Rating</label>
|
||||||
<select
|
<select
|
||||||
@@ -462,7 +469,6 @@ function ContactsContent() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Category Filter */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">Category</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">Category</label>
|
||||||
<select
|
<select
|
||||||
@@ -477,7 +483,6 @@ function ContactsContent() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Clear Filters */}
|
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -500,7 +505,6 @@ function ContactsContent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contacts Table */}
|
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="p-12">
|
<div className="p-12">
|
||||||
@@ -547,9 +551,9 @@ function ContactsContent() {
|
|||||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
</th>
|
</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">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">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">Status</th>
|
||||||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Actions</th>
|
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">Actions</th>
|
||||||
@@ -576,17 +580,18 @@ function ContactsContent() {
|
|||||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<div className="flex items-center gap-3">
|
{getListCompanyName(contact) !== '-' && (
|
||||||
<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">
|
<div className="flex items-center gap-2">
|
||||||
{contact.name.charAt(0)}
|
<Building2 className="h-4 w-4 text-gray-400" />
|
||||||
</div>
|
<span className="text-sm text-gray-900">
|
||||||
<div>
|
{getListCompanyName(contact)}
|
||||||
<p className="font-semibold text-gray-900">{contact.name}</p>
|
</span>
|
||||||
{contact.nameAr && <p className="text-sm text-gray-600">{contact.nameAr}</p>}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{contact.email && (
|
{contact.email && (
|
||||||
@@ -595,28 +600,40 @@ function ContactsContent() {
|
|||||||
{contact.email}
|
{contact.email}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{contact.phone && (
|
{(contact.phone || contact.mobile) && (
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||||
<Phone className="h-4 w-4" />
|
<Phone className="h-4 w-4" />
|
||||||
{contact.phone}
|
{contact.phone || contact.mobile}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
{contact.companyName && (
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<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">
|
||||||
<Building2 className="h-4 w-4 text-gray-400" />
|
{getListContactName(contact).charAt(0)}
|
||||||
<span className="text-sm text-gray-900">{contact.companyName}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
|
||||||
<td className="px-6 py-4">
|
<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)}`}>
|
<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" />
|
<Tag className="h-3 w-3" />
|
||||||
{getTypeLabel(contact.type)}
|
{getTypeLabel(contact.type)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${
|
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${
|
||||||
contact.status === 'ACTIVE'
|
contact.status === 'ACTIVE'
|
||||||
@@ -626,6 +643,7 @@ function ContactsContent() {
|
|||||||
{contact.status === 'ACTIVE' ? 'Active' : 'Inactive'}
|
{contact.status === 'ACTIVE' ? 'Active' : 'Inactive'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link
|
<Link
|
||||||
@@ -652,12 +670,12 @@ function ContactsContent() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)})}
|
)
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination */}
|
|
||||||
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
|
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
Showing <span className="font-semibold">{((currentPage - 1) * pageSize) + 1}</span> to{' '}
|
Showing <span className="font-semibold">{((currentPage - 1) * pageSize) + 1}</span> to{' '}
|
||||||
@@ -703,7 +721,6 @@ function ContactsContent() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Create Modal */}
|
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={showCreateModal}
|
isOpen={showCreateModal}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
@@ -714,6 +731,7 @@ function ContactsContent() {
|
|||||||
size="xl"
|
size="xl"
|
||||||
>
|
>
|
||||||
<ContactForm
|
<ContactForm
|
||||||
|
key="create-contact"
|
||||||
onSubmit={async (data) => {
|
onSubmit={async (data) => {
|
||||||
await handleCreate(data as CreateContactData)
|
await handleCreate(data as CreateContactData)
|
||||||
}}
|
}}
|
||||||
@@ -725,7 +743,6 @@ function ContactsContent() {
|
|||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* Edit Modal */}
|
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={showEditModal}
|
isOpen={showEditModal}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
@@ -736,6 +753,7 @@ function ContactsContent() {
|
|||||||
size="xl"
|
size="xl"
|
||||||
>
|
>
|
||||||
<ContactForm
|
<ContactForm
|
||||||
|
key={selectedContact?.id || 'edit-contact'}
|
||||||
contact={selectedContact || undefined}
|
contact={selectedContact || undefined}
|
||||||
onSubmit={async (data) => {
|
onSubmit={async (data) => {
|
||||||
await handleEdit(data as UpdateContactData)
|
await handleEdit(data as UpdateContactData)
|
||||||
@@ -748,7 +766,6 @@ function ContactsContent() {
|
|||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* Export Modal */}
|
|
||||||
{showExportModal && (
|
{showExportModal && (
|
||||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
<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="fixed inset-0 bg-black bg-opacity-50" onClick={() => setShowExportModal(false)} />
|
||||||
@@ -842,7 +859,6 @@ function ContactsContent() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Delete Confirmation Dialog */}
|
|
||||||
{showDeleteDialog && selectedContact && (
|
{showDeleteDialog && selectedContact && (
|
||||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
<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="fixed inset-0 bg-black bg-opacity-50" onClick={() => setShowDeleteDialog(false)} />
|
||||||
@@ -891,7 +907,6 @@ function ContactsContent() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Import Modal */}
|
|
||||||
{showImportModal && (
|
{showImportModal && (
|
||||||
<ContactImport
|
<ContactImport
|
||||||
onClose={() => setShowImportModal(false)}
|
onClose={() => setShowImportModal(false)}
|
||||||
|
|||||||
@@ -15,11 +15,7 @@ interface ContactFormProps {
|
|||||||
submitting?: boolean
|
submitting?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ContactForm({ contact, onSubmit, onCancel, submitting = false }: ContactFormProps) {
|
const buildInitialFormData = (contact?: Contact): CreateContactData => ({
|
||||||
const isEdit = !!contact
|
|
||||||
|
|
||||||
// Form state
|
|
||||||
const [formData, setFormData] = useState<CreateContactData>({
|
|
||||||
type: contact?.type || 'INDIVIDUAL',
|
type: contact?.type || 'INDIVIDUAL',
|
||||||
name: contact?.name || '',
|
name: contact?.name || '',
|
||||||
nameAr: contact?.nameAr,
|
nameAr: contact?.nameAr,
|
||||||
@@ -43,12 +39,23 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
customFields: contact?.customFields
|
customFields: contact?.customFields
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export default function ContactForm({ contact, onSubmit, onCancel, submitting = false }: ContactFormProps) {
|
||||||
|
const isEdit = !!contact
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<CreateContactData>(buildInitialFormData(contact))
|
||||||
const [rating, setRating] = useState<number>(contact?.rating || 0)
|
const [rating, setRating] = useState<number>(contact?.rating || 0)
|
||||||
const [newTag, setNewTag] = useState('')
|
const [newTag, setNewTag] = useState('')
|
||||||
const [formErrors, setFormErrors] = useState<Record<string, string>>({})
|
const [formErrors, setFormErrors] = useState<Record<string, string>>({})
|
||||||
const [categories, setCategories] = useState<Category[]>([])
|
const [categories, setCategories] = useState<Category[]>([])
|
||||||
const [employees, setEmployees] = useState<Employee[]>([])
|
const [employees, setEmployees] = useState<Employee[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFormData(buildInitialFormData(contact))
|
||||||
|
setRating(contact?.rating || 0)
|
||||||
|
setNewTag('')
|
||||||
|
setFormErrors({})
|
||||||
|
}, [contact])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
categoriesAPI.getTree().then(setCategories).catch(() => {})
|
categoriesAPI.getTree().then(setCategories).catch(() => {})
|
||||||
}, [])
|
}, [])
|
||||||
@@ -72,7 +79,23 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
|
|
||||||
const isCompanyEmployeeSelected = companyEmployeeCategoryId && (formData.categories || []).includes(companyEmployeeCategoryId)
|
const isCompanyEmployeeSelected = companyEmployeeCategoryId && (formData.categories || []).includes(companyEmployeeCategoryId)
|
||||||
|
|
||||||
// Validation
|
const organizationTypes = new Set([
|
||||||
|
'COMPANY',
|
||||||
|
'HOLDING',
|
||||||
|
'GOVERNMENT',
|
||||||
|
'ORGANIZATION',
|
||||||
|
'EMBASSIES',
|
||||||
|
'BANK',
|
||||||
|
'UNIVERSITY',
|
||||||
|
'SCHOOL',
|
||||||
|
'UN',
|
||||||
|
'NGO',
|
||||||
|
'INSTITUTION',
|
||||||
|
])
|
||||||
|
|
||||||
|
const isOrganizationType = organizationTypes.has(formData.type)
|
||||||
|
const showCompanyFields = isOrganizationType
|
||||||
|
|
||||||
const validateForm = (): boolean => {
|
const validateForm = (): boolean => {
|
||||||
const errors: Record<string, string> = {}
|
const errors: Record<string, string> = {}
|
||||||
|
|
||||||
@@ -103,28 +126,49 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!validateForm()) return
|
if (!validateForm()) return
|
||||||
|
|
||||||
// Clean up empty strings to undefined for optional fields
|
|
||||||
const cleanData = Object.entries({ ...formData, rating }).reduce((acc, [key, value]) => {
|
const cleanData = Object.entries({ ...formData, rating }).reduce((acc, [key, value]) => {
|
||||||
// Keep the value if it's not an empty string, or if it's a required field
|
const requiredFields = ['type', 'name', 'source', 'country']
|
||||||
if (value !== '' || ['type', 'name', 'source', 'country'].includes(key)) {
|
|
||||||
|
// keep required fields as-is
|
||||||
|
if (requiredFields.includes(key)) {
|
||||||
|
acc[key] = value
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
// in edit mode, allow clearing optional fields by sending null
|
||||||
|
if (isEdit && value === '') {
|
||||||
|
acc[key] = null
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
// in create mode, ignore empty optional fields
|
||||||
|
if (value !== '') {
|
||||||
acc[key] = value
|
acc[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
}, {} as any)
|
}, {} as any)
|
||||||
|
|
||||||
// Remove parentId if it's empty or undefined
|
|
||||||
if (!cleanData.parentId) {
|
if (!cleanData.parentId) {
|
||||||
delete cleanData.parentId
|
delete cleanData.parentId
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove categories if empty array
|
|
||||||
if (cleanData.categories && cleanData.categories.length === 0) {
|
if (cleanData.categories && cleanData.categories.length === 0) {
|
||||||
delete cleanData.categories
|
delete cleanData.categories
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove employeeId if empty
|
if (!cleanData.parentId) {
|
||||||
if (!cleanData.employeeId) {
|
delete cleanData.parentId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanData.categories && cleanData.categories.length === 0) {
|
||||||
|
delete cleanData.categories
|
||||||
|
}
|
||||||
|
|
||||||
|
// employeeId:
|
||||||
|
// - in create: remove if empty
|
||||||
|
// - in edit: keep null if user cleared it
|
||||||
|
if (!isEdit && !cleanData.employeeId) {
|
||||||
delete cleanData.employeeId
|
delete cleanData.employeeId
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,16 +196,12 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const showCompanyFields = ['COMPANY', 'HOLDING', 'GOVERNMENT'].includes(formData.type)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} noValidate className="space-y-6">
|
||||||
{/* Basic Information Section */}
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Basic Information</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Basic Information</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{/* Contact Type */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Contact Type <span className="text-red-500">*</span>
|
Contact Type <span className="text-red-500">*</span>
|
||||||
@@ -187,7 +227,6 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
{formErrors.type && <p className="text-red-500 text-xs mt-1">{formErrors.type}</p>}
|
{formErrors.type && <p className="text-red-500 text-xs mt-1">{formErrors.type}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Source */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Source <span className="text-red-500">*</span>
|
Source <span className="text-red-500">*</span>
|
||||||
@@ -210,37 +249,20 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Name */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Name <span className="text-red-500">*</span>
|
{isOrganizationType ? 'Contact Person Name' : 'Name'} <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, name: 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"
|
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"
|
||||||
placeholder="Enter contact name"
|
placeholder={isOrganizationType ? 'Enter contact person name' : 'Enter contact name'}
|
||||||
/>
|
/>
|
||||||
{formErrors.name && <p className="text-red-500 text-xs mt-1">{formErrors.name}</p>}
|
{formErrors.name && <p className="text-red-500 text-xs mt-1">{formErrors.name}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Arabic Name */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Arabic Name - الاسم بالعربية
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.nameAr || ''}
|
|
||||||
onChange={(e) => setFormData({ ...formData, nameAr: 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"
|
|
||||||
placeholder="أدخل الاسم بالعربية"
|
|
||||||
dir="rtl"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Rating */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Rating
|
Rating
|
||||||
@@ -276,12 +298,10 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contact Methods Section */}
|
|
||||||
<div className="pt-6 border-t">
|
<div className="pt-6 border-t">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Contact Methods</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Contact Methods</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{/* Email */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Email
|
Email
|
||||||
@@ -296,7 +316,6 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
{formErrors.email && <p className="text-red-500 text-xs mt-1">{formErrors.email}</p>}
|
{formErrors.email && <p className="text-red-500 text-xs mt-1">{formErrors.email}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Phone */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Phone
|
Phone
|
||||||
@@ -313,7 +332,6 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{/* Mobile */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Mobile
|
Mobile
|
||||||
@@ -327,7 +345,6 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Website */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Website
|
Website
|
||||||
@@ -344,44 +361,27 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Company Information Section (conditional) */}
|
|
||||||
{showCompanyFields && (
|
{showCompanyFields && (
|
||||||
<div className="pt-6 border-t">
|
<div className="pt-6 border-t">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Company Information</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Company Information</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{/* Company Name */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Company Name
|
{formData.type === 'EMBASSIES' ? 'Embassy Name' : 'Company / Organization Name'}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.companyName || ''}
|
value={formData.companyName || ''}
|
||||||
onChange={(e) => setFormData({ ...formData, companyName: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, companyName: 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"
|
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"
|
||||||
placeholder="Company name"
|
placeholder={formData.type === 'EMBASSIES' ? 'Embassy name' : 'Company / organization name'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Company Name Arabic */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Company Name (Arabic) - اسم الشركة
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.companyNameAr || ''}
|
|
||||||
onChange={(e) => setFormData({ ...formData, companyNameAr: 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"
|
|
||||||
placeholder="اسم الشركة بالعربية"
|
|
||||||
dir="rtl"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{/* Tax Number */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Tax Number
|
Tax Number
|
||||||
@@ -395,7 +395,6 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Commercial Register */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Commercial Register
|
Commercial Register
|
||||||
@@ -413,11 +412,9 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Address Section */}
|
|
||||||
<div className="pt-6 border-t">
|
<div className="pt-6 border-t">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Address Information</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Address Information</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Address */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Street Address
|
Street Address
|
||||||
@@ -432,7 +429,6 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
{/* City */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
City
|
City
|
||||||
@@ -446,7 +442,6 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Country */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Country
|
Country
|
||||||
@@ -460,7 +455,6 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Postal Code */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Postal Code
|
Postal Code
|
||||||
@@ -477,7 +471,6 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Categories Section */}
|
|
||||||
<div className="pt-6 border-t">
|
<div className="pt-6 border-t">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Categories</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Categories</h3>
|
||||||
<CategorySelector
|
<CategorySelector
|
||||||
@@ -487,7 +480,6 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Employee Link - when Company Employee category is selected */}
|
|
||||||
{isCompanyEmployeeSelected && (
|
{isCompanyEmployeeSelected && (
|
||||||
<div className="pt-6 border-t">
|
<div className="pt-6 border-t">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Link to Employee (Optional)</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Link to Employee (Optional)</h3>
|
||||||
@@ -509,11 +501,9 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tags Section */}
|
|
||||||
<div className="pt-6 border-t">
|
<div className="pt-6 border-t">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Tags</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Tags</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* Tag input */}
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -532,7 +522,6 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tags display */}
|
|
||||||
{formData.tags && formData.tags.length > 0 && (
|
{formData.tags && formData.tags.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{formData.tags.map((tag, index) => (
|
{formData.tags.map((tag, index) => (
|
||||||
@@ -555,7 +544,6 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Duplicate Detection */}
|
|
||||||
<DuplicateAlert
|
<DuplicateAlert
|
||||||
email={formData.email}
|
email={formData.email}
|
||||||
phone={formData.phone}
|
phone={formData.phone}
|
||||||
@@ -564,14 +552,12 @@ export default function ContactForm({ contact, onSubmit, onCancel, submitting =
|
|||||||
commercialRegister={formData.commercialRegister}
|
commercialRegister={formData.commercialRegister}
|
||||||
excludeId={contact?.id}
|
excludeId={contact?.id}
|
||||||
onMerge={(contactId) => {
|
onMerge={(contactId) => {
|
||||||
// Navigate to merge page with pre-selected contacts
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.location.href = `/contacts/merge?sourceId=${contactId}`
|
window.location.href = `/contacts/merge?sourceId=${contactId}`
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Form Actions */}
|
|
||||||
<div className="flex items-center justify-end gap-3 pt-6 border-t">
|
<div className="flex items-center justify-end gap-3 pt-6 border-t">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user