Compare commits
2 Commits
c4439e779f
...
45164faa9d
| Author | SHA1 | Date | |
|---|---|---|---|
| 45164faa9d | |||
| 0e712f2e2f |
@ -39,6 +39,15 @@ export default function AdminCustomers() {
|
||||
const [page, setPage] = useState(initialPage);
|
||||
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
|
||||
const [customers, setCustomers] = useState<Customer[]>([]);
|
||||
const [pagination, setPagination] = useState<PaginationInfo>({
|
||||
@ -57,11 +66,16 @@ export default function AdminCustomers() {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Build query string with pagination
|
||||
// Build query string with pagination and filters
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', initialPage.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()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
@ -80,7 +94,31 @@ export default function AdminCustomers() {
|
||||
};
|
||||
|
||||
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
|
||||
const handlePageChange = (newPage: number) => {
|
||||
@ -96,8 +134,15 @@ export default function AdminCustomers() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<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
|
||||
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"
|
||||
@ -106,6 +151,123 @@ export default function AdminCustomers() {
|
||||
</Link>
|
||||
</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 && (
|
||||
<div className="bg-red-50 border-l-4 border-red-400 p-4 my-4 dark:bg-red-900/20 dark:border-red-500">
|
||||
|
||||
@ -15,9 +15,31 @@ export async function GET(request: NextRequest) {
|
||||
const pageSize = parseInt(url.searchParams.get('pageSize') || '10');
|
||||
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
|
||||
let queryBuilder = customerRepository.createQueryBuilder('customer')
|
||||
.orderBy('customer.createdAt', 'DESC');
|
||||
let queryBuilder = customerRepository.createQueryBuilder('customer');
|
||||
|
||||
// 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
|
||||
const totalCount = await queryBuilder.getCount();
|
||||
|
||||
Reference in New Issue
Block a user