Compare commits
4 Commits
features/p
...
45164faa9d
| Author | SHA1 | Date | |
|---|---|---|---|
| 45164faa9d | |||
| 0e712f2e2f | |||
| c4439e779f | |||
| b512aef63d |
@ -54,7 +54,6 @@ export default function ContactRecordsList() {
|
|||||||
|
|
||||||
// State for data
|
// State for data
|
||||||
const [contactRecords, setContactRecords] = useState<ContactRecord[]>([]);
|
const [contactRecords, setContactRecords] = useState<ContactRecord[]>([]);
|
||||||
const [customers, setCustomers] = useState<Customer[]>([]);
|
|
||||||
const [pagination, setPagination] = useState<PaginationInfo>({
|
const [pagination, setPagination] = useState<PaginationInfo>({
|
||||||
page: initialPage,
|
page: initialPage,
|
||||||
pageSize: initialPageSize,
|
pageSize: initialPageSize,
|
||||||
@ -64,25 +63,6 @@ export default function ContactRecordsList() {
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Fetch customers for the filter dropdown
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchCustomers = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/customers');
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to fetch customers');
|
|
||||||
}
|
|
||||||
const data = await response.json();
|
|
||||||
setCustomers(data);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching customers:', err);
|
|
||||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchCustomers();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Fetch contact records with filters and pagination
|
// Fetch contact records with filters and pagination
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchContactRecords = async () => {
|
const fetchContactRecords = async () => {
|
||||||
@ -173,22 +153,17 @@ export default function ContactRecordsList() {
|
|||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-4">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="customerId" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label htmlFor="customerId" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Customer
|
Search Customer
|
||||||
</label>
|
</label>
|
||||||
<select
|
<input
|
||||||
|
type="text"
|
||||||
id="customerId"
|
id="customerId"
|
||||||
name="customerId"
|
name="customerId"
|
||||||
value={customerId}
|
value={customerId}
|
||||||
onChange={(e) => setCustomerId(e.target.value)}
|
onChange={(e) => setCustomerId(e.target.value)}
|
||||||
|
placeholder="Search customer..."
|
||||||
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:bg-gray-800 dark:border-gray-700 dark:text-gray-200"
|
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:bg-gray-800 dark:border-gray-700 dark:text-gray-200"
|
||||||
>
|
/>
|
||||||
<option value="">All Customers</option>
|
|
||||||
{customers.map((customer) => (
|
|
||||||
<option key={customer.id} value={customer.id}>
|
|
||||||
{customer.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="contactType" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label htmlFor="contactType" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export default function ContactRecordList({ customerId }: ContactRecordListProps
|
|||||||
|
|
||||||
// Function to fetch contact records
|
// Function to fetch contact records
|
||||||
const fetchContactRecords = async () => {
|
const fetchContactRecords = async () => {
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/contact-records?customerId=${customerId}`);
|
const response = await fetch(`/api/contact-records?customerId=${customerId}`);
|
||||||
@ -35,7 +36,7 @@ export default function ContactRecordList({ customerId }: ContactRecordListProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setContactRecords(data);
|
setContactRecords(data.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'An error occurred while fetching contact records');
|
setError(err instanceof Error ? err.message : 'An error occurred while fetching contact records');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -39,6 +39,15 @@ export default function AdminCustomers() {
|
|||||||
const [page, setPage] = useState(initialPage);
|
const [page, setPage] = useState(initialPage);
|
||||||
const [pageSize, setPageSize] = useState(initialPageSize);
|
const [pageSize, setPageSize] = useState(initialPageSize);
|
||||||
|
|
||||||
|
// Search filters state
|
||||||
|
const [filters, setFilters] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
url: '',
|
||||||
|
hasEmail: false
|
||||||
|
});
|
||||||
|
const [debouncedFilters, setDebouncedFilters] = useState(filters);
|
||||||
|
|
||||||
// State for data
|
// State for data
|
||||||
const [customers, setCustomers] = useState<Customer[]>([]);
|
const [customers, setCustomers] = useState<Customer[]>([]);
|
||||||
const [pagination, setPagination] = useState<PaginationInfo>({
|
const [pagination, setPagination] = useState<PaginationInfo>({
|
||||||
@ -57,11 +66,16 @@ export default function AdminCustomers() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build query string with pagination
|
// Build query string with pagination and filters
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append('page', initialPage.toString());
|
params.append('page', initialPage.toString());
|
||||||
params.append('pageSize', initialPageSize.toString());
|
params.append('pageSize', initialPageSize.toString());
|
||||||
|
|
||||||
|
if (debouncedFilters.name) params.append('name', debouncedFilters.name);
|
||||||
|
if (debouncedFilters.email) params.append('email', debouncedFilters.email);
|
||||||
|
if (debouncedFilters.url) params.append('url', debouncedFilters.url);
|
||||||
|
if (debouncedFilters.hasEmail) params.append('hasEmail', 'true');
|
||||||
|
|
||||||
const response = await fetch(`/api/customers?${params.toString()}`);
|
const response = await fetch(`/api/customers?${params.toString()}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -80,7 +94,31 @@ export default function AdminCustomers() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchCustomers();
|
fetchCustomers();
|
||||||
}, [initialPage, initialPageSize]);
|
}, [initialPage, initialPageSize, debouncedFilters]);
|
||||||
|
|
||||||
|
// Debounce filter changes
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setDebouncedFilters(filters);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [filters]);
|
||||||
|
|
||||||
|
// Handle filter changes
|
||||||
|
const handleFilterChange = (key: keyof typeof filters, value: string | boolean) => {
|
||||||
|
setFilters(prev => ({ ...prev, [key]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
const handleClearFilters = () => {
|
||||||
|
setFilters({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
url: '',
|
||||||
|
hasEmail: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Handle page change
|
// Handle page change
|
||||||
const handlePageChange = (newPage: number) => {
|
const handlePageChange = (newPage: number) => {
|
||||||
@ -96,8 +134,15 @@ export default function AdminCustomers() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h1 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">Customers</h1>
|
<div>
|
||||||
|
<h1 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">Customers</h1>
|
||||||
|
{!isLoading && (
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
Total: {pagination.totalCount} customer{pagination.totalCount !== 1 ? 's' : ''}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/admin/customers/new"
|
href="/admin/customers/new"
|
||||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
@ -106,6 +151,123 @@ export default function AdminCustomers() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Search Filters */}
|
||||||
|
<div className="mb-6 bg-white dark:bg-gray-800 shadow rounded-lg p-6">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h2 className="text-lg font-medium text-gray-900 dark:text-gray-100">Filters</h2>
|
||||||
|
<button
|
||||||
|
onClick={handleClearFilters}
|
||||||
|
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
|
>
|
||||||
|
Clear all filters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="name-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Search by Name
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 relative rounded-md shadow-sm">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name-filter"
|
||||||
|
value={filters.name}
|
||||||
|
onChange={(e) => handleFilterChange('name', e.target.value)}
|
||||||
|
className="block w-full rounded-md border-gray-300 pr-10 focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white sm:text-sm"
|
||||||
|
placeholder="Enter name..."
|
||||||
|
/>
|
||||||
|
{filters.name && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleFilterChange('name', '')}
|
||||||
|
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||||
|
>
|
||||||
|
<svg className="h-4 w-4 text-gray-400 hover:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Search by Email
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 relative rounded-md shadow-sm">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="email-filter"
|
||||||
|
value={filters.email}
|
||||||
|
onChange={(e) => handleFilterChange('email', e.target.value)}
|
||||||
|
className="block w-full rounded-md border-gray-300 pr-10 focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white sm:text-sm"
|
||||||
|
placeholder="Enter email..."
|
||||||
|
/>
|
||||||
|
{filters.email && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleFilterChange('email', '')}
|
||||||
|
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||||
|
>
|
||||||
|
<svg className="h-4 w-4 text-gray-400 hover:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="url-filter" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Search by URL
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 relative rounded-md shadow-sm">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="url-filter"
|
||||||
|
value={filters.url}
|
||||||
|
onChange={(e) => handleFilterChange('url', e.target.value)}
|
||||||
|
className="block w-full rounded-md border-gray-300 pr-10 focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white sm:text-sm"
|
||||||
|
placeholder="Enter URL..."
|
||||||
|
/>
|
||||||
|
{filters.url && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleFilterChange('url', '')}
|
||||||
|
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||||
|
>
|
||||||
|
<svg className="h-4 w-4 text-gray-400 hover:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||||
|
Email Filter
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="has-email"
|
||||||
|
checked={filters.hasEmail}
|
||||||
|
onChange={(e) => handleFilterChange('hasEmail', e.target.checked)}
|
||||||
|
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
<label htmlFor="has-email" className="ml-2 block text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
Has Email
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(filters.name || filters.email || filters.url || filters.hasEmail) && (
|
||||||
|
<div className="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Active filters: {[
|
||||||
|
filters.name && 'Name',
|
||||||
|
filters.email && 'Email',
|
||||||
|
filters.url && 'URL',
|
||||||
|
filters.hasEmail && 'Has Email'
|
||||||
|
].filter(Boolean).join(', ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Error Message */}
|
{/* Error Message */}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-50 border-l-4 border-red-400 p-4 my-4 dark:bg-red-900/20 dark:border-red-500">
|
<div className="bg-red-50 border-l-4 border-red-400 p-4 my-4 dark:bg-red-900/20 dark:border-red-500">
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const dataSource = await getDataSource();
|
const dataSource = await getDataSource();
|
||||||
const contactRecordRepository = dataSource.getRepository(ContactRecord);
|
const contactRecordRepository = dataSource.getRepository(ContactRecord);
|
||||||
|
|
||||||
|
|
||||||
// Get query parameters
|
// Get query parameters
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const customerId = url.searchParams.get('customerId');
|
const customerId = url.searchParams.get('customerId');
|
||||||
|
|||||||
@ -15,9 +15,31 @@ export async function GET(request: NextRequest) {
|
|||||||
const pageSize = parseInt(url.searchParams.get('pageSize') || '10');
|
const pageSize = parseInt(url.searchParams.get('pageSize') || '10');
|
||||||
const skip = (page - 1) * pageSize;
|
const skip = (page - 1) * pageSize;
|
||||||
|
|
||||||
|
// Get filter parameters
|
||||||
|
const nameFilter = url.searchParams.get('name');
|
||||||
|
const emailFilter = url.searchParams.get('email');
|
||||||
|
const urlFilter = url.searchParams.get('url');
|
||||||
|
const hasEmailFilter = url.searchParams.get('hasEmail');
|
||||||
|
|
||||||
// Build query
|
// Build query
|
||||||
let queryBuilder = customerRepository.createQueryBuilder('customer')
|
let queryBuilder = customerRepository.createQueryBuilder('customer');
|
||||||
.orderBy('customer.createdAt', 'DESC');
|
|
||||||
|
// Apply filters
|
||||||
|
if (nameFilter) {
|
||||||
|
queryBuilder = queryBuilder.andWhere('LOWER(customer.name) LIKE LOWER(:name)', { name: `%${nameFilter}%` });
|
||||||
|
}
|
||||||
|
if (emailFilter) {
|
||||||
|
queryBuilder = queryBuilder.andWhere('LOWER(customer.email) LIKE LOWER(:email)', { email: `%${emailFilter}%` });
|
||||||
|
}
|
||||||
|
if (urlFilter) {
|
||||||
|
queryBuilder = queryBuilder.andWhere('LOWER(customer.url) LIKE LOWER(:url)', { url: `%${urlFilter}%` });
|
||||||
|
}
|
||||||
|
if (hasEmailFilter === 'true') {
|
||||||
|
queryBuilder = queryBuilder.andWhere('customer.email IS NOT NULL AND customer.email != :emptyString', { emptyString: '' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add ordering
|
||||||
|
queryBuilder = queryBuilder.orderBy('customer.createdAt', 'DESC');
|
||||||
|
|
||||||
// Get total count for pagination
|
// Get total count for pagination
|
||||||
const totalCount = await queryBuilder.getCount();
|
const totalCount = await queryBuilder.getCount();
|
||||||
|
|||||||
Reference in New Issue
Block a user