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 [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">
|
||||||
|
<div>
|
||||||
<h1 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">Customers</h1>
|
<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">
|
||||||
|
|||||||
@ -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