Align badges and fix search

This commit is contained in:
2025-01-17 00:12:34 -05:00
parent 3f966ceac3
commit 0ba7d9f533
4 changed files with 272 additions and 81 deletions

View File

@@ -19,7 +19,7 @@ router.get('/', async (req, res) => {
const params = []; const params = [];
if (search) { if (search) {
whereConditions.push('(c.name LIKE ? OR c.description LIKE ?)'); whereConditions.push('(LOWER(c.name) LIKE LOWER(?) OR LOWER(c.description) LIKE LOWER(?))');
params.push(`%${search}%`, `%${search}%`); params.push(`%${search}%`, `%${search}%`);
} }

View File

@@ -19,7 +19,7 @@ router.get('/', async (req, res) => {
const params = []; const params = [];
if (search) { if (search) {
whereConditions.push('(p.vendor LIKE ? OR vd.contact_name LIKE ?)'); whereConditions.push('(LOWER(p.vendor) LIKE LOWER(?) OR LOWER(vd.contact_name) LIKE LOWER(?))');
params.push(`%${search}%`, `%${search}%`); params.push(`%${search}%`, `%${search}%`);
} }

View File

@@ -1,5 +1,5 @@
import { useState } from "react"; import { useState, useMemo } from "react";
import { useQuery, keepPreviousData } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
@@ -37,27 +37,101 @@ export function Categories() {
parent: "all", parent: "all",
performance: "all", performance: "all",
}); });
const [sorting, setSorting] = useState({
column: 'name',
direction: 'asc'
});
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
queryKey: ["categories", page, sortColumn, sortDirection, filters], queryKey: ["categories"],
queryFn: async () => { queryFn: async () => {
const searchParams = new URLSearchParams({ const response = await fetch(`${config.apiUrl}/categories`);
page: page.toString(),
limit: "50",
sortColumn: sortColumn.toString(),
sortDirection,
...filters.search && { search: filters.search },
...filters.parent !== "all" && { parent: filters.parent },
...filters.performance !== "all" && { performance: filters.performance },
});
const response = await fetch(`${config.apiUrl}/categories?${searchParams}`);
if (!response.ok) throw new Error("Failed to fetch categories"); if (!response.ok) throw new Error("Failed to fetch categories");
return response.json(); return response.json();
}, },
placeholderData: keepPreviousData,
}); });
// Filter and sort the data client-side
const filteredData = useMemo(() => {
if (!data?.categories) return [];
let filtered = [...data.categories];
// Apply search filter
if (filters.search) {
const searchLower = filters.search.toLowerCase();
filtered = filtered.filter(category =>
category.name.toLowerCase().includes(searchLower) ||
(category.description?.toLowerCase().includes(searchLower))
);
}
// Apply parent filter
if (filters.parent !== 'all') {
if (filters.parent === 'none') {
filtered = filtered.filter(category => !category.parent_category);
} else {
filtered = filtered.filter(category => category.parent_category === filters.parent);
}
}
// Apply performance filter
if (filters.performance !== 'all') {
filtered = filtered.filter(category => {
const growth = category.growth_rate ?? 0;
switch (filters.performance) {
case 'high_growth': return growth >= 20;
case 'growing': return growth >= 5 && growth < 20;
case 'stable': return growth >= -5 && growth < 5;
case 'declining': return growth < -5;
default: return true;
}
});
}
// Apply sorting
filtered.sort((a, b) => {
const aVal = a[sortColumn];
const bVal = b[sortColumn];
if (typeof aVal === 'number' && typeof bVal === 'number') {
return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
}
const aStr = String(aVal || '');
const bStr = String(bVal || '');
return sortDirection === 'asc' ?
aStr.localeCompare(bStr) :
bStr.localeCompare(aStr);
});
return filtered;
}, [data?.categories, filters, sortColumn, sortDirection]);
// Calculate pagination
const paginatedData = useMemo(() => {
const startIndex = (page - 1) * 50;
return filteredData.slice(startIndex, startIndex + 50);
}, [filteredData, page]);
// Calculate stats from filtered data
const stats = useMemo(() => {
if (!filteredData.length) return data?.stats;
const activeCategories = filteredData.filter(c => c.status === 'active').length;
const totalValue = filteredData.reduce((sum, c) => sum + (c.total_value || 0), 0);
const margins = filteredData.map(c => c.avg_margin || 0).filter(m => m !== 0);
const growthRates = filteredData.map(c => c.growth_rate || 0).filter(g => g !== 0);
return {
totalCategories: filteredData.length,
activeCategories,
totalValue,
avgMargin: margins.length ? margins.reduce((a, b) => a + b, 0) / margins.length : 0,
avgGrowth: growthRates.length ? growthRates.reduce((a, b) => a + b, 0) / growthRates.length : 0
};
}, [filteredData, data?.stats]);
const handleSort = (column: keyof Category) => { const handleSort = (column: keyof Category) => {
setSortDirection(prev => { setSortDirection(prev => {
if (sortColumn !== column) return "asc"; if (sortColumn !== column) return "asc";
@@ -83,23 +157,40 @@ export function Categories() {
}; };
return ( return (
<motion.div layout className="container mx-auto py-6 space-y-4"> <motion.div
<div className="flex items-center justify-between"> layout
transition={{
layout: {
duration: 0.15,
ease: [0.4, 0, 0.2, 1] // Material Design easing
}
}}
className="container mx-auto py-6 space-y-4"
>
<motion.div
layout="position"
transition={{ duration: 0.15 }}
className="flex items-center justify-between"
>
<h1 className="text-3xl font-bold tracking-tight">Categories</h1> <h1 className="text-3xl font-bold tracking-tight">Categories</h1>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{data?.pagination.total.toLocaleString() ?? "..."} categories {filteredData.length.toLocaleString()} categories
</div> </div>
</div> </motion.div>
<div className="grid gap-4 md:grid-cols-4"> <motion.div
layout="preserve-aspect"
transition={{ duration: 0.15 }}
className="grid gap-4 md:grid-cols-4"
>
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Categories</CardTitle> <CardTitle className="text-sm font-medium">Total Categories</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{data?.stats?.totalCategories ?? "..."}</div> <div className="text-2xl font-bold">{stats?.totalCategories ?? "..."}</div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{data?.stats?.activeCategories ?? "..."} active {stats?.activeCategories ?? "..."} active
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@@ -139,7 +230,7 @@ export function Categories() {
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
</div> </motion.div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex flex-1 items-center space-x-2"> <div className="flex flex-1 items-center space-x-2">
@@ -203,7 +294,7 @@ export function Categories() {
Loading categories... Loading categories...
</TableCell> </TableCell>
</TableRow> </TableRow>
) : data?.categories?.map((category: Category) => ( ) : paginatedData.map((category: Category) => (
<TableRow key={category.category_id}> <TableRow key={category.category_id}>
<TableCell> <TableCell>
<div className="font-medium">{category.name}</div> <div className="font-medium">{category.name}</div>
@@ -215,15 +306,17 @@ export function Categories() {
<TableCell>{typeof category.avg_margin === 'number' ? category.avg_margin.toFixed(1) : "0.0"}%</TableCell> <TableCell>{typeof category.avg_margin === 'number' ? category.avg_margin.toFixed(1) : "0.0"}%</TableCell>
<TableCell>{typeof category.turnover_rate === 'number' ? category.turnover_rate.toFixed(1) : "0.0"}x</TableCell> <TableCell>{typeof category.turnover_rate === 'number' ? category.turnover_rate.toFixed(1) : "0.0"}x</TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2" style={{ minWidth: '120px' }}>
{typeof category.growth_rate === 'number' ? category.growth_rate.toFixed(1) : "0.0"}% <div style={{ width: '50px', textAlign: 'right' }}>
{typeof category.growth_rate === 'number' ? category.growth_rate.toFixed(1) : "0.0"}%
</div>
{getPerformanceBadge(category.growth_rate ?? 0)} {getPerformanceBadge(category.growth_rate ?? 0)}
</div> </div>
</TableCell> </TableCell>
<TableCell>{category.status}</TableCell> <TableCell>{category.status}</TableCell>
</TableRow> </TableRow>
))} ))}
{!isLoading && !data?.categories.length && ( {!isLoading && !paginatedData.length && (
<TableRow> <TableRow>
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground"> <TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
No categories found No categories found
@@ -234,8 +327,12 @@ export function Categories() {
</Table> </Table>
</div> </div>
{data?.pagination.pages > 1 && ( {filteredData.length > 0 && (
<div className="flex justify-center"> <motion.div
layout="position"
transition={{ duration: 0.15 }}
className="flex justify-center"
>
<Pagination> <Pagination>
<PaginationContent> <PaginationContent>
<PaginationItem> <PaginationItem>
@@ -243,22 +340,22 @@ export function Categories() {
href="#" href="#"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
setPage(p => Math.max(1, p - 1)); if (page > 1) setPage(p => p - 1);
}} }}
aria-disabled={page === 1} aria-disabled={page === 1}
/> />
</PaginationItem> </PaginationItem>
{Array.from({ length: data.pagination.pages }, (_, i) => i + 1).map((p) => ( {Array.from({ length: Math.ceil(filteredData.length / 50) }, (_, i) => (
<PaginationItem key={p}> <PaginationItem key={i + 1}>
<PaginationLink <PaginationLink
href="#" href="#"
isActive={page === p}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
setPage(p); setPage(i + 1);
}} }}
isActive={page === i + 1}
> >
{p} {i + 1}
</PaginationLink> </PaginationLink>
</PaginationItem> </PaginationItem>
))} ))}
@@ -267,14 +364,14 @@ export function Categories() {
href="#" href="#"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
setPage(p => Math.min(data.pagination.pages, p + 1)); if (page < Math.ceil(filteredData.length / 50)) setPage(p => p + 1);
}} }}
aria-disabled={page === data.pagination.pages} aria-disabled={page >= Math.ceil(filteredData.length / 50)}
/> />
</PaginationItem> </PaginationItem>
</PaginationContent> </PaginationContent>
</Pagination> </Pagination>
</div> </motion.div>
)} )}
</motion.div> </motion.div>
); );

