Rearrange vendors and purchase orders page content, fix pagination

This commit is contained in:
2025-01-17 00:53:58 -05:00
parent 0ba7d9f533
commit 2235121761
4 changed files with 133 additions and 117 deletions

View File

@@ -15,39 +15,43 @@ router.get('/', async (req, res) => {
const sortDirection = req.query.sortDirection || 'asc'; const sortDirection = req.query.sortDirection || 'asc';
// Build the WHERE clause based on filters // Build the WHERE clause based on filters
const whereConditions = []; const whereConditions = ['p.vendor IS NOT NULL AND p.vendor != \'\''];
const params = []; const params = [];
if (search) { if (search) {
whereConditions.push('(LOWER(p.vendor) LIKE LOWER(?) OR LOWER(vd.contact_name) LIKE LOWER(?))'); whereConditions.push('LOWER(p.vendor) LIKE LOWER(?)');
params.push(`%${search}%`, `%${search}%`); params.push(`%${search}%`);
} }
if (status !== 'all') { if (status !== 'all') {
whereConditions.push('vm.status = ?'); whereConditions.push(`
CASE
WHEN COALESCE(vm.total_orders, 0) > 0 AND COALESCE(vm.order_fill_rate, 0) >= 75 THEN 'active'
WHEN COALESCE(vm.total_orders, 0) > 0 THEN 'inactive'
ELSE 'pending'
END = ?
`);
params.push(status); params.push(status);
} }
if (performance !== 'all') { if (performance !== 'all') {
switch (performance) { switch (performance) {
case 'excellent': case 'excellent':
whereConditions.push('vm.order_fill_rate >= 95'); whereConditions.push('COALESCE(vm.order_fill_rate, 0) >= 95');
break; break;
case 'good': case 'good':
whereConditions.push('vm.order_fill_rate >= 85 AND vm.order_fill_rate < 95'); whereConditions.push('COALESCE(vm.order_fill_rate, 0) >= 85 AND COALESCE(vm.order_fill_rate, 0) < 95');
break; break;
case 'fair': case 'fair':
whereConditions.push('vm.order_fill_rate >= 75 AND vm.order_fill_rate < 85'); whereConditions.push('COALESCE(vm.order_fill_rate, 0) >= 75 AND COALESCE(vm.order_fill_rate, 0) < 85');
break; break;
case 'poor': case 'poor':
whereConditions.push('vm.order_fill_rate < 75'); whereConditions.push('COALESCE(vm.order_fill_rate, 0) < 75');
break; break;
} }
} }
const whereClause = whereConditions.length > 0 const whereClause = 'WHERE ' + whereConditions.join(' AND ');
? 'WHERE ' + whereConditions.join(' AND ')
: '';
// Get total count for pagination // Get total count for pagination
const [countResult] = await pool.query(` const [countResult] = await pool.query(`
@@ -59,57 +63,105 @@ router.get('/', async (req, res) => {
// Get vendors with metrics // Get vendors with metrics
const [vendors] = await pool.query(` const [vendors] = await pool.query(`
SELECT SELECT DISTINCT
p.vendor as name, p.vendor as name,
vd.contact_name, COALESCE(vm.active_products, 0) as active_products,
vd.email, COALESCE(vm.total_orders, 0) as total_orders,
vd.phone, COALESCE(vm.avg_lead_time_days, 0) as avg_lead_time_days,
vm.status, COALESCE(vm.on_time_delivery_rate, 0) as on_time_delivery_rate,
vm.avg_lead_time_days, COALESCE(vm.order_fill_rate, 0) as order_fill_rate,
vm.on_time_delivery_rate, CASE
vm.order_fill_rate, WHEN COALESCE(vm.total_orders, 0) > 0 AND COALESCE(vm.order_fill_rate, 0) >= 75 THEN 'active'
vm.total_orders, WHEN COALESCE(vm.total_orders, 0) > 0 THEN 'inactive'
COUNT(DISTINCT p.product_id) as active_products ELSE 'pending'
END as status
FROM products p FROM products p
LEFT JOIN vendor_metrics vm ON p.vendor = vm.vendor LEFT JOIN vendor_metrics vm ON p.vendor = vm.vendor
LEFT JOIN vendor_details vd ON p.vendor = vd.vendor
${whereClause} ${whereClause}
GROUP BY p.vendor AND p.vendor IS NOT NULL AND p.vendor != ''
ORDER BY ${sortColumn} ${sortDirection} ORDER BY ${sortColumn} ${sortDirection}
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
`, [...params, limit, offset]); `, [...params, limit, offset]);
// Get cost metrics for these vendors
const vendorNames = vendors.map(v => v.name);
const [costMetrics] = await pool.query(`
SELECT
vendor,
ROUND(SUM(ordered * cost_price) / NULLIF(SUM(ordered), 0), 2) as avg_unit_cost,
SUM(ordered * cost_price) as total_spend
FROM purchase_orders
WHERE status = 'closed'
AND cost_price IS NOT NULL
AND ordered > 0
AND vendor IN (?)
GROUP BY vendor
`, [vendorNames]);
// Create a map of cost metrics by vendor
const costMetricsMap = costMetrics.reduce((acc, curr) => {
acc[curr.vendor] = {
avg_unit_cost: curr.avg_unit_cost,
total_spend: curr.total_spend
};
return acc;
}, {});
// Get overall stats // Get overall stats
const [stats] = await pool.query(` const [stats] = await pool.query(`
SELECT SELECT
COUNT(DISTINCT p.vendor) as totalVendors, COUNT(DISTINCT p.vendor) as totalVendors,
COUNT(DISTINCT CASE WHEN vm.status = 'active' THEN p.vendor END) as activeVendors, COUNT(DISTINCT CASE
COALESCE(ROUND(AVG(NULLIF(vm.avg_lead_time_days, 0)), 1), 0) as avgLeadTime, WHEN COALESCE(vm.total_orders, 0) > 0 AND COALESCE(vm.order_fill_rate, 0) >= 75
COALESCE(ROUND(AVG(NULLIF(vm.order_fill_rate, 0)), 1), 0) as avgFillRate, THEN p.vendor
COALESCE(ROUND(AVG(NULLIF(vm.on_time_delivery_rate, 0)), 1), 0) as avgOnTimeDelivery END) as activeVendors,
ROUND(AVG(NULLIF(vm.avg_lead_time_days, 0)), 1) as avgLeadTime,
ROUND(AVG(NULLIF(vm.order_fill_rate, 0)), 1) as avgFillRate,
ROUND(AVG(NULLIF(vm.on_time_delivery_rate, 0)), 1) as avgOnTimeDelivery
FROM products p FROM products p
LEFT JOIN vendor_metrics vm ON p.vendor = vm.vendor LEFT JOIN vendor_metrics vm ON p.vendor = vm.vendor
WHERE p.vendor IS NOT NULL AND p.vendor != ''
`);
// Get overall cost metrics
const [overallCostMetrics] = await pool.query(`
SELECT
ROUND(SUM(ordered * cost_price) / NULLIF(SUM(ordered), 0), 2) as avg_unit_cost,
SUM(ordered * cost_price) as total_spend
FROM purchase_orders
WHERE status = 'closed'
AND cost_price IS NOT NULL
AND ordered > 0
AND vendor IS NOT NULL AND vendor != ''
`); `);
res.json({ res.json({
vendors: vendors.map(vendor => ({ vendors: vendors.map(vendor => ({
...vendor, vendor_id: vendor.vendor_id || vendor.name,
name: vendor.name,
status: vendor.status,
avg_lead_time_days: parseFloat(vendor.avg_lead_time_days || 0), avg_lead_time_days: parseFloat(vendor.avg_lead_time_days || 0),
on_time_delivery_rate: parseFloat(vendor.on_time_delivery_rate || 0), on_time_delivery_rate: parseFloat(vendor.on_time_delivery_rate || 0),
order_fill_rate: parseFloat(vendor.order_fill_rate || 0), order_fill_rate: parseFloat(vendor.order_fill_rate || 0),
total_orders: parseInt(vendor.total_orders || 0), total_orders: parseInt(vendor.total_orders || 0),
active_products: parseInt(vendor.active_products || 0) active_products: parseInt(vendor.active_products || 0),
avg_unit_cost: parseFloat(costMetricsMap[vendor.name]?.avg_unit_cost || 0),
total_spend: parseFloat(costMetricsMap[vendor.name]?.total_spend || 0)
})), })),
stats: { stats: {
...stats[0], totalVendors: parseInt(stats[0].totalVendors || 0),
activeVendors: parseInt(stats[0].activeVendors || 0),
avgLeadTime: parseFloat(stats[0].avgLeadTime || 0), avgLeadTime: parseFloat(stats[0].avgLeadTime || 0),
avgFillRate: parseFloat(stats[0].avgFillRate || 0), avgFillRate: parseFloat(stats[0].avgFillRate || 0),
avgOnTimeDelivery: parseFloat(stats[0].avgOnTimeDelivery || 0) avgOnTimeDelivery: parseFloat(stats[0].avgOnTimeDelivery || 0),
avgUnitCost: parseFloat(overallCostMetrics[0].avg_unit_cost || 0),
totalSpend: parseFloat(overallCostMetrics[0].total_spend || 0)
}, },
pagination: { pagination: {
total: countResult[0].total, total: parseInt(countResult[0].total || 0),
pages: Math.ceil(countResult[0].total / limit), currentPage: page,
current: page, pages: Math.ceil(parseInt(countResult[0].total || 0) / limit),
limit
} }
}); });
} catch (error) { } catch (error) {

View File

@@ -37,7 +37,7 @@ export function Categories() {
parent: "all", parent: "all",
performance: "all", performance: "all",
}); });
const [sorting, setSorting] = useState({ const [] = useState({
column: 'name', column: 'name',
direction: 'asc' direction: 'asc'
}); });

View File

@@ -87,7 +87,7 @@ interface PurchaseOrdersResponse {
export default function PurchaseOrders() { export default function PurchaseOrders() {
const [purchaseOrders, setPurchaseOrders] = useState<PurchaseOrder[]>([]); const [purchaseOrders, setPurchaseOrders] = useState<PurchaseOrder[]>([]);
const [vendorMetrics, setVendorMetrics] = useState<VendorMetrics[]>([]); const [, setVendorMetrics] = useState<VendorMetrics[]>([]);
const [costAnalysis, setCostAnalysis] = useState<CostAnalysis | null>(null); const [costAnalysis, setCostAnalysis] = useState<CostAnalysis | null>(null);
const [summary, setSummary] = useState<ReceivingStatus | null>(null); const [summary, setSummary] = useState<ReceivingStatus | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -391,39 +391,6 @@ export default function PurchaseOrders() {
</div> </div>
)} )}
{/* Vendor Performance */}
<Card className="mb-6">
<CardHeader>
<CardTitle>Vendor Performance</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Vendor</TableHead>
<TableHead>Total Orders</TableHead>
<TableHead>Avg Delivery Days</TableHead>
<TableHead>Fulfillment Rate</TableHead>
<TableHead>Avg Unit Cost</TableHead>
<TableHead>Total Spend</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{vendorMetrics.map((vendor) => (
<TableRow key={vendor.vendor_name}>
<TableCell>{vendor.vendor_name}</TableCell>
<TableCell>{vendor.total_orders.toLocaleString()}</TableCell>
<TableCell>{vendor.avg_delivery_days?.toFixed(1) || 'N/A'}</TableCell>
<TableCell>{formatPercent(vendor.fulfillment_rate)}</TableCell>
<TableCell>${formatNumber(vendor.avg_unit_cost)}</TableCell>
<TableCell>${formatNumber(vendor.total_spend)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Cost Analysis */} {/* Cost Analysis */}
<Card> <Card>
<CardHeader> <CardHeader>

View File

@@ -12,15 +12,14 @@ import config from "../config";
interface Vendor { interface Vendor {
vendor_id: number; vendor_id: number;
name: string; name: string;
contact_name: string;
email: string;
phone: string;
status: string; status: string;
avg_lead_time_days: number; avg_lead_time_days: number;
on_time_delivery_rate: number; on_time_delivery_rate: number;
order_fill_rate: number; order_fill_rate: number;
total_orders: number; total_orders: number;
active_products: number; active_products: number;
avg_unit_cost: number;
total_spend: number;
} }
interface VendorFilters { interface VendorFilters {
@@ -38,15 +37,26 @@ export function Vendors() {
status: "all", status: "all",
performance: "all", performance: "all",
}); });
const [sorting, setSorting] = useState({ const [] = useState({
column: 'name', column: 'name',
direction: 'asc' direction: 'asc'
}); });
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
queryKey: ["vendors"], queryKey: ["vendors", page, filters, sortColumn, sortDirection],
queryFn: async () => { queryFn: async () => {
const response = await fetch(`${config.apiUrl}/vendors`); const params = new URLSearchParams({
page: page.toString(),
limit: '50',
search: filters.search,
status: filters.status,
performance: filters.performance,
sortColumn,
sortDirection
});
const response = await fetch(`${config.apiUrl}/vendors?${params}`, {
credentials: 'include'
});
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();
}, },
@@ -62,9 +72,7 @@ export function Vendors() {
if (filters.search) { if (filters.search) {
const searchLower = filters.search.toLowerCase(); const searchLower = filters.search.toLowerCase();
filtered = filtered.filter(vendor => filtered = filtered.filter(vendor =>
vendor.name.toLowerCase().includes(searchLower) || vendor.name.toLowerCase().includes(searchLower)
vendor.contact_name?.toLowerCase().includes(searchLower) ||
vendor.email?.toLowerCase().includes(searchLower)
); );
} }
@@ -108,27 +116,15 @@ export function Vendors() {
// Calculate pagination // Calculate pagination
const paginatedData = useMemo(() => { const paginatedData = useMemo(() => {
const startIndex = (page - 1) * 50; if (!data?.vendors) return [];
return filteredData.slice(startIndex, startIndex + 50); return data.vendors;
}, [filteredData, page]); }, [data?.vendors]);
// Calculate stats from filtered data // Calculate stats from filtered data
const stats = useMemo(() => { const stats = useMemo(() => {
if (!filteredData.length) return data?.stats; if (!data?.stats) return null;
return data.stats;
const activeVendors = filteredData.filter(v => v.status === 'active').length; }, [data?.stats]);
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 => {
@@ -186,36 +182,38 @@ export function Vendors() {
<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">Avg Lead Time</CardTitle> <CardTitle className="text-sm font-medium">Total Spend</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{typeof stats?.avgLeadTime === 'number' ? stats.avgLeadTime.toFixed(1) : "..."} days</div> <div className="text-2xl font-bold">
${typeof stats?.totalSpend === 'number' ? stats.totalSpend.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 }) : "..."}
</div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Across all vendors Avg unit cost: ${typeof stats?.avgUnitCost === 'number' ? stats.avgUnitCost.toFixed(2) : "..."}
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
<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">Fill Rate</CardTitle> <CardTitle className="text-sm font-medium">Performance</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{typeof stats?.avgFillRate === 'number' ? 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 Fill rate / {typeof stats?.avgOnTimeDelivery === 'number' ? stats.avgOnTimeDelivery.toFixed(1) : "..."}% on-time
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
<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">On-Time Delivery</CardTitle> <CardTitle className="text-sm font-medium">Lead Time</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{typeof stats?.avgOnTimeDelivery === 'number' ? stats.avgOnTimeDelivery.toFixed(1) : "..."}%</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">
Average on-time rate Average delivery time
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@@ -265,12 +263,13 @@ export function Vendors() {
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead onClick={() => handleSort("name")} className="cursor-pointer">Name</TableHead> <TableHead onClick={() => handleSort("name")} className="cursor-pointer">Vendor</TableHead>
<TableHead onClick={() => handleSort("contact_name")} className="cursor-pointer">Contact</TableHead>
<TableHead onClick={() => handleSort("status")} className="cursor-pointer">Status</TableHead> <TableHead onClick={() => handleSort("status")} className="cursor-pointer">Status</TableHead>
<TableHead onClick={() => handleSort("avg_lead_time_days")} className="cursor-pointer">Lead Time</TableHead> <TableHead onClick={() => handleSort("avg_lead_time_days")} className="cursor-pointer">Lead Time</TableHead>
<TableHead onClick={() => handleSort("on_time_delivery_rate")} className="cursor-pointer">On-Time Rate</TableHead> <TableHead onClick={() => handleSort("on_time_delivery_rate")} className="cursor-pointer">On-Time %</TableHead>
<TableHead onClick={() => handleSort("order_fill_rate")} className="cursor-pointer">Fill Rate</TableHead> <TableHead onClick={() => handleSort("order_fill_rate")} className="cursor-pointer">Fill Rate</TableHead>
<TableHead onClick={() => handleSort("avg_unit_cost")} className="cursor-pointer">Avg Unit Cost</TableHead>
<TableHead onClick={() => handleSort("total_spend")} className="cursor-pointer">Total Spend</TableHead>
<TableHead onClick={() => handleSort("total_orders")} className="cursor-pointer">Orders</TableHead> <TableHead onClick={() => handleSort("total_orders")} className="cursor-pointer">Orders</TableHead>
<TableHead onClick={() => handleSort("active_products")} className="cursor-pointer">Products</TableHead> <TableHead onClick={() => handleSort("active_products")} className="cursor-pointer">Products</TableHead>
</TableRow> </TableRow>
@@ -278,17 +277,13 @@ export function Vendors() {
<TableBody> <TableBody>
{isLoading ? ( {isLoading ? (
<TableRow> <TableRow>
<TableCell colSpan={8} className="text-center py-8"> <TableCell colSpan={9} className="text-center py-8">
Loading vendors... Loading vendors...
</TableCell> </TableCell>
</TableRow> </TableRow>
) : paginatedData.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>
<div>{vendor.contact_name}</div>
<div className="text-sm text-muted-foreground">{vendor.email}</div>
</TableCell>
<TableCell>{vendor.status}</TableCell> <TableCell>{vendor.status}</TableCell>
<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>
@@ -300,13 +295,15 @@ export function Vendors() {
{getPerformanceBadge(vendor.order_fill_rate ?? 0)} {getPerformanceBadge(vendor.order_fill_rate ?? 0)}
</div> </div>
</TableCell> </TableCell>
<TableCell>${typeof vendor.avg_unit_cost === 'number' ? vendor.avg_unit_cost.toFixed(2) : "0.00"}</TableCell>
<TableCell>${typeof vendor.total_spend === 'number' ? vendor.total_spend.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 }) : "0"}</TableCell>
<TableCell>{vendor.total_orders?.toLocaleString() ?? 0}</TableCell> <TableCell>{vendor.total_orders?.toLocaleString() ?? 0}</TableCell>
<TableCell>{vendor.active_products?.toLocaleString() ?? 0}</TableCell> <TableCell>{vendor.active_products?.toLocaleString() ?? 0}</TableCell>
</TableRow> </TableRow>
))} ))}
{!isLoading && !paginatedData.length && ( {!isLoading && !paginatedData.length && (
<TableRow> <TableRow>
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground"> <TableCell colSpan={9} className="text-center py-8 text-muted-foreground">
No vendors found No vendors found
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -315,7 +312,7 @@ export function Vendors() {
</Table> </Table>
</div> </div>
{filteredData.length > 0 && ( {data?.pagination && data.pagination.total > 0 && (
<motion.div <motion.div
layout="position" layout="position"
transition={{ duration: 0.15 }} transition={{ duration: 0.15 }}
@@ -333,7 +330,7 @@ export function Vendors() {
aria-disabled={page === 1} aria-disabled={page === 1}
/> />
</PaginationItem> </PaginationItem>
{Array.from({ length: Math.ceil(filteredData.length / 50) }, (_, i) => ( {Array.from({ length: data.pagination.pages }, (_, i) => (
<PaginationItem key={i + 1}> <PaginationItem key={i + 1}>
<PaginationLink <PaginationLink
href="#" href="#"
@@ -352,9 +349,9 @@ export function Vendors() {
href="#" href="#"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
if (page < Math.ceil(filteredData.length / 50)) setPage(p => p + 1); if (page < data.pagination.pages) setPage(p => p + 1);
}} }}
aria-disabled={page >= Math.ceil(filteredData.length / 50)} aria-disabled={page >= data.pagination.pages}
/> />
</PaginationItem> </PaginationItem>
</PaginationContent> </PaginationContent>