View File

@@ -1,5 +1,5 @@
import { useState } from "react"; import { useState, useMemo } from "react";
import { useQuery, keepPreviousData } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
@@ -38,27 +38,98 @@ export function Vendors() {
status: "all", status: "all",
performance: "all", performance: "all",
}); });
const [sorting, setSorting] = useState({
column: 'name',
direction: 'asc'
});
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
queryKey: ["vendors", page, sortColumn, sortDirection, filters], queryKey: ["vendors"],
queryFn: async () => { queryFn: async () => {
const searchParams = new URLSearchParams({ const response = await fetch(`${config.apiUrl}/vendors`);
page: page.toString(),
limit: "50",
sortColumn: sortColumn.toString(),
sortDirection,
...filters.search && { search: filters.search },
...filters.status !== "all" && { status: filters.status },
...filters.performance !== "all" && { performance: filters.performance },
});
const response = await fetch(`${config.apiUrl}/vendors?${searchParams}`);
if (!response.ok) throw new Error("Failed to fetch vendors"); if (!response.ok) throw new Error("Failed to fetch vendors");
return response.json(); return response.json();
}, },
placeholderData: keepPreviousData,
}); });
// Filter and sort the data client-side
const filteredData = useMemo(() => {
if (!data?.vendors) return [];
let filtered = [...data.vendors];
// Apply search filter
if (filters.search) {
const searchLower = filters.search.toLowerCase();
filtered = filtered.filter(vendor =>
vendor.name.toLowerCase().includes(searchLower) ||
vendor.contact_name?.toLowerCase().includes(searchLower) ||
vendor.email?.toLowerCase().includes(searchLower)
);
}
// Apply status filter
if (filters.status !== 'all') {
filtered = filtered.filter(vendor => vendor.status === filters.status);
}
// Apply performance filter
if (filters.performance !== 'all') {
filtered = filtered.filter(vendor => {
const fillRate = vendor.order_fill_rate ?? 0;
switch (filters.performance) {
case 'excellent': return fillRate >= 95;
case 'good': return fillRate >= 85 && fillRate < 95;
case 'fair': return fillRate >= 75 && fillRate < 85;
case 'poor': return fillRate < 75;
default: return true;
}
});
}
// Apply sorting
filtered.sort((a, b) => {
const aVal = a[sortColumn];
const bVal = b[sortColumn];
if (typeof aVal === 'number' && typeof bVal === 'number') {
return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
}
const aStr = String(aVal || '');
const bStr = String(bVal || '');
return sortDirection === 'asc' ?
aStr.localeCompare(bStr) :
bStr.localeCompare(aStr);
});
return filtered;
}, [data?.vendors, filters, sortColumn, sortDirection]);
// Calculate pagination
const paginatedData = useMemo(() => {
const startIndex = (page - 1) * 50;
return filteredData.slice(startIndex, startIndex + 50);
}, [filteredData, page]);
// Calculate stats from filtered data
const stats = useMemo(() => {
if (!filteredData.length) return data?.stats;
const activeVendors = filteredData.filter(v => v.status === 'active').length;
const leadTimes = filteredData.map(v => v.avg_lead_time_days || 0).filter(lt => lt !== 0);
const fillRates = filteredData.map(v => v.order_fill_rate || 0).filter(fr => fr !== 0);
const onTimeRates = filteredData.map(v => v.on_time_delivery_rate || 0).filter(otr => otr !== 0);
return {
totalVendors: filteredData.length,
activeVendors,
avgLeadTime: leadTimes.length ? leadTimes.reduce((a, b) => a + b, 0) / leadTimes.length : 0,
avgFillRate: fillRates.length ? fillRates.reduce((a, b) => a + b, 0) / fillRates.length : 0,
avgOnTimeDelivery: onTimeRates.length ? onTimeRates.reduce((a, b) => a + b, 0) / onTimeRates.length : 0
};
}, [filteredData, data?.stats]);
const handleSort = (column: keyof Vendor) => { const handleSort = (column: keyof Vendor) => {
setSortDirection(prev => { setSortDirection(prev => {
if (sortColumn !== column) return "asc"; if (sortColumn !== column) return "asc";
@@ -75,23 +146,40 @@ export function Vendors() {
}; };
return ( return (
<motion.div layout className="container mx-auto py-6 space-y-4"> <motion.div
<div className="flex items-center justify-between"> layout
transition={{
layout: {
duration: 0.15,
ease: [0.4, 0, 0.2, 1] // Material Design easing
}
}}
className="container mx-auto py-6 space-y-4"
>
<motion.div
layout="position"
transition={{ duration: 0.15 }}
className="flex items-center justify-between"
>
<h1 className="text-3xl font-bold tracking-tight">Vendors</h1> <h1 className="text-3xl font-bold tracking-tight">Vendors</h1>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{data?.pagination.total.toLocaleString() ?? "..."} vendors {filteredData.length.toLocaleString()} vendors
</div> </div>
</div> </motion.div>
<div className="grid gap-4 md:grid-cols-4"> <motion.div
layout="preserve-aspect"
transition={{ duration: 0.15 }}
className="grid gap-4 md:grid-cols-4"
>
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Vendors</CardTitle> <CardTitle className="text-sm font-medium">Total Vendors</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{data?.stats?.totalVendors ?? "..."}</div> <div className="text-2xl font-bold">{stats?.totalVendors ?? "..."}</div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{data?.stats?.activeVendors ?? "..."} active {stats?.activeVendors ?? "..."} active
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@@ -101,7 +189,7 @@ export function Vendors() {
<CardTitle className="text-sm font-medium">Avg Lead Time</CardTitle> <CardTitle className="text-sm font-medium">Avg Lead Time</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{typeof data?.stats?.avgLeadTime === 'number' ? data.stats.avgLeadTime.toFixed(1) : "..."} days</div> <div className="text-2xl font-bold">{typeof stats?.avgLeadTime === 'number' ? stats.avgLeadTime.toFixed(1) : "..."} days</div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Across all vendors Across all vendors
</p> </p>
@@ -113,7 +201,7 @@ export function Vendors() {
<CardTitle className="text-sm font-medium">Fill Rate</CardTitle> <CardTitle className="text-sm font-medium">Fill Rate</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{typeof data?.stats?.avgFillRate === 'number' ? data.stats.avgFillRate.toFixed(1) : "..."}%</div> <div className="text-2xl font-bold">{typeof stats?.avgFillRate === 'number' ? stats.avgFillRate.toFixed(1) : "..."}%</div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Average order fill rate Average order fill rate
</p> </p>
@@ -125,13 +213,13 @@ export function Vendors() {
<CardTitle className="text-sm font-medium">On-Time Delivery</CardTitle> <CardTitle className="text-sm font-medium">On-Time Delivery</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{typeof data?.stats?.avgOnTimeDelivery === 'number' ? data.stats.avgOnTimeDelivery.toFixed(1) : "..."}%</div> <div className="text-2xl font-bold">{typeof stats?.avgOnTimeDelivery === 'number' ? stats.avgOnTimeDelivery.toFixed(1) : "..."}%</div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Average on-time rate Average on-time rate
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
</div> </motion.div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex flex-1 items-center space-x-2"> <div className="flex flex-1 items-center space-x-2">
@@ -194,7 +282,7 @@ export function Vendors() {
Loading vendors... Loading vendors...
</TableCell> </TableCell>
</TableRow> </TableRow>
) : data?.vendors?.map((vendor: Vendor) => ( ) : paginatedData.map((vendor: Vendor) => (
<TableRow key={vendor.vendor_id}> <TableRow key={vendor.vendor_id}>
<TableCell className="font-medium">{vendor.name}</TableCell> <TableCell className="font-medium">{vendor.name}</TableCell>
<TableCell> <TableCell>
@@ -205,8 +293,10 @@ export function Vendors() {
<TableCell>{typeof vendor.avg_lead_time_days === 'number' ? vendor.avg_lead_time_days.toFixed(1) : "0.0"} days</TableCell> <TableCell>{typeof vendor.avg_lead_time_days === 'number' ? vendor.avg_lead_time_days.toFixed(1) : "0.0"} days</TableCell>
<TableCell>{typeof vendor.on_time_delivery_rate === 'number' ? vendor.on_time_delivery_rate.toFixed(1) : "0.0"}%</TableCell> <TableCell>{typeof vendor.on_time_delivery_rate === 'number' ? vendor.on_time_delivery_rate.toFixed(1) : "0.0"}%</TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2" style={{ minWidth: '120px' }}>
{typeof vendor.order_fill_rate === 'number' ? vendor.order_fill_rate.toFixed(1) : "0.0"}% <div style={{ width: '50px', textAlign: 'right' }}>
{typeof vendor.order_fill_rate === 'number' ? vendor.order_fill_rate.toFixed(1) : "0.0"}%
</div>
{getPerformanceBadge(vendor.order_fill_rate ?? 0)} {getPerformanceBadge(vendor.order_fill_rate ?? 0)}
</div> </div>
</TableCell> </TableCell>
@@ -214,7 +304,7 @@ export function Vendors() {
<TableCell>{vendor.active_products?.toLocaleString() ?? 0}</TableCell> <TableCell>{vendor.active_products?.toLocaleString() ?? 0}</TableCell>
</TableRow> </TableRow>
))} ))}
{!isLoading && !data?.vendors.length && ( {!isLoading && !paginatedData.length && (
<TableRow> <TableRow>
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground"> <TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
No vendors found No vendors found
@@ -225,8 +315,12 @@ export function Vendors() {
</Table> </Table>
</div> </div>
{data?.pagination.pages > 1 && ( {filteredData.length > 0 && (
<div className="flex justify-center"> <motion.div
layout="position"
transition={{ duration: 0.15 }}
className="flex justify-center"
>
<Pagination> <Pagination>
<PaginationContent> <PaginationContent>
<PaginationItem> <PaginationItem>
@@ -234,22 +328,22 @@ export function Vendors() {
href="#" href="#"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
setPage(p => Math.max(1, p - 1)); if (page > 1) setPage(p => p - 1);
}} }}
aria-disabled={page === 1} aria-disabled={page === 1}
/> />
</PaginationItem> </PaginationItem>
{Array.from({ length: data.pagination.pages }, (_, i) => i + 1).map((p) => ( {Array.from({ length: Math.ceil(filteredData.length / 50) }, (_, i) => (
<PaginationItem key={p}> <PaginationItem key={i + 1}>
<PaginationLink <PaginationLink
href="#" href="#"
isActive={page === p}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
setPage(p); setPage(i + 1);
}} }}
isActive={page === i + 1}
> >
{p} {i + 1}
</PaginationLink> </PaginationLink>
</PaginationItem> </PaginationItem>
))} ))}
@@ -258,14 +352,14 @@ export function Vendors() {
href="#" href="#"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
setPage(p => Math.min(data.pagination.pages, p + 1)); if (page < Math.ceil(filteredData.length / 50)) setPage(p => p + 1);
}} }}
aria-disabled={page === data.pagination.pages} aria-disabled={page >= Math.ceil(filteredData.length / 50)}
/> />
</PaginationItem> </PaginationItem>
</PaginationContent> </PaginationContent>
</Pagination> </Pagination>
</div> </motion.div>
)} )}
</motion.div> </motion.div>
); );